Back to BlogTroubleshooting

Failed and declined payments: recovery, retries and smart dunning

Rupak Amin

Founder & Lead Engineer, RAITHub

8 min read

RAITHub ships and tests production software. See QA as a Service or talk to us.

Most failed payments are recoverable, but only the right ones. Split declines into hard (do not retry) and soft (retry on a schedule). Retry soft declines a few times over days, not minutes, and pair each retry with a dunning email that lets the customer fix the card themselves. Treat lost renewals as involuntary churn, not as cancellations.

If you would rather have recovery built for you, see how RAITHub would build it at the end of this guide.

Why do card payments fail in the first place?

A decline is the issuing bank saying no, and the reason is carried in a code your gateway passes through. The single most important split is hard versus soft. A hard decline will fail again if you retry it; a soft decline often succeeds a day or two later, once funds arrive or a temporary hold clears. Retrying a hard decline wastes a fee and can look like card testing to a fraud system.

Decline typeExamplesRetry?What to do
Soft, temporaryInsufficient funds, issuer unavailable, do-not-honorYes, on a scheduleRetry over days; email the customer
Expired or wrong cardExpired card, incorrect number or CVCOnly after the customer updates itPrompt for a new card; do not blind-retry
Hard, finalStolen card, lost card, pickup card, revoked authorizationNoStop; ask for a different method
Fraud or risk blockDeclined by the gateway's risk rulesNoReview; retrying looks like card testing

Stripe documents the decline and error codes it returns in its error codes reference, and marks which are safe to retry. Read the codes your own gateway sends and map each to one of the rows above before you build any retry logic.

How should I time payment retries?

Spread them over days, and stop early. Hammering a card every few minutes rarely changes the bank's answer, burns a processing fee on each attempt, and is exactly the pattern fraud systems flag. A simple, effective schedule for a soft decline is a small number of attempts across a week, backing off as you go.

  • Attempt 1: the original charge.
  • Attempt 2: after roughly three days, when temporary holds and low balances often clear.
  • Attempt 3: after another few days, paired with a stronger email.
  • Stop after three to four tries and move the account to a manual or cancellation flow. Endless retries annoy customers and keep paying fees.

Managed billing products already do this. Stripe's revenue recovery documentation describes retrying failed subscription payments, and its smart retries feature times retries using signals about when a card is more likely to succeed. If you are on a billing platform, turn these on before you build anything. Build your own schedule only when you are not on managed billing, or you need rails the platform does not support.

What does retry and dunning logic look like in code?

Keep a small state machine per failed invoice: how many attempts, when the next one is due, and whether the decline was retryable at all. The sketch is TypeScript and deliberately minimal.

type DeclineClass = 'soft' | 'card_fix' | 'hard'

// Days to wait before each retry of a soft decline.
const RETRY_SCHEDULE_DAYS = [3, 3, 4] as const

function classify(declineCode: string): DeclineClass {
  const soft = new Set(['insufficient_funds', 'issuer_not_available', 'do_not_honor'])
  const cardFix = new Set(['expired_card', 'incorrect_number', 'incorrect_cvc'])
  if (soft.has(declineCode)) return 'soft'
  if (cardFix.has(declineCode)) return 'card_fix'
  return 'hard' // stolen, lost, pickup, fraud: never auto-retry
}

function planNextAttempt(
  declineCode: string,
  attemptsMade: number,
): { action: 'retry'; runAt: Date } | { action: 'ask_customer' } | { action: 'stop' } {
  const kind = classify(declineCode)
  if (kind === 'hard') return { action: 'stop' }
  if (kind === 'card_fix') return { action: 'ask_customer' }
  if (attemptsMade >= RETRY_SCHEDULE_DAYS.length) return { action: 'stop' }
  const waitDays = RETRY_SCHEDULE_DAYS[attemptsMade]
  const runAt = new Date(Date.now() + waitDays * 24 * 60 * 60 * 1000)
  return { action: 'retry', runAt }
}

Two rules keep this safe. Every retry must be idempotent: send the same idempotency key per attempt so a webhook replay or a crash mid-retry cannot double-charge the customer. The pattern is in idempotency in API design, and the double-charge failure mode is covered in stopping duplicate payments. And every attempt and outcome belongs in an audit log, so you can prove to a customer what happened and when.

How do I write dunning emails that actually recover payment?

Dunning is the sequence of messages that ask a customer to fix a failed payment. The email does more work than the retry, because most recoverable failures need the customer to act. Make it easy and specific.

  • Say what happened, plainly. "Your payment for October did not go through." Not a vague "action required".
  • One obvious button: update the card. Link straight to a hosted update page, so the customer never re-enters other details.
  • Escalate tone, not frequency. A friendly first note, a clearer second, a final notice with the date access ends.
  • Tell them the deadline. People act on "your account pauses on the 20th", not on "please update soon".
  • Stop when they pay or cancel. Nothing erodes trust like a dunning email after the card already went through.

Buy, build or hire?

Most teams should start with their billing platform's recovery tools. Build only when you are off managed billing or your rails are not supported.

OptionExampleChoose this whenWhere it stops
Managed billing recoveryStripe smart retries and dunningYou use the platform's subscriptions and cards it supportsOnly its own rails and card types
Dunning SaaS add-onThird-party retry and email toolsYou want ready-made email sequences on top of your billingMonthly cost; another vendor in the loop
No-code email automationTrigger emails on a failed-payment webhookLow volume, simple reminders are enoughNo smart retry timing; manual logic
Custom recovery engineYour own state machine plus emailsLocal wallets, several gateways, or off-platform billingYou own the schedule, fraud care and maintenance

How long does it take to build recovery yourself?

Our estimate, for a developer who knows the gateway's API: one to two days to turn on and configure managed retries and dunning; one to two weeks to build a custom retry state machine with a three-email dunning sequence, idempotent retries and an audit log; longer if you support several gateways, each with its own decline codes. The main risk of doing it yourself is double-charging on retry, which turns a recovery feature into a refund-and-apology feature.

How RAITHub would build this

  • Classify declines: map every decline code your gateways return to retry, ask-customer or stop.
  • Retry safely: a scheduled, idempotent retry state machine that backs off over days and stops early.
  • Dunning: an escalating email sequence linking to a hosted card-update page, that halts the moment the customer pays or cancels.
  • Measure: a recovery dashboard and an audit log of every attempt and outcome.

Timeline: 4–6 weeks as a SaaS feature on a fixed scope, shorter if you are on managed billing and only need configuration and emails. What you receive: automated tests and CI, handover docs, and full IP under NDA. Production and financial data stay in your own cloud account; development uses synthetic data.

RAITHub is an engineering studio and has not shipped a regulated or licensed fintech product; how you handle refunds and billing terms is a question for your accountant and adviser, and this is general information. Proof we can point to: PadhAI, built by RAITHub, was designed for 9 payment gateways behind one abstraction, and TheSkinProof, the founder's own venture, runs multi-gateway payments with 750+ automated tests. Next step: a free 15-minute technical audit, then a written fixed quote. Book the audit. More is on the SaaS development page, the FinTech page, and in our payments engineering guide. If retries are double-charging, start with stopping duplicate payments.

Frequently asked questions

Which failed payments are worth retrying?

Soft, temporary declines such as insufficient funds or a temporary issuer problem. Hard declines (stolen, lost, pickup, fraud) will fail again, and expired or wrong-card declines need the customer to update the card first. Read the decline code and classify before retrying.

How many times should I retry a failed payment?

Three to four attempts across about a week for a soft decline, then stop and move to a manual or cancellation flow. Retrying every few minutes rarely helps, costs a fee each time, and can look like card testing to fraud systems.

What is dunning?

The sequence of messages asking a customer to fix a failed payment. Good dunning explains what happened, links to a hosted card-update page, escalates in tone with a clear deadline, and stops as soon as the customer pays or cancels.

What is involuntary churn?

Customers you lose to a failed renewal payment rather than a decision to cancel. It is often recoverable with retries and dunning, which is why lost renewals should be treated as a recovery problem, not a cancellation.

How do I retry without double-charging?

Make each attempt idempotent: send the same idempotency key for a given attempt so a webhook replay or a crash cannot create a second charge, and record every attempt in an audit log so you can prove what happened.

failed payment recoverydunningpayment retriesinvoluntary churnFinTechsubscriptions

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.