Custom Routes
A route binds a recipient address pattern to one destination endpoint. Inbound mail resolves to a single destination at delivery time — no fan-out — and the decision is fully explainable: you can ask where any address would land, and why, before a single email arrives.
This is the product stance that an email address is a dispatch pattern, not a mailbox. A route maps where mail goes; your handler still owns what happens next.
All endpoints below live under https://api.primitive.dev/v1. Every command shown also exists as a primitive routes CLI verb.
Concepts
- Pattern — the recipient address a route matches on, in one of three tiers:
exact— one full address, e.g.billing@acme.dev.wildcard— a glob over the local part, e.g.*@acme.dev(the rest of a domain).regex— a safe, linear-time pattern (plan-gated; requires Power).
- Destination — exactly one endpoint. Provide an existing
endpoint_id, or pass afunction_idand Primitive mints a dedicated route-target endpoint for that function in the same transaction (per-address function routing). - Priority — evaluation order within a scope. Lower is checked first.
- Scope — a route is scoped to a domain, or org-wide when
domain_idis null. Domain-scoped routes are evaluated before org-wide ones. - Fallback — if no route matches, mail falls through to the domain's default endpoint, then the org default. If neither exists, the outcome is
none: the mail is stored, and nothing is delivered.
How a route is chosen
For a given normalized recipient, enabled in-scope routes are evaluated in a deterministic order — priority ascending, then tier specificity (exact > wildcard > regex), then creation time as a stable tiebreak. The first match wins and mail is delivered to that one endpoint. The full evaluation trace is recorded, so every delivery can answer "what went where, and why."
Create a route
Bind an exact address to a function. With function_id, the route-target endpoint is minted and bound for you:
curl -X POST https://api.primitive.dev/v1/routes \ -H "Authorization: Bearer $PRIMITIVE_AUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "match_type": "exact", "pattern": "billing@acme.dev", "function_id": "<function-uuid>" }'
Catch the rest of the domain with a wildcard pointed at an existing endpoint, at a lower priority so the exact rule always wins:
curl -X POST https://api.primitive.dev/v1/routes \ -H "Authorization: Bearer $PRIMITIVE_AUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "match_type": "wildcard", "pattern": "*@acme.dev", "endpoint_id": "<endpoint-uuid>", "priority": 200 }'
Provide exactly one of endpoint_id or function_id. priority defaults to 100 and enabled defaults to true.
Simulate before you ship
Ask where an address would land — and why, rule by rule — without sending anything:
curl -X POST https://api.primitive.dev/v1/routes/simulate \ -H "Authorization: Bearer $PRIMITIVE_AUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "recipient": "sales@acme.dev" }'
{ "outcome": "matched", "recipient": "sales@acme.dev", "endpoint_id": "a0d6f29c-4e13-4b87-9c52-1f8a3e7b6d09", "matched_route_id": "c4a9e1b7-3d28-4f60-b915-8e2c7a0d6f31", "matched_tier": "wildcard", "matched_pattern": "*@acme.dev", "default_scope": null, "evaluated": [ { "route_id": "b7e2d4a1-9c3f-4e08-8a17-2d6c5f0b9e44", "tier": "exact", "pattern": "billing@acme.dev", "result": "miss", "reason": "recipient 'sales@acme.dev' is not 'billing@acme.dev'" }, { "route_id": "c4a9e1b7-3d28-4f60-b915-8e2c7a0d6f31", "tier": "wildcard", "pattern": "*@acme.dev", "result": "hit" } ], "truncated": false }
outcome is matched (a route hit), defaulted (no route matched, a fallback endpoint applied — see default_scope), or none (nothing matched and no fallback exists). event_type defaults to email.received; pass it to model how a different event subscribes.
List, reorder, update, delete
# routes in evaluation order curl https://api.primitive.dev/v1/routes \ -H "Authorization: Bearer $PRIMITIVE_AUTH_TOKEN" # repriority in bulk curl -X POST https://api.primitive.dev/v1/routes/reorder \ -H "Authorization: Bearer $PRIMITIVE_AUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "updates": [{ "id": "<route-uuid>", "priority": 50 }] }' # change or disable a route curl -X PATCH https://api.primitive.dev/v1/routes/<route-uuid> \ -H "Authorization: Bearer $PRIMITIVE_AUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "enabled": false }' # remove a route curl -X DELETE https://api.primitive.dev/v1/routes/<route-uuid> \ -H "Authorization: Bearer $PRIMITIVE_AUTH_TOKEN"
From the CLI
Every operation above is a primitive routes verb, generated from the same API spec:
primitive routes create-route --match-type exact \ --pattern billing@acme.dev --function-id <function-uuid> primitive routes list-routes primitive routes simulate-route --recipient sales@acme.dev
The CLI prints the response data as formatted JSON. Add --envelope to see the full { success, data, meta } wrapper.
Limits
Routes are plan-limited by count, and regex routes require the Power plan. Disabled or deleted routes are skipped at delivery; a route pointing at a deactivated endpoint falls through to the next match rather than dropping mail.