Ran into this on a subscription platform after a batch of card reissues from one issuer. Payments started failing with decline code 61 (no such card) even though the customer's physical card was still active and working at other merchants. Turned out the card network's account updater service had already pushed a new expiration date and PAN reference to our acquirer weeks earlier, but our stored token in the vault still pointed to the old card data. Every renewal request went out with the stale expiration date, and this particular acquirer routed that straight to a hard decline instead of quietly mapping it to the new card. Nothing in the response told us the mismatch was the actual cause; it just looked like a normal decline.
Fixed it by subscribing to the account updater feed directly from the network side and refreshing our vault token ahead of the billing cycle instead of waiting for a decline to trigger it. Renewal failures on affected cards dropped from around 9% to under 1% the next cycle.
Anyone else building recurring billing on stored credentials, how are you handling the gap between when the network updates a card and when your acquirer's decline reason actually tells you why? Proactive updater feed, or retry-and-detect?
Top comments (4)
On your question, both, and not as a fallback. The updater feed is the mechanism and the decline path is the only honest measurement of it, because a decline the feed should have prevented is a coverage miss with a timestamp attached. Keep counting those after you stop depending on them.
The part I would look at next is the under 1 percent you are left with, because it is not one population. Some are cards where the updater had nothing to give you. The account closed with no replacement, so your vault is correct and the problem is now a conversation with the customer. The rest are timing, where the update landed between your refresh and the charge. Those want opposite fixes. One goes to dunning, the other you fix by moving the refresh closer to the billing attempt rather than earlier in the cycle. Rolled into one residual they mask each other in the reporting and you end up tuning whichever is louder.
The wider version of your last sentence is what I would fix at the API boundary whichever approach you pick. A decline reason that names a symptom leaves the caller unable to answer the one question a retry policy runs on: can this identical request ever succeed? A closed account is never. A stale token is yes, once refreshed. Your acquirer handed you the same shape for both, so that split is worth deriving yourself from updater state rather than hoping a reason code carries it.
Thanks, this is a helpful way to split it. We've been reporting the residual as one number, and you're right that it hides two different problems. The timing cases are the ones we can shrink on our side, so the next step is moving the refresh to run right before each billing attempt instead of once at the start of the cycle. The closed-account cases go to dunning. There the metric that matters is how fast the customer adds a new card, not the retry count.
Agreed on keeping the decline path as a measurement too. We'll log every decline the feed should have prevented as a coverage miss, so we can see whether the feed keeps up over time.
Your last point is the one we'll take furthest. Right now our retry logic only sees the acquirer's code, and that code looks the same for "this card is gone" and "your token is stale." Deciding "can this request ever succeed?" from updater state before we retry is what we're building next.
That split turns a vague retry metric into two separate decisions. I especially like treating updater coverage misses as first-class: it lets you ask whether the feed's promise is holding independently of processor declines. Once updater state decides whether a request can ever succeed, keeping the decision and reason code beside the attempt should help distinguish stale state, a known-closed account, and transient timing. That feels like the bridge from a retry count to an inspectable recovery policy.
That split gives you a much cleaner control loop. I would keep three outcomes separate in the event record: updater state said the request could never succeed; a retry was attempted and declined; or updater data was missing or stale. Coverage misses can then measure feed quality without hiding cases where the feed was correct but the payment path still failed. The pre-attempt refresh should also make its freshness observable, so you can distinguish a stale decision from a stale source.