Skip to content

Dunning — Failed Payment Recovery

When a subscription renewal fails, Dunning retries on a schedule you configure and lets you send the customer a signed, time-limited link to update their card without exposing any internal IDs.

Subscription renewals fail for many reasons — expired cards, insufficient funds, fraud-flag declines, lost cards. Dunning automates the recovery loop:

  1. Auto-retry — the failed payment is retried after a configurable delay (defaults: 3 days, 5 days, 7 days, then cancel at 14 days).
  2. Customer notification — the merchant can send the customer a “update your payment method” email with a signed link that lasts 72 hours.
  3. Tracking — every failure is logged with status pending → retrying → resolved | cancelled so the merchant has a clear view of recovery health.

Swft Dashboard → Dunning Management.

SettingDefaultPurpose
Enable dunningOffMaster toggle.
Retry 1 (days)3Days after the first failure before the first retry.
Retry 2 (days)5Days after retry 1.
Retry 3 (days)7Days after retry 2.
Cancel after (days)14Days after the first failure before the subscription is cancelled.

The retry schedule is cumulative — by default the customer’s card is retried at day 3, day 8, day 15, and then cancelled at day 17 if all three retries fail.

When you click Send update link in the Dunning page, the customer receives an email like:

Your recent payment failed. Update your payment method to keep your subscription active: [Update payment method]

The link points at https://checkout.swft.co.uk/update-card?token=<signed-token> and is valid for 72 hours. The token is HMAC-signed and encodes the failure ID, expiry, and signature — no raw IDs are exposed to the customer or anyone who might inspect the URL.

When the customer clicks through, they see a streamlined Swft Checkout that asks for new card details only (no shipping, no review). On success, the failure is marked resolved and the subscription resumes.

The Dunning page lists every recent failure with:

  • Customer email and order reference
  • Failed amount and currency
  • Retry attempt count and next retry timestamp
  • Current status (pending, retrying, resolved, cancelled)
  • Actions: Send update link, Mark resolved manually, Cancel subscription

Failed payments are stored in the subscription_failures table with the schema: id, merchant_id, subscription_id, customer_email, amount_pence, currency, status, retry_count, next_retry_at, created_at, resolved_at.

Retries are scheduled by a cron job that walks the subscription_failures table for status = 'pending' rows whose next_retry_at <= now(), calls Stripe’s retry endpoint, and either advances the retry counter or marks the row resolved / cancelled.

The update-card link is generated by the POST /api/dunning/send-update-link/:id endpoint, which builds a JWT-like token (failure_id | expiry | hmac) signed with a per-merchant secret. The receiving page (/update-card) verifies the signature before showing any failure detail.

Recovery emails are sent via Resend (RESEND_API_KEY in the API env). Delivery is fire-and-forget — if a send fails, the merchant can re-send from the dashboard.

  • The 72-hour token window is hard. If a customer misses it, you must re-send the link from the dashboard.
  • Email deliverability depends on your Swft sending domain’s SPF/DKIM. Talk to support if recovery emails land in spam.
  • Manually marking a failure resolved doesn’t retry the payment — use that for cases where the customer paid via another channel.
  • Dunning only handles subscription payments. One-off failed checkouts aren’t retried; the shopper has to start over.