A resilient checkout admits what it cannot know
A pending screen without a reconciliation job merely hides abandonment. The unresolved payment can outlive the session, while support and fulfilment act on different truths.
8 min read
A resilient checkout does not turn silence into failure. If a risk service times out, a browser returns an ambiguous authentication error, or a payment provider never returns a response, the system has learned less than the interface wants to say. A network error can leave a payment operation indeterminate, and WebAuthn deliberately uses NotAllowedError for several causes, including user cancellation and client-specific failures.712 “Declined” would be fiction. So would “Paid.”
The honest product behaviour is to preserve that uncertainty, restrict the next action and explain what will happen next. That requires more than softer error copy. The payment state model must distinguish a negative result from a missing result; retries must preserve the identity of the original operation; recovery must seek new evidence; and the checkout must not invite a second payment while the first may exist.
Unknown is not failed
Many checkout models compress every dependency into loading, success or error. That is convenient for rendering and dangerous for money. A request can be accepted without being completed: HTTP 202 Accepted is explicitly noncommittal, and the request may or may not ultimately be acted upon.1 Payment status vocabularies are similarly careful. ISO 20022 distinguishes PDNG (further checks and a status update will follow), ACSP (accepted for execution with settlement in process) and ACSC (settlement completed).2
Those labels should not be copied indiscriminately across every payment method. Their value is structural: acceptance, progress and completion are different facts.
A useful checkout therefore records two things. The first is business progress: risk assessed, authentication requested, authorization submitted, provider acknowledged, payment completed. The second is what the system knows about that progress: authoritative, provisional, missing or contradictory. RECONCILING is not a polite synonym for failure. It says that an operation crossed a boundary and its outcome has not yet been established.
That distinction changes behaviour. A definitive issuer decline may permit the customer to choose another instrument. An unknown authorization outcome should freeze creation of a new charge for the same purchase, retain the payment reference and start reconciliation. The screen can look calm in both cases. The permitted next action cannot be the same.
Authentication has its own uncertainty
Authentication is not a single synchronous gate. In EMV 3-D Secure, an issuer’s Access Control Server can request a challenge after evaluating the authentication request, and it supplies the authentication outcome later in a Results Request.3 EMVCo also defines decoupled authentication for cases in which the normal challenge is unavailable or fails; that authentication can occur separately from checkout, with the result arriving afterwards.4
The product model needs room for AUTHENTICATION_REQUIRED, AUTHENTICATION_IN_PROGRESS, AUTHENTICATED, NOT_AUTHENTICATED and AUTHENTICATION_UNAVAILABLE. Collapsing the last two destroys information. One is an adverse conclusion. The other says the conclusion could not be reached.
Browser APIs add another limit. WebAuthn describes NotAllowedError as a catch-all spanning multiple possible causes, some controlled by the client.12 The checkout should not claim “You cancelled” merely because that exception appeared. “Authentication didn’t complete” is less specific because the evidence is less specific.
Even successful confirmation is not payment success. Secure Payment Confirmation can produce signed evidence that the user confirmed payment details; the relying party verifies that evidence as part of a later authorization decision.5 The correct sequence is therefore “confirmed, awaiting payment result,” not an early success page painted over an unfinished authorization.
A missing risk answer is not a low-risk answer
Risk failure creates a different temptation: use a default score and keep conversion moving. But “no signal” and “low risk” are not interchangeable.
The European Union’s regulatory technical standards provide a concrete example. The transaction-risk-analysis exemption from strong customer authentication depends on a real-time risk analysis that has not identified specified anomalies, including unusual behaviour, device information, malware signals and known fraud scenarios.6 If the analysis did not run, its low-risk conclusion does not exist. The exact fallback depends on the payment method, jurisdiction and risk owner; the general product rule does not. Absence must remain visible to policy.
A degraded-risk policy can stop the payment, require stronger authentication, or allow only a narrowly pre-approved path. What it cannot safely do is let a timeout masquerade as an approval. The decision record should retain which signals were available, which dependency failed and which fallback rule authorized the next step. That makes degraded behaviour reviewable instead of accidental.
The customer does not need the internal topology. “We need another verification step” is useful. “Risk engine timeout” is not. Honesty means accurately stating the consequence, not exporting infrastructure jargon.
Retry the intent, not the button click
The most dangerous uncertainty begins after a payment request leaves the checkout. If the connection drops, the provider may have received and processed it even though the caller saw no response. Stripe advises treating some server errors as indeterminate because an operation may later be reconciled and surfaced by webhook.7
An idempotency key gives repeated transmission the same operation identity. Stripe’s public API contract stores the first result for a key, returns that result to subsequent matching requests and rejects reuse with different parameters.8 This makes a same-key retry a way to ask about the original mutation, not a request to create another one.
The checkout still needs its own durable payment identity. It should bind the purchase, amount, currency, instrument choice, provider attempt and idempotency key before sending the mutation. A timeout then resumes that record. It does not manufacture a new attempt because the customer refreshed the page or pressed Pay again.
Recovery should prefer evidence over repetition. Visa Direct’s account-and-wallet documentation exposes a query operation that can be used when its send API returned no response; the query uses the original payout or client reference to obtain the latest status.9 Where a provider offers equivalent retrieval, the checkout can reconcile by stable reference. Where it does not, the system should remain bounded by that weaker contract rather than guessing.
Rerouting is especially sensitive. Sending an unknown attempt to a second provider may turn one uncertainty into two valid charges. A new route becomes safe only when the earlier attempt is known not to have executed, has been authoritatively cancelled, or the orchestration layer has an enforceable deduplication contract across both routes.
Asynchronous truth is still truth
Callbacks are not a neat continuation of the browser session. Stripe documents that webhook deliveries can be duplicated and are not guaranteed to arrive in generation order; it recommends deduplicating events and retrieving objects when needed.10 A state machine that trusts arrival order can therefore move backwards or repeat a financial side effect.
The safer pattern is to treat each callback as evidence about a named payment. Verify and deduplicate it, compare it with the durable state, and apply only a valid transition. If an event exposes a gap or contradiction, retrieve the provider’s current object instead of inventing the missing history. The browser, webhook consumer and reconciliation worker should all update the same payment record.
This is also why “pending” needs a service-level meaning. It should identify who currently owes the next fact, how that fact will be obtained and when escalation begins. A pending state without an owner is just an indefinite spinner stored in a database.
Let the interface carry the truth
The W3C Payment Request API includes unknown alongside success and fail; for unknown, the user agent should not display either a success or failure indication.11 Checkout UI should preserve the same discipline after its own window closes.
When submission is indeterminate, the useful message is: “We’re confirming your payment. Don’t try again yet.” Keep the amount, instrument hint and purchase reference visible. Restore the same status after refresh. Poll or listen for an authoritative update. If confirmation will outlast the session, say where the final result will appear.
When authentication returns an ambiguous browser error, say it did not complete and offer a safe retry or another permitted method. When risk policy requires a step-up, explain the extra verification. Reserve “declined” for an authoritative negative response and “paid” for the product’s defined success boundary.
The interface should also reflect capability. While a payment is reconciling, the primary action may be disabled; support and self-service views should see the same state; fulfilment should wait for the business’s chosen authoritative event. These are not cosmetic details. They prevent the customer, an operator and an automated worker from each creating a different answer to the same uncertainty.
Resilience is often described as continuing through failure. In checkout, resilience sometimes means refusing to continue until the system knows enough. A trustworthy product does not promise omniscience. It makes uncertainty explicit, makes the next move safe and keeps working until an authoritative fact replaces the unknown.
Sources
HTTP Semantics (RFC 9110) (opens in a new tab) — IETF / RFC Editor.
Exceptions and Investigations — Maintenance 2020–2021 (opens in a new tab) — ISO 20022.
Challenge Flow: Technical Features (opens in a new tab) — EMVCo.
Decoupled Authentication (opens in a new tab) — EMVCo.
Secure Payment Confirmation (opens in a new tab) — World Wide Web Consortium (W3C).
Commission Delegated Regulation (EU) 2018/389 (opens in a new tab) — EUR-Lex / European Commission.
Advanced error handling (opens in a new tab) — Stripe Documentation.
Idempotent requests (opens in a new tab) — Stripe API Reference.
How to Use Visa Direct Account and Wallet (opens in a new tab) — Visa Developer.
Receive Stripe events in your webhook endpoint (opens in a new tab) — Stripe Documentation.
Payment Request API (opens in a new tab) — World Wide Web Consortium (W3C).
Web Authentication: An API for accessing Public Key Credentials — Level 3 (opens in a new tab) — World Wide Web Consortium (W3C).