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.

POST /v1/payments your rules ascending priority — first match wins prefer_provider provider to front weight_split split by weight blocked_by_routing_rule payment fails — no provider attempted no match smart routing ranked by health · auth-rate · cost

Two properties of this model are worth internalizing:

  • Rules always win over the automatic ranking — a prefer_provider rule 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

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"
}
FieldTypeMeaning
idstringUnique identifier, always prefixed rr_.
priorityinteger ≥ 1Evaluation order — lowest number first. Must be unique among your rules.
activebooleanInactive rules are skipped during evaluation. Flip this to pause a rule without deleting it.
descriptionstring · nullHuman-readable label shown in the dashboard. Future-you will thank present-you.
matchobjectConditions a payment must meet for the rule to apply — see matching semantics.
actionobjectWhat happens on match: prefer_provider, weight_split, or block — see the three actions.
livemodebooleanTest-mode and live-mode rules are separate sets. Rules you create with sk_test_… never touch live traffic.
created_atstringISO 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.

ConditionTypeMatches
methodmethod idThe payment's method type — any id from the method catalog, e.g. ideal, card, pix.
currencyISO 4217The payment's presentment currency, e.g. EUR.
countryISO 3166-1The customer's two-letter country, e.g. NL.
amount_gteintegerAmounts greater than or equal to this, in minor units — 100000 matches €1,000.00 and up on EUR payments.
Minor units, like everywhere else

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:

ActionWhat it doesFailover
prefer_providerMoves 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_splitDistributes 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.
blockRejects 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 + country should outrank one matching only method, or the general rule swallows the specific one's traffic.
PriorityRuleWhy this position
5Block card where amount_gte: 500000Refusals evaluate before anything can route the payment.
10Prefer prov_eu_acq_02 for ideal in NLSpecific corridor commitment.
20Weight-split EUR card 80 / 20General 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:

POST /v1/routing_rules
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:

EndpointDoes
GET /v1/routing_rulesLists 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 block rule firing produces a payment.failed event with failure_code: blocked_by_routing_rule in the payment payload.
  • prefer_provider and weight_split are visible in each payment's provider and provider_attempts — not in any event stream of their own.

The full event catalog is on the webhooks page.

Error scenarios

ScenarioWhat the API returns
Weights don't sum to 100 in a weight_split400invalid_request_error with param: "action.weights". The rule is not created.
priority already used by another rule400invalid_request_error with param: "priority".
Unknown provider id in the action400invalid_request_error with param: "action.provider". Provider ids must come from GET /v1/providers.
Rule id doesn't exist on retrieve/update/delete404invalid_request_error, code: resource_missing.
A payment matches a block ruleNot an API error — the payment is created and immediately fails with failure_code: blocked_by_routing_rule, and payment.failed fires.
400 — weights must sum to 100
{
  "error": {
    "type": "invalid_request_error",
    "code": null,
    "message": "weights must sum to exactly 100 (got 90).",
    "param": "action.weights",
    "request_id": "req_7Hf3kQd2"
  }
}
  • 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.