Skip to content
API DocsDocs

Error Recovery Patterns

Decision trees for handling failed, stuck, and ambiguous transactions

4 min readUpdated May 18, 2026

Payment transactions can fail, get stuck, or return ambiguous results. This guide provides decision trees for every scenario so you know exactly what to do.

#Decision Tree: Payment Failed

When a payment returns FAILED status (synchronously or via webhook):

Payment Failed Recovery

#Decision Tree: Transaction Stuck in PENDING

Transaction Stuck in PENDING

Use the same recovery pattern for transactions stuck in CUSTOMER_VERIFICATION: confirm the customer was redirected to the returned challenge or redirect URL, wait for the webhook, poll as fallback, and do not create a duplicate transaction for the same order.

Stuck StatusAfter 5 minAfter 30 minAfter 2 hrs
PENDINGPoll the status endpointKeep order pending; do not fulfillContact support with transactionId
CUSTOMER_VERIFICATIONConfirm customer reached the challenge or redirect URLAsk customer to retry the verification flowContact support if no final webhook/status

#Decision Tree: 3DS Challenge Flow

3DS Challenge Flow

Critical: The successRedirectUrl is for user experience only. Always treat the webhook callback as the authoritative payment result. A customer can be redirected to the success URL even if the payment ultimately fails (rare edge case with async processing).

#Webhook vs Redirect Disagree

Always trust the webhook. The successRedirectUrl is for user experience only — never use it as payment confirmation.

SignalAction
Webhook = SUCCEEDFulfill order
Webhook = FAILEDDo not fulfill — notify customer
No webhook receivedPoll GET /payments/card/info/{id} (returns status and declineCode)
Webhook and poll disagreeContact support with transactionId

#Retry Strategy

#When to Retry

Decline Code RangeRetry?Max AttemptsBackoff
1, 9, 12, 13 (transient)Yes3Exponential: 2s, 4s, 8s
61, 63, 70, 72, 73 (gateway)Yes3Exponential: 5s, 15s, 30s
3, 4, 5, 6, 10, 19-23 (customer)NoPrompt customer to retry manually
8, 14, 17, 24-31, 65 (merchant)NoFix payload first
7, 28, 29, 62, 64, 66-69 (config)NoContact support

#Retry with Idempotency

Always use the same requestId when retrying a failed payment. This ensures:

  • You won't be charged twice if the first attempt actually succeeded
  • Exirom returns the existing transaction result instead of creating a new one
async function payWithRetry(paymentData, token, maxRetries = 3) {
  // Use a stable requestId for all retries
  const requestId = paymentData.requestId;
 
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    const res = await fetch('https://sandbox.api.exirom.com/api/api/v1/payments/card', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify(paymentData),
    });
 
    const result = await res.json();
 
    // Stop API retry. SUCCEED is final; CUSTOMER_VERIFICATION needs customer action.
    if (['SUCCEED', 'CUSTOMER_VERIFICATION'].includes(result.transactionStatus)) {
      return result;
    }
 
    if (result.transactionStatus === 'FAILED') {
      const retryable = [1, 9, 12, 13, 61, 63, 70, 72, 73];
      if (!retryable.includes(result.declineCode) || attempt === maxRetries) {
        return result; // Non-retryable or exhausted retries
      }
    }
 
    // Exponential backoff
    await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 1000));
  }
}

#Common Mistakes

MistakeConsequenceFix
Trusting redirect URLs as payment confirmationOrders fulfilled for failed paymentsAlways wait for webhook
Not deduplicating webhooksOrders processed twice, double fulfillmentDeduplicate on transactionId + transactionStatus
Using a new requestId on retryCustomer charged multiple timesReuse the same requestId
Retrying customer-action errors (3DS, funds)Wastes API calls, same resultPrompt customer instead
Processing webhook before responding 200Timeouts cause duplicate deliveriesRespond 200 first, process async
Not polling after extended PENDINGStuck orders, angry customersPoll after 5 min, escalate after 2 hrs
Was this helpful?