3D Secure step-up: the ledger pause
When the issuer wants extra verification, the payment is in limbo. Your books need a name for that state.
By Solomon Ajayi · Free to read, no signup
A user enters their card details to fund their wallet. Your risk engine (or the issuer's) decides this transaction looks slightly off, first time on this device, unusual amount, foreign IP. 3D Secure 2 fires: the user gets pushed to their bank's app or an OTP screen to confirm. From your fintech's POV the authorization is now in a HELD state: not approved, not rejected, awaiting issuer verification. Could resolve in 5 seconds or 5 minutes. Could resolve as success or failure. Your books need an account for 'auth pending 3DS' that's separate from a clean auth, because a 3DS pause carries different fraud exposure and your sponsor bank's risk team WILL ask about it.
A normal card auth has two states your code already understands: pending and resolved. 3D Secure adds a third state that lives between them, where the issuer has neither approved nor declined but is challenging the cardholder to prove they are who they say they are. The money is not real yet, but the auth is no longer a simple clean hold either. It is held in limbo until the user finishes (or abandons) the verification step.
The temptation is to treat a 3DS-pending auth like any other pending auth and dump it in the same bucket. Resist that. A 3DS step-up carries different fraud exposure and different risk economics than a clean auth, and your sponsor bank's risk team will eventually demand to know how much volume is stuck in verification, how long it sits there, and how often it gets abandoned. You cannot answer those questions if clean and 3DS auths share one account.
A 3DS challenge resolves into exactly one of three outcomes, and each is a tiny journal entry. Success promotes the auth from the 3DS bucket to the clean bucket (a pure reclassification between two assets), then capture fires. Decline or abandonment releases the placeholder, reversing the original held entry, with nothing ever touching the user's wallet. None of these entries are hard; the discipline is making sure every held auth reaches one of them.
Worked example, step by step
Auth request triggers 3DS step-up: book to 3DS-pending bucket
User submits a ₦20,000 card deposit. Issuer responds: 'authorization conditional on 3DS verification.' You can't credit the user's wallet yet, verification might fail. Book the auth into a SEPARATE pending bucket so you can report your 3DS-blocked auth volume separately from clean auths.
| Account | Debit | Credit |
|---|---|---|
| Auth Pending (3DS step-up) (1115) | ₦20,000.00 | |
| Auth Counterparty (2110) | ₦20,000.00 |
Auth Pending (3DS step-up) UP ₦20,000 (asset, debit). Auth Counterparty UP ₦20,000 (liability, credit). Same balanced-placeholder shape as a normal auth, but in the 3DS bucket. Your daily report can now answer 'how many naira are sitting in 3DS verification right now.' Risk metric.
3DS succeeds: promote to clean auth, then capture
User completed 3DS in their banking app. Issuer confirms: auth approved. Two things happen atomically: the auth moves from the 3DS-pending bucket to the clean-auth bucket (so the user's clean-auth balance shows expected exposure), then capture fires immediately because the wallet credit was queued behind verification.
| Account | Debit | Credit |
|---|---|---|
| Auth Pending (clean) (1110) | ₦20,000.00 | |
| Auth Pending (3DS step-up) (1115) | ₦20,000.00 |
Auth Pending (3DS) DOWN ₦20,000 (credit, asset). Auth Pending (clean) UP ₦20,000 (debit, asset). Pure reclassification between two asset buckets, no money in or out. The downstream capture (Provider Receivable up, User Wallet up, the Auth Counterparty cleared) happens in a follow-up entry triggered by the same event flow.
3DS fails on a DIFFERENT auth: release without capture
Different user, ₦5,000 attempt. 3DS challenge sent, user closed the app, never completed. Issuer signals 'authorization not completed' after the timeout window (typically 10-15 minutes). You release the 3DS-pending bucket, no money was real, just a held placeholder. The bookkeeping reversal looks just like the original entry inverted.
| Account | Debit | Credit |
|---|---|---|
| Auth Counterparty (2110) | ₦5,000.00 | |
| Auth Pending (3DS step-up) (1115) | ₦5,000.00 |
Auth Pending (3DS) DOWN ₦5,000 (credit). Auth Counterparty DOWN ₦5,000 (debit). The placeholder dissolves. NOTHING happens to the user's wallet, they never had this money. NOTHING happens to your bank. This pattern repeats on every abandoned auth; the count of these per hour is a fraud-pressure signal.
Takeaway
3DS step-up creates a third auth state (alongside 'clean auth' and 'declined') that your books need to track separately. The bucket itself is just another Auth Pending account; the value is in the SEPARATION, you can report 3DS-blocked volume, time-to-resolution, and abandonment rate as risk metrics your sponsor bank will eventually require. The challenge resolves into one of three outcomes: clean (promote bucket → capture), declined (release auth), or abandoned (release auth after timeout). Each one is a tiny journal entry. None are difficult. The hard part is the LISTENER reliability, making sure no 3DS held auth gets orphaned past its timeout because your webhook handler missed an event.
Practice this on a real ledger
Reading is half of it. Open this lesson in the lab to post the entries yourself against a real Postgres-backed double-entry ledger, with the validation on. Free, your sandbox is yours.