Routing rules
Smart routing picks the best provider automatically — routing rules are how you override it when you have a reason to. Pin a corridor to one provider, split volume for an A/B test, or block traffic you never want to accept. This page covers the rule object, matching semantics, priorities, and the full lifecycle over /v1/routing_rules.
How rules are evaluated
When a payment enters routing, your active rules are evaluated in ascending priority — lowest number first. The first rule whose match fits the payment wins; its action applies and evaluation stops. If nothing matches, smart routing's automatic ranking takes over as if no rules existed.
Two properties of this model are worth internalizing:
- Rules always win over the automatic ranking — a
prefer_providerrule applies even when live health says another provider is currently better. That power is the point, and the risk. - Only the first match applies. Rules don't stack. A payment matching your priority-5 block rule never reaches your priority-10 prefer rule.
The routing rule object
{
"id": "rr_2Nc6wB4hJ9Ke",
"object": "routing_rule",
"priority": 10,
"active": true,
"description": "Prefer prov_eu_acq_02 for iDEAL in the Netherlands",
"match": { "method": "ideal", "country": "NL" },
"action": { "type": "prefer_provider", "provider": "prov_eu_acq_02" },
"livemode": false,
"created_at": "2026-07-16T09:24:31Z"
}
| Field | Type | Meaning |
|---|---|---|
id | string | Unique identifier, always prefixed rr_. |
priority | integer ≥ 1 | Evaluation order — lowest number first. Must be unique among your rules. |
active | boolean | Inactive rules are skipped during evaluation. Flip this to pause a rule without deleting it. |
description | string · null | Human-readable label shown in the dashboard. Future-you will thank present-you. |
match | object | Conditions a payment must meet for the rule to apply — see matching semantics. |
action | object | What happens on match: prefer_provider, weight_split, or block — see the three actions. |
livemode | boolean | Test-mode and live-mode rules are separate sets. Rules you create with sk_test_… never touch live traffic. |
created_at | string | ISO 8601 UTC creation time. |
Matching semantics
The match object has four optional conditions. Conditions you set must all hold (they AND together); conditions you omit (or set null) match everything. An empty match matches every payment.
| Condition | Type | Matches |
|---|---|---|
method | method id | The payment's method type — any id from the method catalog, e.g. ideal, card, pix. |
currency | ISO 4217 | The payment's presentment currency, e.g. EUR. |
country | ISO 3166-1 | The customer's two-letter country, e.g. NL. |
amount_gte | integer | Amounts greater than or equal to this, in minor units — 100000 matches €1,000.00 and up on EUR payments. |
amount_gte follows the same money convention as the rest of the API: integer minor units plus the payment's ISO currency. There is no amount_lte — express "small tickets" by giving the large-ticket rule a lower priority and letting everything else fall through.
The three actions
Every rule carries exactly one action, discriminated by type:
| Action | What it does | Failover |
|---|---|---|
prefer_provider | Moves the named provider to the front of the candidate list. Everything else about routing is unchanged. | Still applies — a retryable failure falls through to the ranked chain. |
weight_split | Distributes matching volume across two or more providers by weight. Weights are integers 1–99 and must sum to exactly 100. | Still applies within each attempt. |
block | Rejects matching payments before any provider is attempted. The payment fails immediately with failure_code: blocked_by_routing_rule. | Never — nothing is attempted. |
Worked examples of all three, with JSON payloads and when to reach for each, live on the smart routing page. The short version: prefer_provider for negotiated commitments, weight_split for evaluating a provider on real traffic, block for corridors or ticket sizes you refuse outright.
Priorities
Priorities are your rule set's ordering, and the first match short-circuits — so structure them deliberately:
- Blocks first. Give refusal rules the lowest numbers so nothing routes around them. A block at priority 5 protects you from a prefer at priority 10 sending the same payment to a provider anyway.
- Leave gaps. Number 5, 10, 20 — not 1, 2, 3. Priorities must be unique, and gaps let you slot a rule between two others without renumbering the set.
- Specific before general. A rule matching
method + countryshould outrank one matching onlymethod, or the general rule swallows the specific one's traffic.
| Priority | Rule | Why this position |
|---|---|---|
5 | Block card where amount_gte: 500000 | Refusals evaluate before anything can route the payment. |
10 | Prefer prov_eu_acq_02 for ideal in NL | Specific corridor commitment. |
20 | Weight-split EUR card 80 / 20 | General experiment — only sees traffic the rules above let through. |
Create, update, delete
Rules are account-level objects with a standard CRUD surface. Creating one takes effect on the next payment that enters routing — there is no deploy step:
curl https://api.finscale.dev/v1/routing_rules \
-H "Authorization: Bearer sk_test_51FinscaleDemo…" \
-H "Idempotency-Key: idem_rr_ideal_nl_01" \
-H "Content-Type: application/json" \
-d '{
"priority": 10,
"description": "Prefer prov_eu_acq_02 for iDEAL in the Netherlands",
"match": { "method": "ideal", "country": "NL" },
"action": { "type": "prefer_provider", "provider": "prov_eu_acq_02" }
}'
The response is the routing rule object above, with its assigned rr_ id. The rest of the surface:
| Endpoint | Does |
|---|---|
GET /v1/routing_rules | Lists your rules — cursor-paginated, like every list endpoint. |
GET /v1/routing_rules/{id} | Retrieves one rule. |
POST /v1/routing_rules/{id} | Updates priority, active, description, match, or action. Set "active": false to pause a rule. |
DELETE /v1/routing_rules/{id} | Deletes the rule. In-flight payments already routed are unaffected. |
Webhook events
Routing rules emit no events of their own — they are configuration, not money movement. You see their effects on payments:
- A
blockrule firing produces apayment.failedevent withfailure_code: blocked_by_routing_rulein the payment payload. prefer_providerandweight_splitare visible in each payment'sproviderandprovider_attempts— not in any event stream of their own.
The full event catalog is on the webhooks page.
Error scenarios
| Scenario | What the API returns |
|---|---|
Weights don't sum to 100 in a weight_split | 400 — invalid_request_error with param: "action.weights". The rule is not created. |
priority already used by another rule | 400 — invalid_request_error with param: "priority". |
| Unknown provider id in the action | 400 — invalid_request_error with param: "action.provider". Provider ids must come from GET /v1/providers. |
| Rule id doesn't exist on retrieve/update/delete | 404 — invalid_request_error, code: resource_missing. |
A payment matches a block rule | Not an API error — the payment is created and immediately fails with failure_code: blocked_by_routing_rule, and payment.failed fires. |
{
"error": {
"type": "invalid_request_error",
"code": null,
"message": "weights must sum to exactly 100 (got 90).",
"param": "action.weights",
"request_id": "req_7Hf3kQd2"
}
}
Related
- Smart routing — the automatic ranking your rules override, and worked examples of all three actions.
- Provider health & failover — why a preferred provider can still lose traffic to failover.
- Payment lifecycle — where routing sits in the payment state machine.
- Errors — the unified error model, including
blocked_by_routing_rule.