How Routes Work
A route decides which gateway profile ODUS attempts for a card payment, and where it fails over to after a decline. Unlike a cascade, a route can read the payment: the card's brand, BIN, issuer and funding type, the amount and currency, who initiated it, and your own metadata. So you can send cards from one issuer to one gateway and everything else to another.
Routes apply to card payments only, and are managed through the Core API v2. To create one and point payments at it, see Routing card payments with a route.
Versions
A route is a named container. Its decision tree lives in a version, and a version never changes once published. To change a route, publish a new version and activate it. To roll back, activate an earlier one.
Only the active version routes payments. Each payment records the version it was routed by, so activating another version later does not change how an earlier payment is explained.
The decision tree
A tree is built from four kinds of node:
| Node | What it does |
|---|---|
condition | Tests the payment and follows the first branch that matches, or else. |
split | Sends a fixed share of payments down each of its outputs, such as 20% and 80%. |
pool | Tries its children in turn, and fails over to the next child after a decline. |
gateway | Attempts the payment at one gateway profile. |
Nodes nest freely: a branch or a split's output can lead to a pool, and a pool can hold conditions.
{
"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" }
}
Cards issued by Capital One go to the first gateway, and fail over to the second after a decline. Every other card goes to the second gateway directly.
Conditions
A condition is a single comparison, { "field", "op", "value" }, or all, any and not over other conditions.
| Field | Operators | Value |
|---|---|---|
card.brand | eq ne in nin exists | A card brand, such as visa |
card.bin | startsWith exists | A prefix of 1–8 digits, or a list of them |
card.issuerName | eq ne in nin contains exists | The issuing bank's name |
card.issuerCountry | eq ne in nin exists | An ISO 3166-1 alpha-2 code, such as US |
card.fundingType | eq ne in nin exists | credit, debit, prepaid or charge |
amount | eq ne gt gte lt lte | A number in minor units |
currency | eq ne in nin exists | A currency code, such as usd |
initiator | eq ne in nin exists | customer or merchant |
statementDescriptor | eq ne in nin contains exists | The payment's statement descriptor |
statementDescriptorSuffix | eq ne in nin contains exists | The payment's statement descriptor suffix |
metadata.<key> | eq ne in nin contains exists | The value of <key> in the payment's metadata |
in and nin take a list, and exists takes no value. Metadata compares exactly; every other field ignores case.
statementDescriptor and statementDescriptorSuffix are the values the payment carries: the ones you sent when you created it, or else those of the first product in its cart. When the payment has no value for the field, a condition on it does not match, just as an unknown card field does not.
Branches
A condition tests its branches in order and follows the first one that matches. When none matches, it follows else. Without else, nothing is attempted from that node.
Once a branch matches, the condition never tries another of its branches or its else — not when the gateway in that branch declines, and not when it is inactive. What happens next depends on the node around the condition: inside a pool, the pool moves on to its next child; at the top of the tree, the route ends. To fail over within a branch, put a pool inside it, as the example above does.
Splits
A split sends each payment down one of its outputs, in proportion to each output's percent. Use it to compare two setups on live traffic, or to move payments to a new gateway a share at a time:
{
"type": "split",
"outputs": [
{
"percent": 20,
"then": { "type": "gateway", "gatewayProfileId": "gwp_2mXGTxXIkqv4uSdbm8DJCzvHUIu" }
},
{
"percent": 80,
"then": {
"type": "pool",
"strategy": "ordered",
"children": [
{ "type": "gateway", "gatewayProfileId": "gwp_3RTHQUROb9Bt2dJwhLmTGEykKEe" },
{ "type": "gateway", "gatewayProfileId": "gwp_2mXGTxXIkqv4uSdbm8DJCzvHUIu" }
]
}
}
]
}
About 20% of payments go to the first gateway alone. The other 80% go to the second gateway, and fail over to the first after a decline.
A split has 2 to 20 outputs. Each percent is a whole number from 1 to 99, and the percentages of one split add up to 100.
The output is chosen from the payment, not drawn again each time:
- Authorizing the same payment again sends it down the same output, as long as the split and its percentages have not changed.
- Each payment is chosen on its own. A customer's next payment, such as a subscription renewal, may follow a different output.
- The percentages hold across many payments. Over a handful of payments, the shares can differ noticeably from them.
- Two splits in one tree choose independently.
A split never sends a payment down a second output — not when a gateway in the chosen output declines, and not when it is inactive. As with a matched branch, what happens next depends on the node around the split. To fail over within an output, put a pool inside it, as the example above does. To fail over across every gateway, use a weighted pool instead.
Pools
A pool tries its children in an order set by its strategy:
| Strategy | Order |
|---|---|
ordered | The order the children are written in. |
roundRobin | Written order, starting at a position that moves on from one payment to the next. |
weighted | A random order, in proportion to weights: one weight from 1 to 100 per child. |
A weighted pool picks its whole order at once, so failover after a decline also follows the weights. To send a fixed share of payments down separate paths, with no failover between them, use a split.
maxAttempts limits how many gateway attempts a pool makes, including those made by nodes inside it. When it is reached, the pool's remaining children are not tried.
Skipped is not declined
A node that was never attempted is skipped. A gateway that was attempted and refused the payment declined. A pool moves on after either, but they are not the same:
| Skipped | Declined | |
|---|---|---|
| Examples | An inactive gateway profile, a condition with no matching branch | The gateway refused the payment |
| Creates a transaction | No | Yes |
Counts toward maxAttempts and the attempt limit | No | Yes |
| If nothing else is attempted, the route ends as | not_routed | declined |
A gateway profile that is inactive when the payment is authorized is skipped, even though it was active when the version was published. If every gateway a payment can reach is skipped, nothing is attempted. The payment stays open and its latestMessage says so.
When card data is unknown
ODUS reads the issuer name, issuer country and funding type from the card's BIN. They are not available for every card, and for some saved cards they are filled in later. A field ODUS does not have is unknown, and a comparison against an unknown field is neither true nor false.
A branch whose condition is unknown is not taken, so the payment moves on to the next branch or else. This also holds for negative operators: ne, nin and not do not match an unknown field either. A card with no known issuer country is not matched by { "field": "card.issuerCountry", "op": "ne", "value": "US" }, and nothing reports an error.
To decide what happens to those cards yourself, test for them with exists, which is always true or false:
{ "not": { "field": "card.issuerCountry", "op": "exists" } }
all is false as soon as one of its terms is false, and any is true as soon as one is true, whatever the others are. Only when the result depends on an unknown term is the whole condition unknown.
ODUS may also hold fewer digits of a card's BIN than your prefix has. When the digits it holds agree with your prefix, startsWith is unknown, because the card may or may not be in that range.
A metadata key the payment does not have is unknown too.
Rules every route follows
These hold across the whole tree, and no node can turn them off:
- One attempt per gateway profile. A payment is attempted at each gateway profile at most once. A second node naming the same profile is skipped.
- At most 11 attempts. One authorization makes at most 11 gateway attempts in total.
- No failover after an unknown outcome. When a gateway gives no answer, it may have charged the card, so the route stops there.
- A pending attempt ends the route. When an attempt is left waiting on the customer or the gateway, such as for 3D Secure, the route ends at that gateway and does not move on.
- No failover without time for it. When too little time is left in the request for another attempt, the route stops instead of starting one. The first attempt is always made.
Automatic retries under a retry policy do not go through the route: each retry uses the gateway its retry step names.
Limits
| Limit | Value |
|---|---|
| Nodes in one version | 100 |
Branches in one condition | 20 |
Children in one pool | 20 |
Outputs in one split | 2–20 |
percent on a split's output | 1–99 |
Terms in one branch's condition, all, any and not included | 50 |
Values in one in, nin or startsWith list | 1,000 |
maxAttempts on a pool | 1–11 |