Routing rules
The JSON rule language for smart links — fields, operators, actions, evaluation order, split tests and validation.
Updated 2026-09-02
Rules are an ordered array stored on the smart link. The first enabled rule whose conditions match decides the destination; if none match, the default resolution applies.
Rule shape#
{
"id": "tr-local",
"name": "Türkiye → local storefront",
"enabled": true,
"all": [ { "field": "country", "op": "in", "value": ["TR"] } ],
"any": [ { "field": "os", "op": "eq", "value": "ios" }, { "field": "os", "op": "eq", "value": "android" } ],
"action": { "type": "deeplink", "path": "/tr/collections/summer", "fallbackUrl": "https://example.com/tr" }
}all conditions are AND-ed, any conditions are OR-ed; a rule matches when every all condition holds and at least one any condition holds (when any is present). Max 100 rules, 20 conditions per list.
Fields#
| Field | Type | Notes |
|---|---|---|
country | ISO-3166 alpha-2 | From edge geo or the IP pool |
region, city | string | Region / city names |
continent | EU, NA, AS, … | |
isEU | boolean | EU-27 membership |
timezone | IANA tz | Visitor's timezone when known |
ipType | RESIDENTIAL, MOBILE, DATACENTER, VPN, PROXY, TOR, CDN, SEARCH_BOT, UNKNOWN | From the IP pool |
asn | string | Autonomous system number |
ipVersion | 4 or 6 | |
os | ios, android, windows, macos, linux, other | |
osVersion | number | Major.minor as a number, e.g. 17.5 |
device | mobile, tablet, desktop, tv, console | |
browser | string | safari, chrome, … |
isBot | boolean | Verified crawler |
isInAppBrowser | boolean | Instagram, TikTok, Facebook, Line, WeChat … |
language | BCP-47 primary tag | From Accept-Language |
referrerHost | hostname | From Referer |
hour | 0–23 | In the rule's timezone if set, else the visitor's |
weekday | 0–6 (Sunday = 0) | |
date | YYYY-MM-DD | |
param.<name> | string | Any query parameter, e.g. param.utm_source |
Operators#
in, notIn, eq, neq, gte, lte, between (value: [min, max]), matches (regex), startsWith, exists, notExists.
Actions#
type | Fields | Effect |
|---|---|---|
redirect | url, passParams?, appendClickId? | 302 to a URL |
store | iosUrl?, androidUrl?, fallbackUrl? | OS-aware store redirect (Play gets the Install Referrer) |
deeplink | path, scheme?, universalLinkBase?, iosUrl?, androidUrl?, fallbackUrl?, schemeTimeoutMs? | Universal link → custom scheme interstitial → store, and stores path for deferred deep linking |
split | variants: [{ weight, label?, action }] (2–10) | Deterministic A/B split by visitor bucket (stable per IP + UA + slug) |
block | status? (403 / 404 / 410), message? | Stop the request |
passthrough | — | Skip remaining rules and use the link defaults |
Example rule set#
[
{
"id": "tr-local",
"name": "Türkiye → local storefront",
"all": [
{
"field": "country",
"op": "in",
"value": [
"TR"
]
}
],
"action": {
"type": "deeplink",
"path": "/tr/collections/summer",
"fallbackUrl": "https://example.com/tr"
}
},
{
"id": "eu-consent",
"name": "EU visitors → consent landing",
"all": [
{
"field": "isEU",
"op": "eq",
"value": true
},
{
"field": "os",
"op": "notIn",
"value": [
"ios",
"android"
]
}
],
"action": {
"type": "redirect",
"url": "https://example.com/eu/landing"
}
},
{
"id": "istanbul-night",
"name": "Istanbul 00:00–06:00 → night campaign",
"all": [
{
"field": "city",
"op": "eq",
"value": "Istanbul"
},
{
"field": "hour",
"op": "between",
"value": [
0,
6
]
}
],
"action": {
"type": "redirect",
"url": "https://example.com/night"
}
},
{
"id": "old-ios",
"name": "iOS < 15 → web (app unsupported)",
"all": [
{
"field": "os",
"op": "eq",
"value": "ios"
},
{
"field": "osVersion",
"op": "lte",
"value": 14.9
}
],
"action": {
"type": "redirect",
"url": "https://example.com/app-web"
}
},
{
"id": "tiktok-ab",
"name": "TikTok traffic A/B",
"all": [
{
"field": "param.utm_source",
"op": "eq",
"value": "tiktok"
}
],
"action": {
"type": "split",
"variants": [
{
"weight": 50,
"label": "A",
"action": {
"type": "deeplink",
"path": "/product/a"
}
},
{
"weight": 50,
"label": "B",
"action": {
"type": "deeplink",
"path": "/product/b"
}
}
]
}
},
{
"id": "vpn-block",
"name": "Datacenter / Tor → block",
"all": [
{
"field": "ipType",
"op": "in",
"value": [
"DATACENTER",
"TOR"
]
}
],
"action": {
"type": "block",
"status": 404
}
}
]Evaluation details#
- Rules run at the edge in under a millisecond; geo and IP-type come from
request.cfor the pool lookup. - Deep-link actions on desktop fall back to
fallbackUrl, then the link'swebFallbackUrl. - A visible
url=parameter bypasses rules entirely (Google transparency). - The matched rule id and action type are stored on the click (
routeRuleId,routeAction) and are available as report dimensions.
Validation and simulation#
Rules are validated with the schema on save (dashboard, update_smart_link_routing) and can be checked without saving via validate_routing_rules. Simulate visitors with test_smart_link_route or POST /api/dashboard/route-test:
{ "slug": "summer", "country": "TR", "os": "ios", "params": "utm_source=tiktok", "in_app_browser": true }{ "decision": { "action": { "type": "deeplink", "path": "/tr/collections/summer", "fallbackUrl": "https://example.com/tr" }, "ruleId": "tr-local", "label": "Türkiye → local storefront", "location": "https://links.yourbrand.com/tr/collections/summer?wc_click_id=SIMULATED" } }ruleId is null and label is "default" when no rule matched. location is the URL the visitor would be sent to (for deep-link actions on iOS/Android the store or universal link; the interstitial is used for in-app browsers).