> ## Documentation Index
> Fetch the complete documentation index at: https://docs.revtain.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Dunning Sequences

> Wire it once. Every future recovery failure routes through your domain automatically.

Dunning is webhook-driven. Revtain hands you a one-tap card-update URL the moment a recovery exhausts retries. Your existing email, SMS, or push infrastructure delivers it — from your domain, with your branding. Wire the integration once; every failure after that runs automatically.

<Info>
  **Revtain does not send customer-facing emails or SMS.** Customers trust messages from the merchant they signed up with. Deliverability and brand-trust both win when the message comes from your domain rather than a recovery vendor.
</Info>

## Using a billing platform? Zero dunning code.

Clients on **Chargebee**, **Stripe Billing**, **Recurly**, **Braintree Subscriptions**, **Zuora**, or **Shopify Recharge** ship dunning with no extra integration. The connector receives recovery outcomes from Revtain and dispatches branded emails through the platform's existing sender configuration — already pointed at your domain.

Configure the connector webhook URL once during onboarding. Everything else runs through your billing platform's own dunning engine, using the templates, schedule, and sender address you've already set up there.

## Direct-API clients: ship the handler once

Write a single webhook endpoint that listens for `recovery.failed` and branches on the `recommendedAction` field. Because you own the customer relationship, you already hold the customer's email — match the event back to your invoice (via `revtainTransactionId`, or the `idempotencyKey` you supplied to `/execute`) and send the message from your own infrastructure. Wire it once; every future failure runs through it.

<Info>
  `recovery.failed` does **not** contain the customer's email address or a card-update link — by design. You know your own customer, and for the current direct-gateway model the customer updates their card in **your** billing portal or checkout, not on a Revtain-hosted page. Branch on `recommendedAction` to decide what to do.
</Info>

The handler is short. Below are two ready-to-paste examples — drop into your backend, change the lookup + send calls to your own, and you're done.

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  import express from 'express';
  import crypto from 'crypto';

  const app = express();

  // Required for HMAC signature verification — keep the raw body.
  app.post('/webhooks/revtain', express.raw({ type: 'application/json' }), async (req, res) => {
    // 1. Verify the webhook came from Revtain (constant-time).
    const signature = req.header('X-Revtain-Signature') || '';
    const expected = crypto
      .createHmac('sha256', process.env.REVTAIN_WEBHOOK_SECRET)
      .update(req.body)
      .digest('hex');
    const ok = signature.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
    if (!ok) return res.status(401).end();

    const event = JSON.parse(req.body.toString());

    // 2. Route on event type + recommended action.
    if (event.event === 'recovery.failed') {
      const invoice = await yourDb.findInvoiceByRevtainTxn(event.revtainTransactionId);
      if (event.recommendedAction === 'request_card_update') {
        await yourEmailProvider.send({
          from: 'billing@yourdomain.com',
          to: invoice.customerEmail,                         // you already have this
          template: 'payment-failed',
          vars: { updateUrl: yourBillingPortalUrl(invoice), amount: event.amount },
        });
      }
      // 'retry_later' → do nothing, Revtain may retry. 'manual_review' → flag the account.
    }

    res.json({ received: true });
  });
  ```

  ```python Python (Flask) theme={null}
  import os, hmac, hashlib, json
  from flask import Flask, request, abort, jsonify

  app = Flask(__name__)

  @app.post('/webhooks/revtain')
  def revtain_webhook():
      # 1. Verify the webhook came from Revtain.
      raw = request.get_data()
      expected = hmac.new(
          os.environ['REVTAIN_WEBHOOK_SECRET'].encode(),
          raw,
          hashlib.sha256,
      ).hexdigest()
      if not hmac.compare_digest(request.headers.get('X-Revtain-Signature', ''), expected):
          abort(401)

      event = json.loads(raw)

      # 2. Route on event type + recommended action.
      if event.get('event') == 'recovery.failed':
          invoice = your_db.find_invoice_by_revtain_txn(event['revtainTransactionId'])
          if event.get('recommendedAction') == 'request_card_update':
              your_email_provider.send(
                  from_addr='billing@yourdomain.com',
                  to=invoice.customer_email,                 # you already have this
                  template='payment-failed',
                  vars={'updateUrl': your_billing_portal_url(invoice), 'amount': event['amount']},
              )
          # 'retry_later' -> do nothing. 'manual_review' -> flag the account.

      return jsonify(received=True)
  ```
</CodeGroup>

That is the entire integration. The endpoint runs forever — every future recovery failure for every customer fires it.

## What happens end to end

<Steps>
  <Step title="Recovery engine exhausts retries">
    The payment ran through the available retry paths on the gateway that issued the original token. None succeeded.
  </Step>

  <Step title="Your webhook fires">
    `recovery.failed` arrives with the decline code, `recommendedAction`, and `revtainTransactionId`. You match it back to your invoice and customer.
  </Step>

  <Step title="Your existing infrastructure delivers the message">
    When `recommendedAction` is `request_card_update`, your handler emails the customer from your domain with a link into your billing portal / checkout to re-enter their card.
  </Step>

  <Step title="Customer updates their payment method">
    They update their card where your other billing already lives. Pass the refreshed gateway token on your next `/execute` call for that customer.
  </Step>

  <Step title="Recovery completes">
    The new card on file, or a later scheduled retry, closes the loop. A success fires `recovery.success` so you can mark the invoice paid.
  </Step>
</Steps>

<Note>
  Revtain also offers a fully hosted card-update page via [`POST /api/recovery/update-card/generate`](/guides/card-update-links). For clients whose gateway is Stripe, the customer lands on a secure Stripe-hosted form and the new card attaches automatically — the `card.updated` webhook then tells you the fresh token. Use whichever suits your flow: your own billing portal (above) or the hosted link. For other gateway configurations, your onboarding contact will confirm availability.
</Note>

## Operator alerts (for your team)

If you want Revtain to notify your team — not your customer — when a recovery fails, add those channels during onboarding:

```json theme={null}
{
  "notificationChannels": ["WEBHOOK", "EMAIL", "SMS"]
}
```

These alerts go to you, not the customer. Informational only: *a \$25.00 payment failed, consider reviewing the account.*

## Why webhook-only

Customer-facing email from a third-party recovery vendor lands in promotions or spam — the customer never opted into hearing from us, only from you. Email from your domain lands in the inbox. The webhook handoff keeps your sender reputation intact, your brand on every message, and your team in control of timing, copy, and channel.

Revtain runs the recovery engine. You run the customer relationship.
