# 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](https://docs.odus.com/docs/guides/routing/how-routes-work.md).

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](https://docs.odus.com/docs/guides/dashboard/gateway-profiles.md).

## Create the route

Send a [`POST /v2/routes`](https://docs.odus.com/docs/api/core/v2/route-controller-create) 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`](https://docs.odus.com/docs/api/core/v2/route-controller-publish-version). 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](https://docs.odus.com/docs/guides/routing/how-routes-work.md#limits), 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`](https://docs.odus.com/docs/api/core/v2/route-controller-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`](https://docs.odus.com/docs/api/core/v2/route-evaluation-controller-evaluate). 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](#read-the-path-a-payment-took), 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`](https://docs.odus.com/docs/api/core/v2/route-controller-save-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](https://docs.odus.com/docs/guides/routing/how-routes-work.md#limits). Read it back with [`GET /v2/routes/{id}/draft`](https://docs.odus.com/docs/api/core/v2/route-controller-find-draft), which answers `404` when the route has no draft. Discard it with [`POST /v2/routes/{id}/draft/delete`](https://docs.odus.com/docs/api/core/v2/route-controller-delete-draft).

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`](https://docs.odus.com/docs/api/core/v2/route-controller-publish-draft). 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](https://docs.odus.com/docs/api/core/v2/payment-controller-create) or [update](https://docs.odus.com/docs/api/core/v2/payment-controller-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`](https://docs.odus.com/docs/api/core/v2/merchant-config-controller-set), or choose the route on the dashboard's [Merchant configuration](https://docs.odus.com/docs/guides/dashboard/merchant-configuration.md#default-route) 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}`](https://docs.odus.com/docs/api/core/v2/route-controller-update) 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`](https://docs.odus.com/docs/api/core/v2/payment-controller-find-all-route-executions-paginated). 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}`](https://docs.odus.com/docs/api/core/v2/route-controller-find-version).

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

| `result`       | Meaning                                                                                                                                                        |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `matched`      | The condition followed `branch`: its index, or `else`. For a split, `branch` is the index of the output the payment took.                                      |
| `no_match`     | `branch` did not match.                                                                                                                                        |
| `unknown_fact` | `branch` could not be decided. `unknownFields` lists the fields the payment lacked, and `unsupportedTerms` the parts of the condition ODUS could not evaluate. |
| `attempted`    | The payment was sent to `gatewayProfile`. `outcome` is the gateway's answer.                                                                                   |
| `skipped`      | The node was passed over, for the `reason` given, such as `gateway_inactive`.                                                                                  |
| `withheld`     | The 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](https://docs.odus.com/docs/guides/routing/how-routes-work.md#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.
