← Back to Blog
·11 min read·Yarin Goldstein

Stripe do_not_honor: The Decline That Tells You Nothing?

do_not_honor is the bank saying no without an explicit reason. Why the standard advice does not work for subscription renewals, and my take on retrying and emailing the customer.

I'm Yarin, solo founder of RecoverStack, which is a dunning tool, so I have an obvious interest in making you believe failed payments are worth thinking carefully about. I'll try to earn the read by telling you where I think I might be wrong, because on this particular code I am less sure of my own logic than on any other one in my engine.

do_not_honor is the code I'd bet on being wrong about, if I'm wrong about any of them. I said that in the bootstrapped-dunning post a few weeks ago and then realized I'd never actually written down the reasoning.

What do_not_honor means

do_not_honor is a Stripe decline_code that means the card issuer refused the charge and did not provide an explicit reason for it. It is not a specific failure, it's the absence of a stated one.

Stripe's own label for it in my engine's friendly-names map is "Bank declined without specific reason", which is about as much as anyone can honestly say. The Stripe declines documentation doesn't do much better, and that isn't Stripe documenting it lazily, the information isn't there at all. The issuer had a reason but did not transmit it, and what reaches us is a refusal with the reason field empty.

That vagueness is the entire problem with this code, and it's also why so much of the advice written about it is unhelpful in a specific way I'll get to.

If you go looking for more on this one, note that in the older numeric world of ISO 8583 response codes it is 05. Adyen's write-up puts it plainly: the Do Not Honor response is "often labeled as ISO 8583 code 05", and the reason the bank stays quiet is deliberate, because "a detailed reason ... could potentially help a fraudster bypass security". A lot of the payments writing out there is keyed on that number rather than on Stripe's string, so if you search the number you'll find a different and mostly older body of material about the same problem.

The advice everyone gives, and why it doesn't work for renewals

Search for this code and you will land, very quickly, on the same answer. "Ask the customer to call their bank". Google's AI answer for it says that, citing Stripe, and it isn't wrong exactly.

It's just written for a checkout.

At a checkout, the customer is sitting there with a card in their hand and a browser tab open. Telling them to phone their issuer is actually reasonable, because they're present and motivated to let the charge go through, and the whole thing resolves in ten minutes. That is a real scenario and the advice fits it.

A subscription renewal is a different beast. The charge fires off-session, at whatever hour your billing cycle lands on, against a card the customer had saved months ago. Nobody is present, there is no tab open and no "moment" to act in. The customer doesn't really know the charge happened and failed, and won't know either until you tell them. "Have them call their bank" describes an action that has no one to perform it in real time.

So the standard advice isn't wrong, it's answering a different question than the one a subscription operator is asking. That gap is most of why I wrote this.

I'm not the only one who thinks so, which I was glad to find because I'd been reasoning about it on my own. Adyen splits the two cases in their own guidance: "in cases where your customer is not in-session (such as in a subscription), you'll need to bring your cardholder back to authenticate again." Same page also says not to retry immediately with the same details, because that "may look like fraud and lead to further declines". That second line is a large part of my retry strategy.

What I actually do: one retry, then stop

Here is the entry, copy-pasted from src/recovery/decline-engine.service.ts:

// Category 3 - Bank-block (one careful retry, then dunning)
do_not_honor: {
  retries: [{ delayDays: 1 }],
  emailSequence: 'card_update',
  category: 'bank_block',
},

One retry, one day later, then the customer gets emailed.

The reasoning for that is that do_not_honor covers at least two different situations that arrive wearing the same string. Some of them are a moment: a risk check at the issuer that tripped on this one charge, or a temporary hold that lifts on its own (Those recover on a retry). Others are a standing decision the bank has made about that card or about you as a merchant (Those will refuse a second time, and a fifth time, and a fiftieth)

You cannot tell which one you got when you get them, so one retry is a bet on the first kind, and stopping after one is an acknowledgement that I can't distinguish it from the second. A day is long enough for a temporary hold to lift and short enough that I haven't burned the valuable part of the dunning window where emailing still works well (recovery rates per dunning email peak on day 0 at around 13% and are down near 4% by day 15, per Baremetrics' analysis).

Why one and not three

This is the part that took me longest to settle, and the comparison that makes it clearest is with the code sitting right next to it.

Code Retries Then Why the difference
generic_decline 6h, 1 day, 7 days Standard dunning sequence No signal at all, so a wider net is cheap
do_not_honor 1 day, once Card-update email sequence There IS a signal, and it says the bank meant it
call_issuer none Email explicitly asking customers to phone the bank The bank named the fix, and it needs a human

generic_decline and do_not_honor look like the same failure but I treat them differently on purpose. generic_decline is the bank declining and not even reaching for a category. It gets three retries across a week because there's no evidence pointing either way.

do_not_honor is the bank picking a code that specifically means "not this one". That's a weak signal, but better then nothing, and it points away from retrying. So it gets one attempt instead of three, then the effort moves to emails instead.

There are ten codes in my bank-block category and only two of them retry at all. Eight get zero, because the bank has taken a standing position and another attempt won't move it: the two revocations, stop_payment_order, call_issuer, not_permitted, transaction_not_allowed, invalid_amount & do_not_try_again. generic_decline gets three, do_not_honor gets one. It sits in the middle of that spread on purpose, and if the middle is the wrong place for it, it's a self-contained thing to move later.

The layer that can cancel that retry before it fires

There's a second mechanism sitting on top of the table, and it can override the retry I just described.

When Stripe passes along a network advice code with the decline, the engine reads it. If that advice code is do_not_try_again, which Mastercard uses to say what it sounds like, the strategy gets run through a hard stop and the retries array is emptied before anything is scheduled. The email sequence is still happening, so the customer still hears from me, but the initial retry just never happens.

Which means the honest version of my strategy is: one retry, unless the network already told me not to bother. How often that second case fires I don't know yet, because Stripe doesn't guarantee an advice code on every do_not_honor and I have no traffic of my own to count.

There's also a separate thing that happens quietly. do_not_honor is on the list of five codes that flag a customer as a persistent-failure risk before their next renewal, alongside insufficient_funds, generic_decline, transaction_not_allowed and card_velocity_exceeded. A do_not_honor this month makes me look harder at that customer next month, independently of anything the retry did.

The email, and what bothers me about it

do_not_honor routes into the card-update email sequence (three touches, immediate, day 3, day 7). The first one opens like this:

Subject: Action required: Update your payment method

We were unable to process your recent payment because your card on file needs to be updated.

Read that against what the decline actually told me and you'll quickly see the problem. The bank never said the card needs updating. The bank said "no" and stopped talking. I'm the one who decided that "put a different card on" is the most useful thing the customer can do about a refusal nobody explained, and I wrote an email that states it as though the issuer had diagnosed it.

I've gone back and forth on this. The case for it is that the customer can only do a small number of things, and using a different card is the one most likely to work at that point, so telling them that is useful even when it's an inference. The case against is that it's a claim I don't have, presented as one I do, and if their card is fine they now think it isn't.

I haven't rewritten it, and I'm not fully comfortable with that, but it's on the list. Again, real data will be a better guide on what to do with these.

Common questions

What does do_not_honor mean on Stripe? The card issuer declined the charge and did not provide a reason. It is a refusal with an empty reason field, not a description of a specific problem.

Is do_not_honor worth retrying? Once, in my view. It covers both temporary holds that clear and standing bank decisions that never will, and the code looks identical in both cases. Repeated retries rarely help, and past a point they carry real cost (see below).

Why did my customer's card work last month and return do_not_honor this month? That's consistent with the temporary-hold version: an issuer-side risk check that tripped on this particular charge rather than on a problem with the card. It is also consistent with the bank having changed its mind about you as a merchant. However, the code will not tell you which is which.

Should I tell the customer to call their bank? At checkout time? probably yes. However, for an off-session subscription renewal there is nobody there to call anyone, so the first job is letting them know the charge failed. call_issuer is the code where phoning the bank is the direct fix, and it's a separate code for that reason.

How many times can I retry before it costs me money? Retrying past network limits carries per-transaction fees, and one of them got much more expensive on February 1, 2026. That deserves its own post rather than a paragraph here, and it's the next one I'm writing.

What I'm still unsure about

  • Whether one retry is one too many. Every other code in this category gets zero. I kept this one on a hunch about temporary issuer holds and no data of my own.
  • The email claiming the card needs updating, discussed above. The most likely thing I'll change first when real data flows.
  • Whether do_not_honor should be split at all. It might be two failures wearing one code, in which case the right move isn't a better retry rule, but looking for a second signal that separates them, then divert strategies.

I have zero customers and zero recovery data as of writing this, so everything above is reasoning from Stripe's documentation and from what the code does, not from outcomes. That matters more on this code than on most, because the whole argument turns on a guess about how often do_not_honor is temporary.

So if you're running subscriptions on Stripe and you have a pile of these in your dashboard, I'd rather have your numbers than my logic. How many of your do_not_honor declines cleared on a later attempt, and how long after? Even a rough count from one month would be worth more than anything I've reasoned out here. Reply to any RecoverStack email or hit me at yarin@recoverstack.dev, and I'll tell you what I see in it either way.

Yarin


The full code-by-code reference is the pillar, Stripe Decline Codes: What Each One Means and What I Plan to Do About It. For how these buckets fit a one-person setup, see Dunning for Bootstrapped SaaS. The two other per-code deep dives are insufficient_funds retries and expired_card recovery.

stripedecline-codesdo-not-honorpayment-recoverysaasdunning