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 a function_id and 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_id is 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.