Error Recovery Patterns
Decision trees for handling failed, stuck, and ambiguous transactions
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):
#Decision Tree: 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 Status | After 5 min | After 30 min | After 2 hrs |
|---|---|---|---|
PENDING | Poll the status endpoint | Keep order pending; do not fulfill | Contact support with transactionId |
CUSTOMER_VERIFICATION | Confirm customer reached the challenge or redirect URL | Ask customer to retry the verification flow | Contact support if no final webhook/status |
#Decision Tree: 3DS Challenge Flow
Critical: The
successRedirectUrlis 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
successRedirectUrlis for user experience only — never use it as payment confirmation.
| Signal | Action |
|---|---|
| Webhook = SUCCEED | Fulfill order |
| Webhook = FAILED | Do not fulfill — notify customer |
| No webhook received | Poll GET /payments/card/info/{id} (returns status and declineCode) |
| Webhook and poll disagree | Contact support with transactionId |
#Retry Strategy
#When to Retry
| Decline Code Range | Retry? | Max Attempts | Backoff |
|---|---|---|---|
| 1, 9, 12, 13 (transient) | Yes | 3 | Exponential: 2s, 4s, 8s |
| 61, 63, 70, 72, 73 (gateway) | Yes | 3 | Exponential: 5s, 15s, 30s |
| 3, 4, 5, 6, 10, 19-23 (customer) | No | — | Prompt customer to retry manually |
| 8, 14, 17, 24-31, 65 (merchant) | No | — | Fix payload first |
| 7, 28, 29, 62, 64, 66-69 (config) | No | — | Contact 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
| Mistake | Consequence | Fix |
|---|---|---|
| Trusting redirect URLs as payment confirmation | Orders fulfilled for failed payments | Always wait for webhook |
| Not deduplicating webhooks | Orders processed twice, double fulfillment | Deduplicate on transactionId + transactionStatus |
Using a new requestId on retry | Customer charged multiple times | Reuse the same requestId |
| Retrying customer-action errors (3DS, funds) | Wastes API calls, same result | Prompt customer instead |
| Processing webhook before responding 200 | Timeouts cause duplicate deliveries | Respond 200 first, process async |
| Not polling after extended PENDING | Stuck orders, angry customers | Poll after 5 min, escalate after 2 hrs |
#Related Guides
- Webhook Best Practices — Deduplication, signatures, polling
- Decline Codes Reference — Full code list
- Idempotency — How
requestIdprevents duplicate charges - Callback Retry Mechanism — Retry schedule and behavior