Routing & providers
You call one API. Behind it, Finscale holds connections to multiple acquirers and PSPs, picks the one most likely to approve each transaction, and retries on the next when one fails. This page explains exactly how — and where you can steer it.
Providers are opaque
Every provider in your routing set is an opaque id — prov_eu_acq_01, prov_eu_acq_02, prov_us_psp_01, prov_br_psp_01. You never integrate a provider, sign one, or hold credentials for one. Finscale runs the contracts, the connections, and the compliance behind each id.
The ids are stable, so you can pin rules to them and read them in provider_attempts — but they deliberately carry no company name. That's what makes the abstraction hold:
- Swap without integration work. When a provider underperforms, Finscale adds, removes, or re-weights ids in your set. Your code never changes.
- One contract. You have a commercial relationship with Finscale — not with N acquirers in N jurisdictions.
- One data model. Every provider's response is normalized into the same Payment object, the same error codes, the same settlement lines.
See which providers are active on your account — and their live health — with GET /v1/providers or in the provider health dashboard.
The routing decision
When a payment enters processing, Finscale computes an ordered candidate list. Five stages — first three filter, last two rank:
-
Method support
Only providers that process the payment's method stay. For
ideal, that's your EU acquiring ids; a US-only PSP is out immediately. -
Currency & region
The provider must process the payment's
currencyand be licensed for the transaction's corridor.EURkeepsprov_eu_acq_01andprov_eu_acq_02in;prov_br_psp_01is out. -
Merchant rules
Your routing rules apply:
blockrules reject the transaction outright,prefer_providermoves one id to the front,weight_splitdistributes by weight. Rules always win over the automatic ranking below. -
Health & auth-rate
Candidates are ranked by live health (error rate, latency, open incidents) and rolling authorization rate for this method × currency segment. A degraded provider drops to the back — or out, if it's failing hard.
-
Cost
Among candidates that rank equally, the cheapest processing cost wins. Cost never outranks approval odds — a saved fee is worthless on a declined payment.
The first candidate gets the transaction. The rest become the failover chain. The whole decision runs in single-digit milliseconds inside the payment request.
Automatic failover
The canonical failover story, with the example order ord_9f21_0716 (€49.00): attempt 1 goes to prov_eu_acq_01, which times out. Finscale retries on prov_eu_acq_02, which approves. One API call, two attempts, zero merchant code.
Failover triggers on retriable outcomes only:
| Outcome | Examples | Failover |
|---|---|---|
timeout |
No provider response within the attempt budget (10 s). | Yes — next provider |
error |
Provider 5xx, provider rate limit, scheme downtime. | Yes — next provider |
declined with a retryable decline_code |
issuer_unavailable, try_again_later — the issuer never gave a verdict. |
Yes — next provider |
declined with a final decline_code |
card_declined by the issuer, insufficient_funds, fraud block, invalid or expired credentials. |
Never |
Final declines are the issuer's answer — asking a different acquirer the same question gets the same no, burns your auth-rate reputation, and violates scheme retry rules. Finscale fails the payment cleanly instead, and the error tells you why.
- Up to 3 attempts (primary + 2 failovers) inside one API call.
- Each failover attempt adds roughly one provider round-trip (p50 ≈ 400 ms).
- Idempotency is preserved across attempts: one
Idempotency-Key, one payment, one charge — no double-processing, whichever provider lands it.
"amount": 4999. Finscale forces a retryable decline on the first provider, fails over, and approves on the second — then shows both attempts in provider_attempts.
The provider_attempts array
Every payment carries its full routing history. Each entry records the provider, the outcome, and the attempt latency — the failover story above looks like this:
{
"id": "pay_8Q2mX4nT1cVb",
"object": "payment",
"amount": 4900,
"currency": "EUR",
"status": "succeeded",
"reference": "ord_9f21_0716",
"provider": "prov_eu_acq_02",
"provider_attempts": [
{ "provider": "prov_eu_acq_01", "outcome": "timeout", "latency_ms": 10004 },
{ "provider": "prov_eu_acq_02", "outcome": "approved", "latency_ms": 391 }
],
"created_at": "2026-07-16T09:24:31Z",
"livemode": false
}
outcome is one of pending, approved, declined, timeout, error — when it is declined, the normalized reason is in the attempt's decline_code. The top-level provider always equals the provider of the final attempt. Use the array for latency debugging, provider-mix reporting, and support ("why did this take 10 seconds?" — the first attempt timed out).
Routing rules
The automatic ranking is the right default. When you have a reason to override it — a better negotiated rate, a provider you're evaluating, a method you want isolated — attach routing rules to your account with POST /v1/routing_rules.
A rule has a match (which payments it applies to: method, currency, country, amount_gte) and an action. Rules are evaluated in ascending priority; the first match applies. Three actions cover the practical cases:
{
"id": "rr_2Nk8sQd1Vw4p",
"object": "routing_rule",
"priority": 10,
"match": { "method": "card", "currency": "EUR" },
"action": { "type": "prefer_provider", "provider": "prov_eu_acq_01" },
"active": true
}
prefer_provider moves the provider to the front of the candidate list. Everything else is unchanged — if it returns a retryable decline, errors, or times out, failover proceeds down the ranked chain.
{
"id": "rr_5Tp3wLc9Hm2f",
"object": "routing_rule",
"priority": 5,
"match": { "method": "card", "amount_gte": 500000 },
"action": { "type": "block" },
"active": true
}
block rejects matching transactions before any provider is attempted — the payment fails immediately with code blocked_by_routing_rule. Use it for corridors, methods, or ticket sizes you don't want to accept at all.
{
"id": "rr_8Zx4vRb2Kt6n",
"object": "routing_rule",
"priority": 20,
"match": { "method": "card", "currency": "EUR" },
"action": {
"type": "weight_split",
"weights": [
{ "provider": "prov_eu_acq_01", "weight": 80 },
{ "provider": "prov_eu_acq_02", "weight": 20 }
]
},
"active": true
}
weight_split distributes matching volume by weight — here 80% to prov_eu_acq_01, 20% to prov_eu_acq_02. The standard pattern for A/B-testing a provider's auth-rate on real traffic before committing volume. Failover still applies within each attempt.
prefer_provider rule overrides live health and auth-rate ranking. A preferred provider having a bad day drags your conversion with it. Prefer weight_split for experiments, review rules quarterly, and delete the ones you can't explain.
What you see vs. what Finscale handles
The contract of the abstraction, in one table:
| You see | Finscale handles |
|---|---|
| One REST API and one Payment object | N provider APIs, formats, and protocol quirks |
Opaque ids in provider and provider_attempts | Provider contracts, onboarding, credentials, compliance |
| One unified error model | Hundreds of provider-specific decline codes, mapped |
Refund by payment id — POST /v1/refunds | Which provider processed the original, and its refund rail |
| One settlement report and one payout per currency | Per-provider settlement files, timing differences, netting |
| Routing rules and a health dashboard | Live health scoring, auth-rate tracking, failover execution |
If you ever need the provider dimension — for finance, for support, for curiosity — it's on every payment and every settlement line. You just never have to build against it.