The payment comes back declined.
Support sees a red X. Product logs “failed.” Ops files it under “card didn’t work.” Engineering retries the same charge three more times because “maybe the network was flaky.”
Sometimes that works. Often it makes things worse.
Card rails do not have one decline. They have families of them. Soft declines mean try again later, or try a different way. Hard declines mean stop. If your product treats every decline as the same failure, you will hammer good cards, train issuers to distrust you, and lose sales you could have recovered with a quieter retry.

Soft and hard are different answers

A soft decline is temporary or situational. Insufficient funds right now. Issuer unavailable. Suspected fraud that might clear on a second look. Do not honor — try again. Velocity limit for today. The card is fine. The moment is not.
A hard decline is a closed door. Stolen card. Lost card. Invalid account. Closed account. Do not retry. Fraud confirmed. The issuer is telling you not to ask again with the same credentials.
Those two answers need different product behavior. One belongs in a retry queue with backoff. The other belongs in a clear customer message and a stop. Mixing them is how you turn a recoverable miss into a blocked BIN or a chargeback later.
You do not need to memorize every response code on day one. You do need a map: this code family is soft, that family is hard, and unknown codes default to careful, not aggressive.
Blind retries are not resilience

After an outage, teams often flip on “retry everything that failed.” It feels responsible. It floods the network with the same hard declines that already said no, plus the soft ones that would have recovered if you waited an hour instead of a second.
Issuers notice patterns. Repeated attempts on the same hard-declined PAN look like testing. Soft declines hit in a tight loop look like abuse. Either way, your approval rate can drop on the next clean day for no product reason — only because yesterday’s recovery job was loud.
Resilience here is boring:
- Retry soft declines on a schedule, not instantly in a loop.
- Cap attempts per payment id.
- Do not retry hard declines at all.
- Prefer a new customer action (update card, pick another method) over silent re-posts of the same payload.
- Keep your own payment id stable so a manual retry does not double-charge when the first one actually went through (that is a different post, and it still applies).
If your idempotency story is solid and your decline taxonomy is mush, you still lose. You will safely not double-charge while safely annoying the issuer.
Show the customer a useful sentence

“Payment failed” is almost never the right line.
For soft declines, say something that matches the truth: try again in a bit, or check the balance, or your bank asked us to wait. Offer a scheduled retry when that fits the product. Do not pretend it is permanent.
For hard declines, be direct: this card was declined and we will not keep trying it. Ask for another card or another rail. Soft language here just creates three more support tickets and three more hard attempts.
Ops needs the raw code and the family. Customers need the consequence. One screen that dumps 05 or 51 at a person is not honesty — it is homework. Translate once in the product so support and the UI stay aligned.
Put the rules in code, not in Slack

Someone on the team always knows which codes are soft. That knowledge lives in a thread, a Notion page, or a processor PDF from 2019. Then a new engineer ships a generic retry, and Friday’s queue fills with the same hard declines again.
Encode the map.
- A table of processor/network codes → soft | hard | review.
- Retry policy per soft family: delay, max attempts, whether to require a customer click.
- Hard family: zero automatic retries, clear UX path to change method.
- A review bucket for codes you have not classified yet — default to no auto-retry until someone owns the label.
- Logs that record family and attempt count, so you can see whether recovery is helping or just adding volume.
Processors differ. The same human meaning can arrive under different codes. Keep the map per integration, not one global fantasy list. When you add a second processor, copy the discipline, not the integers.
What this looks like in a real queue

A batch of checkouts fails after lunch. Half are soft (issuer timeout, insufficient funds). Half are hard (stolen, closed).
Without a map, you retry all of them every five minutes until someone kills the job. Approval on the soft half is mixed. The hard half is pure noise. Issuer risk scores tick up.
With a map, soft items enter a backoff queue. Hard items stop and prompt for a new method. Support sees two queues with reasons, not one pile called “failed.” Finance sees fewer mystery second attempts. Product sees recovery rate on soft only — which is the number that actually means something.
That is the whole lesson. Decline is not a boolean. Soft is not no. Hard is not “try harder.”
What to check this week

- Does your retry job filter on decline family, or on “status = failed”?
- Can support tell a customer whether you will try again without reading a processor portal?
- After the last incident, did your recovery script re-post hard declines?
If those answers are fuzzy, start with the map. Ten codes classified beats a clever retry with none.
Futurify builds the software around your payments: the states, the queues, and the rules that match how networks actually answer. We don’t hold funds. If you want to talk it through, email us at hello@futurify.io.