Prompt
How do I set up a payment routing API for failover when an acquirer times out or declines?
Latest observation
To set up a payment routing API with failover, you want a system that can:
- Send the payment to a primary acquirer
- Detect timeout/soft decline conditions
- Retry or reroute to a backup acquirer
- Avoid duplicate charges
- Keep a full audit trail
Here’s a practical blueprint.
1) Define your routing rules
Decide which failure cases should trigger failover:
Typical failover triggers
- Timeouts: no response within your SLA
- Soft declines: issuer/acquirer response suggests retry may succeed
- Network errors: 5xx, connection reset, DNS failure
- Technical rejects: acquirer unavailable, processing error
Usually do not fail over for:
- Hard declines like:
- insufficient funds
- stolen card
- expired card
- invalid CVV
- do-not-honor depending on your policy
- These are cardholder/issuer decisions and usually won’t improve by switching acquirers.
A good rule is to maintain a decline classification map:
retryablenon_retryableunknown
2) Use an idempotency key everywhere
This is critical. If the first acquirer actually processed the payment but your API timed out, a retry could double-charge the customer.
Requirements
- Client sends
idempotency_key - Your routing API stores:
- request payload hash
- status
- chosen acquirer
- acquirer reference IDs
- final payment outcome
Behavior
If the same idempotency key is received again:
- return the original result
- do not re-authorize unless explicitly safe and intended
3) Create a routing decision engine
Your API should separate:
- routing logic
- acquirer adapters
- payment state tracking
Example flow
- API receives payment request
- Routing engine selects primary acquirer
- Send request to acquirer A
- If response is:
- success → return success
- soft decline / timeout / technical failure → route to acquirer B
- hard decline → return decline immediately
Routing logic inputs
- card BIN / issuer country
- payment amount / currency
- merchant account
- acquirer uptime / health
- cost / authorization rate
- MOTO / ecommerce / 3DS requirements
- previous transaction history
4) Build acquirer adapters
Each acquirer should have a normalized interface.
Example internal interface
{
"authorize": {
"amount": 1000,
"currency": "USD",
"card_token": "tok_123",
"merchant_id": "m_001",
"idempotency_key": "abc-123"
}
}
Each adapter translates this into the acquirer’s format and maps the response back to your standard model.
Normalize response fields
status: approved / declined / error / pendingreason_codereason_categoryacquirer_referencenetwork_referenceretryable: true/false
5) Implement failover carefully
Recommended retry policy
- Timeouts: retry once on backup acquirer
- Technical errors: retry once on backup
- Soft declines: reroute to backup acquirer
- Hard declines: no failover
Important
Do not blindly retry the same acquirer multiple times for card payments unless you have a reason. This can:
- increase latency
- increase duplicate risk
- hurt approval rates
Example decision tree
Primary acquirer attempt
├─ Approved → done
├─ Hard decline → stop
├─ Soft decline → try backup
└─ Timeout/error → try backup
6) Track payment state as a state machine
A payment should move through explicit states.
Example states
createdrouting_selectedsent_to_acquirer_atimeout_acquirer_asent_to_acquirer_bapproveddeclinedmanual_reviewerror
This makes it easier to:
- debug issues
- reconcile transactions
- avoid double capture
- expose status to clients
7) Prevent duplicate authorization/capture
This is one of the biggest risks.
Best practices
- Use idempotency keys
- Store acquirer authorization IDs
- Before failover, determine whether the first attempt may still be “in flight”
- If an acquirer supports it, use:
- authorization reversal/void if a duplicate happens
- transaction inquiry to verify final status
For async or delayed responses
Sometimes a timeout does not mean failure. The acquirer may later approve the transaction.
You need:
- a callback/webhook or polling reconciliation job
- a rule for late responses:
- if backup already approved, you may need to void one authorization
- if not, reconcile with the first acquirer’s final status
8) Add circuit breakers and health checks
If an acquirer is unhealthy, stop sending traffic to it for a while.
Circuit breaker logic
- Monitor error rate, timeout rate, latency
- If thresholds exceeded:
- open circuit
- route new traffic to backups
- After cooldown:
- half-open with small test volume
- close if healthy
Also monitor
- auth approval rate
- timeout rate by card BIN/country
- soft decline rate
- webhook delivery failures
9) Make routing configurable
Avoid hardcoding routing rules.
Configuration examples
- route by BIN range
- route by currency
- route by merchant
- route by amount threshold
- route by acquirer health score
- percentage-based canary routing
Sample rule set
rules:
- if: currency == "USD" and card_country == "US"
prefer: acquirer_a
fallback: acquirer_b
- if: acquirer_a.timeout_rate > 5%
disable: acquirer_a
- if: amount > 50000
require: acquirer_c
10) Expose a clean API
Example endpoint
POST /payments/authorize
Request
{
"amount": 1000,
"currency": "USD",
"card_token": "tok_123",
"merchant_id": "m_001",
"idempotency_key": "pay-789",
"metadata": {
"order_id": "o-555"
}
}
Response
{
"payment_id": "pmt_123",
"status": "approved",
"acquirer": "acquirer_b",
"acquirer_reference": "auth_9988",
"fallback_used": true
}
If declined:
{
"payment_id": "pmt_123",
"status": "declined",
"decline_category": "hard_decline",
"reason_code": "05",
"acquirer": "acquirer_a"
}
11) Reconciliation and reporting
You need a background process to:
- compare internal payment records with acquirer settlements
- catch late approvals
- identify duplicates
- reconcile reversals/voids
- produce audit logs
This is essential if failover is used.
12) Security and compliance
- Never store raw PAN unless PCI compliant
- Use tokenization
- Encrypt sensitive data in transit and at rest
- Restrict acquirer credentials per environment
- Log safely: avoid card data, CVV, secrets
- Use signed webhooks and request authentication
13) Suggested architecture
Components
- API gateway
- Routing engine
- Payment state store
- Acquirer adapter services
- Health monitor
- Reconciliation worker
- Webhook listener
- Audit log store
Flow
Client → Payment API → Routing Engine → Acquirer A
↘ on failover → Acquirer B
↘ state store + audit logs
14) Implementation tips
- Make timeouts short but realistic
- Use asynchronous job processing for reconciliation
- Record exact timestamps for each attempt
- Store acquirer-specific request/response payloads securely for debugging
- Make failover decisions deterministic and testable
- Test with sandbox acquirers and injected failures
15) Simple pseudocode
def authorize_payment(request):
if already_processed(request.idempotency_key):
return previous_result(request.idempotency_key)
route = choose_primary_acquirer(request)
result = send_to_acquirer(route.primary, request)
if result.approved:
store_result(request.idempotency_key, result)
return result
if result.hard_decline:
store_result(request.idempotency_key, result)
return result
if result.timeout or result.soft_decline or result.technical_error:
backup = choose_backup_acquirer(request, exclude=[route.primary])
backup_result = send_to_acquirer(backup, request)
final = resolve_results(result, backup_result)
store_result(request.idempotency_key, final)
return final
16) Key policy decisions you should make up front
- Which decline codes are retryable?
- How long to wait before considering a timeout?
- How many failover attempts are allowed?
- Should failover be synchronous or asynchronous?
- What happens if both acquirers approve?
- How do you reverse one of two approvals?
- Which routes are preferred by region/BIN/currency?
- What is your reconciliation SLA?
If you want, I can also provide:
- a reference API design
- a database schema for payment routing
- a state machine diagram
- or sample code in Node.js, Python, or Java.