The timeline
Renewal attempt fails
The billing engine tries to charge the card at
next_bill_at. The card declines.- Subscription moves to
past_due - Invoice stays
open subscription.past_duewebhook fires- Customer still has access — don’t cut them off yet
Retry day +1
We try again 24 hours later. If it succeeds:
subscription.recovered, invoice.paid, access continues as normal.If it fails: we move to the next retry.Retry day +7 (payday-aware)
Fourth attempt. If day +7 falls after the 25th of the month, we move this retry to the 25th instead — the most common salary/payroll date in Nigeria. Higher probability of funds being available.
Retry day +14
Final attempt. If this fails, the subscription moves to
grace.subscription.gracewebhook fires- Customer still has access during grace (default 7 days, configurable)
Soft vs hard declines
We classify decline codes into two categories:| Type | What it means | Our response |
|---|---|---|
| Soft decline | Temporary issue — insufficient funds, daily limit reached, temporary hold | Retry on schedule |
| Hard decline | Permanent issue — stolen card, account closed, invalid card number | Skip straight to grace. Retrying is pointless and may upset the cardholder. |
do_not_honor, stolen_card, lost_card, account_closed, invalid_account.
Common soft codes: insufficient_funds, withdrawal_limit_exceeded, try_again_later.
Recovery via bank transfer
Duringpast_due or grace, the customer can pay the outstanding invoice by bank transfer to their virtual account — even if they’re on a card subscription. We auto-reconcile it and restore access immediately. No manual intervention needed on your end.
subscription.recovered fires as soon as the transfer is matched.
Webhooks to handle
| Event | What to do in your app |
|---|---|
subscription.past_due | Show an in-app banner: “Your payment failed. Please update your card.” |
subscription.recovered | Clear the banner, restore any restricted features |
subscription.grace | Show a stronger warning: “X days left to resolve payment or access will be cut” |
subscription.delinquent | Revoke access. Show a blocked screen with payment instructions. |
What to do in your app
Onsubscription.past_due:
- Show a non-blocking warning banner
- Send a payment failure email with a link to update their card
- Don’t block features yet — most cards recover within a retry
subscription.grace:
- Show a countdown: “Your account will be suspended in N days”
- Send escalating emails (day 1, day 5, day 7 of grace)
- Optionally block non-critical features to create urgency
subscription.delinquent:
- Block access to paid features
- Redirect to a payment recovery page showing their VA account details or a card update link
- Keep their data — don’t delete anything. They might pay tomorrow.
subscription.recovered:
- Restore access immediately
- Send a “welcome back” confirmation email
- Clear any warning states in your UI
Recovering with “Update payment”
Automatic retries only work when there’s a card token to charge. For a subscription with no card on file (e.g. it was funded by bank transfer), or when the customer wants to pay right now, generate a checkout and let them settle the outstanding invoice:checkoutLink. On settlement Plinth marks the outstanding
invoice paid, returns the subscription to active, advances it into the paid period, and emits
subscription.recovered. This works from past_due, grace, or delinquent.
Paying by card captures a token, so future renewals auto-charge and dunning won’t recur. Paying
by transfer recovers the current period but leaves no token — the next renewal will prompt for
payment again. Surface an “add a card to auto-renew” nudge for transfer-funded subscriptions.