Skip to main content
View as Markdown
Version: v2

Routing Card Payments with a Route

This guide creates a route, points card payments at it, and shows how to read the path a payment took. For how a route decides, see How Routes Work.

Routes are managed through the Core API v2 only.

Prerequisites​

  • Every gateway profile your route will name is active and supports card payments. See Gateway Profiles.

Create the route​

Send a POST /v2/routes request with a name:

{
"name": "Card routing",
"description": "Sends Capital One cards to a dedicated gateway"
}

The response is the route. Keep its id for the next steps. A new route has no versions, so activeVersion is null and it cannot route payments yet:

{
"id": "rte_2mXGTxXIkqv4uSdbm8DJCzvHUIu",
"name": "Card routing",
"description": "Sends Capital One cards to a dedicated gateway",
"active": true,
"activeVersion": null,
"createdAt": "2026-10-14T09:00:00Z",
"updatedAt": null
}

Publish and activate a version​

Send the decision tree to POST /v2/routes/{id}/versions. Set activate to true to make it the active version straight away:

{
"activate": true,
"tree": {
"type": "condition",
"branches": [
{
"when": { "field": "card.issuerName", "op": "contains", "value": "Capital One" },
"then": {
"type": "pool",
"strategy": "ordered",
"children": [
{ "type": "gateway", "gatewayProfileId": "gwp_2mXGTxXIkqv4uSdbm8DJCzvHUIu" },
{ "type": "gateway", "gatewayProfileId": "gwp_3RTHQUROb9Bt2dJwhLmTGEykKEe" }
]
}
}
],
"else": { "type": "gateway", "gatewayProfileId": "gwp_3RTHQUROb9Bt2dJwhLmTGEykKEe" }
}
}

The response is the new version. It also carries tree, exactly the tree you sent, which is left out here:

{
"id": "rtv_2mXGTxXIkqv4uSdbm8DJCzvHUIu",
"route": "rte_2mXGTxXIkqv4uSdbm8DJCzvHUIu",
"number": 1,
"createdAt": "2026-10-14T09:01:00Z",
"updatedAt": null
}

A tree that names an unknown or inactive gateway profile, or breaks a limit, is refused with 400.

Without activate, the route keeps its current active version. To activate a version later, or to roll back to an earlier one, send its ID to POST /v2/routes/{id}/activate:

{
"version": "rtv_2mXGTxXIkqv4uSdbm8DJCzvHUIu"
}

The response is the route, with that version as its activeVersion.

Check where a payment would go​

Before you activate a version, you can see where it would send a payment. Send the payment's facts to POST /v2/routes/{id}/evaluation. Nothing is charged or stored.

{
"version": "rtv_2mXGTxXIkqv4uSdbm8DJCzvHUIu",
"facts": {
"amount": 1000,
"currency": "usd",
"initiator": "customer",
"card": { "bin": "41472000", "issuerName": "Capital One", "issuerCountry": "US" }
}
}

Send version to check a published version, tree to check a tree you have not published yet, or neither to check the active version. Leave out any card field you do not know. A condition on a missing field cannot be decided, so it does not match, as it would for a real payment.

attempts lists the gateway profiles the payment would be sent to, in order. Each one is tried only if every one before it declined. steps has the same shape as the steps of a route execution, without the transaction and timestamps:

{
"route": "rte_2mXGTxXIkqv4uSdbm8DJCzvHUIu",
"routeVersion": "rtv_2mXGTxXIkqv4uSdbm8DJCzvHUIu",
"attempts": [
{ "attempt": 1, "gatewayProfile": "gwp_2mXGTxXIkqv4uSdbm8DJCzvHUIu", "route": "rte_2mXGTxXIkqv4uSdbm8DJCzvHUIu", "path": "$.branches[0].then.children[0]" },
{ "attempt": 2, "gatewayProfile": "gwp_3RTHQUROb9Bt2dJwhLmTGEykKEe", "route": "rte_2mXGTxXIkqv4uSdbm8DJCzvHUIu", "path": "$.branches[0].then.children[1]" }
],
"steps": [
{ "path": "$", "result": "matched", "branch": "0" },
{ "path": "$.branches[0].then.children[0]", "result": "attempted", "gatewayProfile": "gwp_2mXGTxXIkqv4uSdbm8DJCzvHUIu", "attempt": 1 },
{ "path": "$.branches[0].then.children[1]", "result": "attempted", "gatewayProfile": "gwp_3RTHQUROb9Bt2dJwhLmTGEykKEe", "attempt": 2 }
]
}

The example is shortened: every step carries every field, null where it does not apply. Gateway profiles are taken as they are now, so an inactive one is skipped. A roundRobin pool starts where your next payment through that version would start it. A weighted pool draws a new random order on every request, as each payment does.

Save an unfinished edit as a draft​

To keep an edit you have not finished, save it as the route's draft with POST /v2/routes/{id}/draft. The tree can be incomplete, such as a branch with no then yet or a gateway with no gatewayProfileId:

{
"tree": {
"type": "condition",
"branches": [{ "when": { "field": "card.issuerCountry", "op": "eq", "value": "US" } }],
"else": { "type": "gateway" }
}
}

The response is the draft, with the tree exactly as you sent it:

{
"id": "rtd_2mXGTxXIkqv4uSdbm8DJCzvHUIu",
"route": "rte_2mXGTxXIkqv4uSdbm8DJCzvHUIu",
"tree": { "type": "condition", "branches": [{ "when": { "field": "card.issuerCountry", "op": "eq", "value": "US" } }], "else": { "type": "gateway" } },
"createdAt": "2026-10-14T09:05:00Z",
"updatedAt": null
}

A route has at most one draft. Saving again replaces its whole tree. A draft must still be shaped like a tree you could publish: a node type, key or field that a route does not have is refused with 400, and so is a draft over the limits. Read it back with GET /v2/routes/{id}/draft, which answers 404 when the route has no draft. Discard it with POST /v2/routes/{id}/draft/delete.

A draft is not a version. No payment is routed by it, and it cannot be activated. When it is finished, publish it with POST /v2/routes/{id}/draft/publish. The body takes only activate, which works as it does for publishing a version:

{
"activate": true
}

The draft's tree goes through the same checks as a tree sent to POST /v2/routes/{id}/versions. If it passes, the response is the new version and the draft is deleted. If it fails, the response is 400, nothing is published, and the draft stays as it was.

Point card payments at the route​

Set the card's mode to route and name the route. You can do this for one payment, or for every payment by default.

For one payment, send it in paymentMethodOptions when you create or update the payment:

{
"amount": 1000,
"currency": "usd",
"paymentMethodOptions": {
"card": {
"mode": "route",
"route": "rte_2mXGTxXIkqv4uSdbm8DJCzvHUIu"
}
}
}

For every payment, set it in defaultPaymentMethodOptions with POST /v2/merchant-config, or choose the route on the dashboard's Merchant configuration page. A payment that does not set its own card options uses it:

{
"defaultPaymentMethodOptions": {
"card": {
"mode": "route",
"route": "rte_2mXGTxXIkqv4uSdbm8DJCzvHUIu"
}
}
}

In the response, the card options name the route. Shortened:

{
"paymentMethodOptions": {
"card": {
"mode": "route",
"route": {
"id": "rte_2mXGTxXIkqv4uSdbm8DJCzvHUIu",
"name": "Card routing",
"description": "Sends Capital One cards to a dedicated gateway",
"active": true
}
}
}
}

The merchant configuration names it the same way, under defaultPaymentMethodOptions. Both requests are refused with 400 unless the route is active and has an active version.

Deactivating a route stops its payments

You can deactivate a route with "active": false on POST /v2/routes/{id} while payments or your default still point at it. Nothing is refused then, and new payments that take the route from your default are still created. At authorization, no gateway is attempted: the payment stays open, and its latestMessage reads "The payment's route is inactive or has no active version, so no gateway was attempted."

Read the path a payment took​

Send GET /v2/payments/{id}/route-executions. Each authorization through a route adds one execution. When the route is inactive or has no active version at authorization, no execution is added: the payment's latestMessage explains it instead. It records the version that ran, the facts its conditions read, and a steps entry for every node the route reached, in order:

{
"data": [
{
"id": "rtx_2mXGTxXIkqv4uSdbm8DJCzvHUIu",
"payment": "pay_2mXGTxXIkqv4uSdbm8DJCzvHUIu",
"routeVersion": {
"id": "rtv_2mXGTxXIkqv4uSdbm8DJCzvHUIu",
"route": "rte_2mXGTxXIkqv4uSdbm8DJCzvHUIu",
"number": 1
},
"status": "completed",
"outcome": "authorized",
"facts": {
"amount": 1000,
"currency": "usd",
"initiator": "customer",
"card": {
"brand": "visa",
"bin": "41472000",
"issuerName": "Capital One",
"issuerCountry": "US",
"fundingType": "credit"
},
"metadata": null
},
"steps": [
{
"position": 0,
"path": "$",
"result": "matched",
"branch": "0"
},
{
"position": 1,
"path": "$.branches[0].then.children[0]",
"result": "attempted",
"attempt": 1,
"gatewayProfile": "gwp_2mXGTxXIkqv4uSdbm8DJCzvHUIu",
"transaction": "tx_2mXGTxXIkqv4uSdbm8DJCzvHUIu",
"outcome": "declined"
},
{
"position": 2,
"path": "$.branches[0].then.children[1]",
"result": "attempted",
"attempt": 2,
"gatewayProfile": "gwp_3RTHQUROb9Bt2dJwhLmTGEykKEe",
"transaction": "tx_3RTHQUROb9Bt2dJwhLmTGEykKEe",
"outcome": "authorized"
}
]
}
],
"pagination": {
"mode": "cursor",
"pageSize": 10,
"hasNextPage": false,
"hasPreviousPage": false,
"nextCursor": null,
"prevCursor": null
}
}

The example is shortened. Every object also carries its timestamps, and every step carries every field, null where it does not apply. path locates the node in the version's tree, which you can read with GET /v2/routes/{id}/versions/{version}.

Each step's result says what happened at that node:

resultMeaning
matchedThe condition followed branch: its index, or else. For a split, branch is the index of the output the payment took.
no_matchbranch did not match.
unknown_factbranch could not be decided. unknownFields lists the fields the payment lacked, and unsupportedTerms the parts of the condition ODUS could not evaluate.
attemptedThe payment was sent to gatewayProfile. outcome is the gateway's answer.
skippedThe node was passed over, for the reason given, such as gateway_inactive.
withheldThe route stopped before trying gatewayProfile, for the reason given.

New reason values may be added. Branch on result, and treat a reason you do not recognise as an explanation only.

Why did a rule not fire?​

Find the condition's steps and check the branch's result:

  • no_match: the payment's facts did not meet the condition. Compare them with facts.
  • unknown_fact: the condition needed a field ODUS did not have for this card, listed in unknownFields, or held a term ODUS could not evaluate, listed in unsupportedTerms. See When card data is unknown for how to route those cards on purpose.

When the route attempted nothing, the execution's outcome is not_routed. Look for the skipped steps that explain why.