Handle failed or cancelled flows
Recover safely from failed, expired, cancelled, delayed, and interrupted hosted 0Gate flows.
Every hosted payment flow needs a recovery model. Customers refresh pages, close iframes, abandon redirects, fail compliance or payment steps, and contact support before webhook delivery catches up. Build the recovery path before launch.
Failure pages must stay public-safe
Do not expose provider names, compliance rule details, treasury state, raw webhook payloads, or internal retry diagnostics on customer-facing screens.
Failure map
| Situation | Customer-facing state | Backend rule |
|---|---|---|
| Customer closes iframe | Offer resume, retry, or support. | Do not mark failed until trusted state says terminal. |
| Return URL opens | Show processing or terminal status from your server. | Ignore status passed only through the URL. |
| Session expires | Ask the user to start again. | Do not reuse expired session values. |
| Hosted flow fails | Show a generic failed state and next action. | Store the event id and close the attempt once. |
| Duplicate webhook arrives | No visible change. | Return success after dedupe. |
| Webhook delayed | Keep processing, then reconcile if needed. | Reconciliation must not double-fulfill. |
Terminal states
| Terminal state | Meaning | Retry guidance |
|---|---|---|
| fulfilled | Verified backend state completed and your ledger updated. | Do not retry fulfillment. Show receipt/status. |
| failed | The hosted flow could not complete. | Let the customer start a new attempt if product rules allow it. |
| expired | The session lifetime ended before completion. | Create a new session; do not reuse the old browser secret. |
| cancelled | User or backend cancelled the attempt. | Preserve audit state and start fresh if needed. |
Recovery controller
The status endpoint your browser reads should be backed by your database, not by browser callback memory.
async function getPaymentRecoveryState(attemptId: string) {
const attempt = await paymentAttempts.get(attemptId);
if (attempt.status === 'fulfilled') return { view: 'complete' };
if (attempt.status === 'failed') return { view: 'failed', canRetry: true };
if (attempt.status === 'expired') return { view: 'expired', canRetry: true };
if (attempt.status === 'cancelled') return { view: 'cancelled', canRetry: true };
if (attempt.updatedAt < minutesAgo(15)) {
await reconciliationQueue.enqueue({ attemptId, reason: 'stale_processing_view' });
}
return { view: 'processing' };
}Retry policy
| Edge | What can happen | Correct behavior |
|---|---|---|
| User double-clicks start | Two browser requests race. | Use one server-side attempt or idempotency key per logical action. |
| Session creation times out | Your server does not know whether the write succeeded. | Retry with the same logical idempotency key. |
| Widget mount fails | Browser cannot open hosted flow. | Show retry, but do not create unlimited attempts. |
| Webhook delivery retries | Same event is delivered more than once. | Dedupe and return success for already-seen events. |
| Support triggers reconciliation | A human needs current trusted state. | Read server-side state and compare before changing the ledger. |
Support view
Give support enough to help without exposing internals.
| Show | Hide |
|---|---|
| Your attempt id and customer/account reference. | Secret keys, webhook secrets, browser session secrets. |
| 0Gate session id and current normalized status. | Raw webhook bodies and signatures. |
| Timeline of received event ids and terminal state changes. | Provider, venue, treasury, compliance-rule, or risk-threshold details. |
| Last reconciliation check and next allowed action. | Internal settlement notes not approved for public/support display. |
Customer copy
| Status | Copy pattern |
|---|---|
| Processing | “We are confirming your payment. You can leave this page and check status later.” |
| Failed | “This payment could not be completed. No final credit has been applied.” |
| Expired | “This session expired. Start again to get a fresh checkout.” |
| Cancelled | “This checkout was cancelled. You can start a new attempt.” |
| Support needed | “Contact support with this attempt id.” |
Related pages
Preview quotes before checkout
Show indicative pricing before the hosted 0Gate flow while keeping final price and fulfillment tied to trusted backend state.
Embed the Gate widget
Mount the hosted 0Gate iframe with the public browser SDK, keep session creation server-side, and use callbacks only for browser UX.