Routing Rules
Transaction Routing Rules
This guide explains how to write transaction routing rules that control which acquirer processes a given transaction. Rules are defined as JSON objects and evaluated at transaction time using the inputs described below.
Overview
A transaction routing rule definition is a JSON object that defines conditions (expressions) and outcomes (actions). When a transaction arrives, the rules engine evaluates your rule against the transaction data and produces a routing output — an object mapping acquirer identifiers to numeric weightings. The platform then uses these weightings, combined with your routing strategy, to select an acquirer.
For your convenience, we have provided a UI for constructing rule definitions. This can be found embedded in our documentation on the 'Rule Builder' page.
Data structure
Creating a new transaction routing rule will typically require three API calls: POST v1/rules for the rule's general details, POST v1/rule-versions for the rule's definition, and POST v1/transaction-routing-rules for transaction-routing specific information (in future the rules engine will be applied to other domains than just transaction routing, so this information is separate). Please refer to the API documentation for the request formats.
To change an existing rule's definition, you should create a new rule version, then update the current_version field on the rule (PATCH v1/rules/:id). Rule versions cannot be deleted, in order to maintain observability of historical routing decisions.
The rest of this page will focus on writing rule definitions.
Routing output
Your rule must return an object where keys are acquirer identifiers and values are numeric weights. Currently supported acquirers:
Key | Acquirer |
|---|---|
shift4 | Shift4 (Visa, Mastercard, etc.) |
shift4_amex | Amex via Shift4 |
Please see the OpenAPI documentation for the latest list of valid acquirer identifiers.
Each value is a numeric weight (0 or undefined = do not route, >0 = eligible; larger numbers indicate greater preference). The system will use these weights according to the rules routing strategy (sorted or weighted_random).
Routing strategies
The routing strategy is configured for the rule outisde of the rule definition itself and determines how the weightings are interpreted:
- sorted — the acquirer with the highest weight is selected. We will fall back to lower-weighted acquirers if the preferred acquirer is unavailable, but acquirers with a weight of 0 will never be routed to.
- weighted_random — an acquirer is selected randomly, with probability proportional to its weight.
Rule structure
A rule is a JSON object with the following fields:
{
"ruleName": "MyRule",
"expression": "...",
"localParams": [...],
"operator": "And" | "Or",
"rules": [...],
"actions": {
"onSuccess": { ... },
"onFailure": { ... }
}
}Fields
Field | Type | Required | Description |
|---|---|---|---|
ruleName | string | Yes | A unique name for the rule. Used in error messages. |
expression | string | No | A boolean expression to evaluate. Required for leaf rules (rules without nested rules). |
localParams | array | No | Scoped parameters — intermediate values computed before the expression is evaluated. |
operator | "And" or "Or" | No | How to combine nested rules. Required when rules is present. |
rules | array | No | Nested child rules. Each child is a rule object with the same structure. |
actions | object | No | What to return when the rule succeeds or fails. Although optional, if an action is not returned all acquirers will be weighted as 0 and the transaction will be unprocessable. |
Expressions
Expressions are boolean-valued strings evaluated against the available inputs. The expression language is based on expr-eval with some extensions.
Operators
Category | Operators | Example |
|---|---|---|
Comparison | ==, !=, >, >=, <, <= | binLookupResult.cardBrand == "Visa" |
Logical | and, or, not | x > 0 and y > 0 |
Arithmetic | +, -, *, /, %, ^ | messageBody.Transaction.TransactionDetails.TotalAmount * 0.15 |
Ternary | ? : | isAmex ? 1 : 0 |
Grouping | (...) | (a or b) and c |
Property access | . | binLookupResult.cardBrand |
Important: String comparisons are case-sensitive. "Visa" is not the same as "visa".
Literals
- Strings: 'hello'
- Numbers: 42, 3.14, 0.5
- Booleans: true, false
- Arrays: [1, 2, 3], ['12-25', '12-26']
Arrays
Array comparisons are frequently useful - for example, to have a routing rule based on a cardholder's authentication method you would need to access messageBody.Environment.Cardholder.Authentication, which is an array field.
The lodash JavaScript package is available to the rules engine via a variable named _ - this contains all of the regular JavaScript array handling methods as well as lodash's extensions. It is important to note that methods requiring callbacks (e.g. _.filter(collection, predicate)) must be done with the iteratorCallback utility documented below.
Built-in functions
The expression language includes these built-in functions:
Function | Description |
|---|---|
min(a, b, ...) | Smallest value |
max(a, b, ...) | Largest value |
abs(x) | Absolute value |
round(x) | Round to nearest integer |
roundTo(x, n) | Round x to n decimal places |
floor(x) | Round down |
ceil(x) | Round up |
length(x) | Length of a string or array |
indexOf(x, a) | First index of x in string or array a, or -1 |
if(c, a, b) | Returns a if c is true, else b (note: both branches are always evaluated — prefer the ternary operator c ? a : b) |
Available inputs
When your rule is evaluated, the following named inputs are available for use in expressions:
binLookupResult
Data from the BIN lookup performed on the card number. Available fields depend on BIN data coverage, and any field may be absent. Common fields include:
Field | Type | Example values |
|---|---|---|
cardBrand | string | "Visa", "Mastercard", "Amex" |
cardCountryCode | string | "GB", "US", "FR" |
cardProductType | string | "consumer", "business" |
messageHeader, messageBody, messageTrailer
The raw nexo authorisation request fields. Refer to the nexo messaging specification for the full schema. These give you access to transaction-level details such as amount, currency, and merchant category code.
poiContext
The point-of-interaction context, including merchant configuration and terminal details. Useful for merchant-level overrides.
Local parameters
Local parameters let you define intermediate values to keep your expressions clean. Each parameter has a name and an expression. Parameters are evaluated in order and can reference earlier parameters.
{
"localParams": [
{
"name": "CardBrandIsAmex",
"expression": "binLookupResult.cardBrand == 'Amex'"
},
{
"name": "CardBrandIsVisaOrMastercard",
"expression": "binLookupResult.cardBrand == 'Visa' or binLookupResult.cardBrand == 'Mastercard'"
},
{
"name": "Shift4Weight",
"expression": "CardBrandIsVisaOrMastercard ? 1 : 0"
},
{
"name": "Shift4AmexWeight",
"expression": "CardBrandIsAmex ? 1 : 0"
}
]
}Actions
Actions define what the rule returns when it succeeds or fails. Two action types are available:
ReturnObject
Returns a literal object. The context field is the object that will be returned. A transaction routing rule must use this action to return an appropriate object.
{
"actions": {
"onSuccess": {
"name": "ReturnObject",
"context": {
"shift4": 1,
"shift4_amex": 0
}
}
}
}ReturnObjectFromParamKeys
Returns an object where the values are resolved from local parameters or input names. This is useful when the weights are computed dynamically.
In the context, each key is an acquirer identifier and each value is the name of a local parameter (or input) whose computed value should be used.
{
"localParams": [
{
"name": "CardBrandIsAmex",
"expression": "binLookupResult.cardBrand == 'Amex'",
},
{
"name": "CardBrandIsVisaOrMastercard",
"expression": "binLookupResult.cardBrand == 'Visa' or binLookupResult.cardBrand == 'Mastercard'",
},
{
"name": "Shift4Weight",
"expression": "CardBrandIsVisaOrMastercard ? 1 : 0",
},
{
"name": "Shift4AmexWeight",
"expression": "CardBrandIsAmex ? 1 : 0",
}
],
"actions": {
"onSuccess": {
"name": "ReturnObjectFromParamKeys",
"context": {
"shift4": "Shift4Weight",
"shift4_amex": "Shift4AmexWeight"
}
}
}
}onFailure
You can also define an onFailure action, which runs when the rule's expression evaluates to false. If no onFailure action is defined and the rule fails, no output is produced for that rule (equivalent to all weights being 0).
{
"actions": {
"onSuccess": {
"name": "ReturnObject",
"context": { "shift4_amex": 1 }
},
"onFailure": {
"name": "ReturnObject",
"context": { "shift4": 1 }
}
}
}Nested rules
Rules can contain child rules combined with And or Or operators. This lets you build complex conditions without writing a single long expression.
With And, all child rules must pass for the parent to succeed. With Or, at least one must pass.
{
"ruleName": "GbAndConsumer",
"operator": "And",
"rules": [
{
"ruleName": "CardCountryIsGB",
"expression": "binLookupResult.cardCountryCode == \"GB\""
},
{
"ruleName": "CardSegmentIsConsumer",
"expression": "binLookupResult.cardProductType == \"consumer\""
}
],
"actions": {
"onSuccess": {
"name": "ReturnObject",
"context": { "shift4_amex": 1 }
},
"onFailure": {
"name": "ReturnObject",
"context": { "shift4": 1 }
}
}
}Utility functions
A set of utility functions is available under the Utils namespace for time-based and date-based routing.
All of the date functions optionally take a timezone string as their last input parameter. This must be an IANA time zone identifier (e.g. Asia/Kolkata) or a time zone offset (e.g. +08:30). The time zones will default to UTC, which is generally undesireable (you will typically want daylight savings time to be taken into account).
Utils.isInWeekdayIndexArray(dayIndexArray, timezone?)
Returns true if the current day of the week (in the given timezone) is in the provided array. Days are numbered 1 (Monday) through 7 (Sunday).
Utils.isInWeekdayIndexArray([6, 7]) // Saturday or Sunday, UTC
Utils.isInWeekdayIndexArray([6, 7], "Europe/London") // in London timezoneUtils.isInMonthDayArray(dateArray, timezone?)
Returns true if today's date is in the provided array (formatted as "MM-DD").
Utils.isInMonthDayArray(["12-25", "12-26"]) // Returns TRUE if today is Christmas Day or Boxing DayUtils.isOnExactDateArray(dateArray, timezone?)
Returns true if today's date including the year is in the provided array (formatted as "YYYY-MM-DD").
Utils.isOnExactDateArray(["2026-04-03", "2026-05-01"])Utils.isBetweenTwoTimes(start, end, timezone?)
Returns true if the current time is between start and end (inclusive). Times are in HH:mm:ss.sssssssss format (minor units are optional and default to 0).
Utils.isBetweenTwoTimes("09:00", "17:00", "Europe/London")Utils.nowZonedDateTimeISO(timezone?)
Returns the current date-time as an ISO string in the given timezone. Primarily useful in local parameters for advanced date logic.
Utils.substring(input, start, end?)
Returns a substring. Equivalent to JavaScript's String.prototype.substring.
Utils.iteratorCallback(expression)
A helper for calling functions that require a callback as input, such as many of the lodash methods. Takes as input an expression in string form, following the same syntax as any other expression in the rules builder.
NB: the evaluator for this doesn't have access to the full context, so e.g. the other utilities above cannot be used, and neither can the local parameters; if this is a problem you should evaluate an intermediate value in a parameter.
_.some(poiContext.toro_config.supported_card_entry_modes, Utils.iteratorCallback('element == "integrated_circuit_card"'))Examples
Example 1: Route all transactions to a single acquirer
The simplest possible rule. Always routes to Shift4.
{
"ruleName": "AlwaysShift4",
"expression": "true",
"actions": {
"onSuccess": {
"name": "ReturnObject",
"context": {
"shift4": 1
}
}
}
}Example 2: Route by card scheme
Route American Express cards to the Amex acquirer and Visa/Mastercard to Shift4, using local parameters and ReturnObjectFromParamKeys.
{
"ruleName": "SchemeBasedRouting",
"localParams": [
{
"name": "CardBrandIsAmex",
"expression": "binLookupResult.cardBrand == 'Amex'"
},
{
"name": "CardBrandIsVisaOrMastercard",
"expression": "binLookupResult.cardBrand == 'Visa' or binLookupResult.cardBrand == 'Mastercard'"
},
{
"name": "Shift4Weight",
"expression": "CardBrandIsVisaOrMastercard ? 1 : 0"
},
{
"name": "Shift4AmexWeight",
"expression": "CardBrandIsAmex ? 1 : 0"
}
],
"expression": "CardBrandIsAmex or CardBrandIsVisaOrMastercard",
"actions": {
"onSuccess": {
"name": "ReturnObjectFromParamKeys",
"context": {
"shift4": "Shift4Weight",
"shift4_amex": "Shift4AmexWeight"
}
}
}
}Example 3: Routing based on card country and segment
Route UK consumer cards to one acquirer and everything else to another, using nested rules.
{
"ruleName": "GbConsumerRouting",
"operator": "And",
"rules": [
{
"ruleName": "CardCountryIsGB",
"expression": "binLookupResult.cardCountryCode == 'GB'"
},
{
"ruleName": "CardSegmentIsConsumer",
"expression": "binLookupResult.cardProductType == 'consumer'"
}
],
"actions": {
"onSuccess": {
"name": "ReturnObject",
"context": { "shift4_amex": 1 }
},
"onFailure": {
"name": "ReturnObject",
"context": { "shift4": 1 }
}
}
}Example 4: Time-based routing
Route transactions on weekends or holidays to a different acquirer. Uses Or nesting and utility functions.
{
"ruleName": "WeekendOrHolidayRouting",
"localParams": [
{
"name": "holidays",
"expression": "['12-25', '12-26', '01-01']"
}
],
"operator": "Or",
"rules": [
{
"ruleName": "IsHoliday",
"expression": "Utils.isInMonthDayArray(holidays)"
},
{
"ruleName": "IsWeekend",
"expression": "Utils.isInWeekdayIndexArray([6, 7])"
}
],
"actions": {
"onSuccess": {
"name": "ReturnObject",
"context": { "shift4_amex": 1 }
},
"onFailure": {
"name": "ReturnObject",
"context": { "shift4": 1 }
}
}
}Example 5: Even split for weighted random routing
When using the weighted_random strategy, give two acquirers equal probability.
{
"ruleName": "EvenSplit",
"expression": "true",
"actions": {
"onSuccess": {
"name": "ReturnObject",
"context": {
"shift4": 1,
"shift4_amex": 1
}
}
}
}To weight 70/30 instead, you would use "shift4": 0.7, "shift4_amex": 0.3.
How weighting works
The acquirer weights in the output will be used to determine the appropriate routing according to the strategy you have set for the rule.
- If the strategy is sorted: the acquirers will be sorted in order of preference, and the highest-weighted acquirer will always be used unless it is unavailable, falling back to the next-highest weighted, and so on.
- If the strategy is weighted_random: an acquirer will be randomly chosen, with the probability determined by the weightings. This is useful if you want to divide transactions between multiple acquirers according to a ratio.
Common pitfalls
- String comparisons are case-sensitive. "Visa" and "visa" are different values. binLookupResult.cardBrand is in Titlecase.
- Every acquirer key you omit defaults to 0. If your rule only returns { "shift4_amex": 1 }, all other acquirers get a weight of 0.
- An onFailure action is optional. If omitted and the rule fails, no output is produced — which means all acquirers default to 0 for that rule.
- The expression field is required for leaf rules (rules without nested rules). Parent rules that use operator and rules do not need an expression.
- Local parameter names must not collide with input names (messageHeader, messageBody, messageTrailer, poiContext, binLookupResult).