---
title: "Custom Routes"
description: "Bind a recipient address pattern to one destination. Exact, wildcard, and regex matching, evaluated in a deterministic order, with an explainable decision you can simulate before any mail arrives."
canonical: "https://docs.primitive.dev/docs/routing"
last-updated: "2026-05-17T00:00:00.000Z"
---

# 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.

## Availability

Custom routing is available to all organizations by default. It is governed by the `recipient_routing` organization entitlement; if the `/v1/routes` endpoints return `feature_disabled`, the entitlement has been disabled for your organization. Contact [support](https://www.primitive.dev/contact) or email the Primitive help agent at `dev@agent.primitive.dev` if you believe that is an error.

## 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. Regex matching is available on the Power and Platinum plans; on other plans, use `exact` or `wildcard`.

- Destination — exactly one endpoint. Provide an existing `endpoint_id`, or pass a `function_id`: Primitive binds the function's existing route-target endpoint, or mints one in the same transaction if the function has none (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 matching routes are ranked in a deterministic order and the **first match wins**:

- Scope — domain-scoped routes outrank org-wide routes. This is the primary key, so a domain-scoped route at `priority: 999` still beats an org-wide route at `priority: 1`.

- Priority ascending — within the same scope, lower priority is checked first.

- Tier specificity — `exact` > `wildcard` > `regex`.

- Wildcard literal length — a wildcard with more fixed (non-`*`) characters is more specific and wins.

- Creation time, then route id, as stable final tiebreaks.

Mail is delivered to that one endpoint, and 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 ordered by priority, then creation time (see "How a route is chosen" for 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 test 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. Regex matching requires the Power or Platinum 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.

Maintained by the [primitive.dev team](/about) · [Trust & security](/trust)Last updated: May 17, 2026
