Repository navigation
Revenue Recovery: is retry timing conditioned on failure reason, or on transaction attributes? #14866
Unanswered
kshitij0305
asked this question in
Q&A
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
I built a payment-recovery system as a side project — it classifies why a payment failed and picks a retry plan from that, rather than retrying blind. Working through Revenue Recovery in the docs, I hit a design question I think you've already answered, and I'd like to know which way.
The thing I conditioned on. The retry plan comes from the failure class, and the spread is wide — 20 minutes for
transient_provideron the same rail, 18 hours forinsufficient_funds, 3 minutes forinstrument_rejectedbut with the failed method hidden from the checkout, 2 minutes forauthentication_abandoned, 24 hours and a single attempt forcustomer_cancelled, and 30 minutes forunknown.The two ends are the point. An empty account and a dead card are both "declined", and they want opposite treatment — eighteen hours versus three minutes, and one of them needs the rail changed rather than the clock run down. A single retry curve serves one of those and burns the other.
Revenue Recovery tunes retries by card bin, region and method. Those are attributes of the transaction rather than of the failure, so I'm curious whether failure class is in there too, or deliberately isn't.
And I think I found the reason it might not be. Conditioning on the failure reason only works if the processor actually tells you the reason, and for cards I couldn't get one.
Testing against Razorpay, five different documented error-scenario cards — declined, insufficient funds, timed out, authentication failed, and a Mastercard decline — each failed at a real checkout and returned identical payloads. Same
error_reason, sameerror_source, same description. I tried it through payment links and again through the direct Checkout integration, which is the flow their published error table actually describes. Same result both ways.The only field that separated a bank decline from a temporary issue was
error_description, and only for netbanking and wallet failures. Never for cards. Of seven real captured failures, two were diagnosable and five classified asunknown.So if card failures are generically reported across processors, tuning on bin and region is the signal you can get, and failure class is a nice idea that doesn't survive contact with real webhooks. Is that roughly why it went that way, or do you see better diagnosability across the 120+ connectors than I saw on one?
One number in case it's useful to anyone reading. I put a Thompson sampling bandit over the candidate retry plans and measured when learning actually pays. Across twelve runs at each volume, median lift was +5.2% at 500 failures and +11.5% at 10,000 — but below roughly 10,000 the range crosses zero, so a low-volume merchant can end up worse off than with fixed defaults. Exploration isn't free. I ended up shipping learning as an explicit switch rather than a default.
Code and the full write-up, including the parts that didn't work: https://github.com/kshitij0305/second-chance
Happy to be told I've misread the docs.
All reactions