Endpoints, Filters & Deliveries
This page covers the three controls that decide where inbound mail is delivered and what reaches each destination:
- Endpoints — the HTTP URLs and Primitive Functions that receive your mail, plus the per-endpoint
rulesthat filter what they get. - Filters — org-wide or per-domain allow/block lists keyed on the sender address.
- Webhook deliveries — the delivery history you can list, inspect, and replay.
This is distinct from Custom Routes: a route binds a recipient address pattern to a single destination, while this page manages the destinations themselves and the rules that govern delivery to them. For the end-to-end inbound flow, see Receiving Mail.
All endpoints below live under https://api.primitive.dev/v1 and authenticate with a bearer API key.
Endpoints
An endpoint is a delivery destination for inbound mail. It comes in two kinds:
kind: "http"— a webhookurlyou host. It must be HTTPS and publicly reachable; private and loopback hosts are rejected.kind: "function"— a hosted Primitive Function, referenced byfunction_id. No URL is sent over the wire; Primitive dispatches to the function internally.
Create an endpoint
Create an HTTP endpoint:
curl -X POST https://api.primitive.dev/v1/endpoints \ -H "Authorization: Bearer $PRIMITIVE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "kind": "http", "url": "https://api.acme.dev/hooks/primitive", "enabled": true }'
Create a function-backed endpoint instead by passing function_id and omitting url:
curl -X POST https://api.primitive.dev/v1/endpoints \ -H "Authorization: Bearer $PRIMITIVE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "kind": "function", "function_id": "<function-uuid>" }'
kind defaults to "http". For an HTTP endpoint, url is required and function_id is not allowed; for a function endpoint, function_id is required and url is not allowed. enabled defaults to true. Pass domain_id to scope the endpoint to one of your organization's domains (see the active-endpoint rule below). A function has at most one endpoint — recreating it returns the existing row.
A URL can have one endpoint per domain, and endpoints with no domain_id count as one domain. This covers route targets too, unlike the default-destination rule below, and a disabled endpoint still holds its URL. Creating an endpoint at a URL another endpoint has on the same domain, or changing one's URL to it, is refused with 400; moving one onto a domain where another endpoint has its URL is refused with 409. Give each its own path (https://api.acme.dev/hooks/primitive/billing), or change or delete the other. URLs are compared whole, with only the scheme and host case-insensitive, so the same path with a different query string is accepted too, but then one handler receives each matching event once per endpoint. Creating a webhook there says so: the answer's warnings holds a same_path_as_another_endpoint entry, with its endpoint_id, for each other webhook you can list that is on and has the same scheme, host and path (at most 5, newest first), and the dashboard says so too, marking that webhook in its list. warnings is absent when this could not be checked: a caller who may not read endpoints, or a read that failed. It never repeats either URL, since a path or query string can be the receiver's own credential. The webhook is still created; delete one if you did not mean to have two. Deleting an endpoint frees its URL, and creating one there again makes a new endpoint with a new id. Function endpoints have no URL, so this does not apply to them.
The one-active-endpoint-per-domain rule
Each domain has a single default destination slot. An organization also has one org-wide fallback slot (the endpoint with domain_id: null), used for domains that have no scoped endpoint of their own.
- Only one active, default-destination endpoint may occupy a given domain's slot (or the org-wide fallback slot). Creating or re-enabling a second one returns
409 conflict; delete the existing endpoint first. - Endpoints created with
is_route_target: trueare exempt from this slot. They are reachable only through an explicit route and never serve as a domain default, so many of them can share a domain. Routes mint these for you when you point a route at afunction_id.
Plan limits apply to the default slots: the free Developer plan allows a single global (org-wide) webhook, and per-domain endpoints require a paid plan. Your effective webhooks_max_global and webhooks_per_domain limits are reported by Limits.
List, update, and delete
# list active endpoints curl https://api.primitive.dev/v1/endpoints \ -H "Authorization: Bearer $PRIMITIVE_API_KEY" # change the URL, scope, rules, or enabled flag curl -X PATCH https://api.primitive.dev/v1/endpoints/<endpoint-uuid> \ -H "Authorization: Bearer $PRIMITIVE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled": false }' # remove an endpoint curl -X DELETE https://api.primitive.dev/v1/endpoints/<endpoint-uuid> \ -H "Authorization: Bearer $PRIMITIVE_API_KEY"
PATCH accepts url, enabled, domain_id, and rules. You cannot set url on a function endpoint. Changing url keeps the endpoint: its id, routes, stats, signing secret and owed deliveries stay, and its consecutive failure count starts again. A webhook turned off because its owner changed is turned back on (the change is refused when it could not be turned on, for example at the plan's webhook limit), unless the same request sets enabled to false. A URL another endpoint on the same domain already uses, enabled or not, is refused (endpoints with no domain count as one domain). A caller limited to its own endpoints (an API key or a person limited to some mailboxes) cannot change the URL of an API key's webhook that signs with the organization's secret; give it a signing secret of its own first. Owners, admins and other callers with the whole organization's mail can change it. Deleting an endpoint deactivates it; deactivated endpoints stop receiving mail and free up their domain slot.
enabled is the endpoint's own switch, and an enabled endpoint can still be sent nothing. Each webhook in a list or get carries delivery. delivery.delivering is false when it is off, held pending a decision, or its owner cannot receive mail (for example, the API key that owns it is suspended, expired or revoked), and delivery.reasons says which, with the same codes an endpoint test gives. Anyone who may read the endpoint sees delivery, and it never names the key. For a caller with access to every mailbox, delivery.held_for_owner counts rule-granted emails waiting while the owner cannot act, and expires_at says when the oldest is given up on. delivery is null when it could not be checked just now, and create and update answers do not carry it.
The rules object
Every endpoint carries a rules object that filters which messages it receives. All fields are optional and ANDed together — a message must pass every rule present to be delivered. An empty object ({}, the default) applies no filtering.
| Field | Type | Meaning |
|---|---|---|
max_size_bytes | integer | Maximum total email size in bytes. Larger messages are not delivered to this endpoint. |
exclude_attachments | boolean | When true, deliver only messages that have no attachments. |
attachment_limit_mb | number | Maximum total attachment size in MB. Larger messages are skipped. |
sender_whitelist | array of emails | When set, deliver only from these sender addresses. |
sender_blacklist | array of emails | When set, never deliver from these sender addresses. |
event_types | array of strings | Event types this endpoint subscribes to (1 to 50 entries). Omitted = receive every event type except opt-in events, such as sent_email.*, email_rule_request.* and email_rule_objection.*, which an endpoint receives only if it lists them. An empty array is rejected; omit the field instead. Names match exactly, and a name that is not an event type is rejected with a 400 naming it (a pull destination takes only the inbound email.* types). |
The size and attachment rules (max_size_bytes, attachment_limit_mb, exclude_attachments) are evaluated against a fast approximation of the message size, not an exact byte count, so treat their thresholds as approximate.
Set rules at create time or with PATCH:
curl -X PATCH https://api.primitive.dev/v1/endpoints/<endpoint-uuid> \ -H "Authorization: Bearer $PRIMITIVE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "rules": { "max_size_bytes": 5242880, "exclude_attachments": true, "sender_whitelist": ["[email protected]"], "event_types": ["email.received"] } }'
How event_types controls subscription
event_types decides which event types reach the endpoint. If you omit it, the endpoint receives every event type except opt-in events, such as sent_email.*, email_rule_request.* and email_rule_objection.*. This is the default and the grandfathered behavior for endpoints that predate event subscriptions. If you set it, the endpoint receives only the listed types (for example email.received, email.bounced) and nothing else. See Webhook Payload for the full list of event types and their field semantics.
sent_email.accepted, sent_email.delivered, sent_email.failed and sent_email.completed (events about mail you send, see Sent Email Events) are delivered only to an endpoint that lists them, and only for sends made after the endpoint first listed one of them. They carry a different payload from inbound mail, so we recommend a dedicated endpoint; an endpoint that also handles inbound mail must keep its inbound event types in the list. Route-target endpoints, pull endpoints and Function endpoints do not receive them, and a domain-scoped endpoint receives them only for sends from its domain.
Subscriptions are matched by exact string, and a name that is not an event type is refused: a typo like email.bouncd gets a 400 naming it, instead of saving and silently matching nothing. Copy event names verbatim from Webhook Payload. A test send only validates email.received; verify specialized subscriptions such as email.bounced with a real matching event or by checking webhook deliveries.
The other rules fields filter on message content (size, attachments, sender); event_types filters on the event itself. They compose: an endpoint can subscribe to only email.received and require the message be under 5 MB and come from a whitelisted sender.
Test an endpoint
Send a synthetic email.received payload to an HTTP endpoint to confirm it is reachable and that your signature verification works:
curl -X POST https://api.primitive.dev/v1/endpoints/<endpoint-uuid>/test \ -H "Authorization: Bearer $PRIMITIVE_API_KEY"
The response reports the HTTP status your endpoint returned, a truncated body, and the signature that was sent. A webhook's signed_with says which secret signs its deliveries, the organization's or its own (see Which secret). The signature comes back for the organization's secret to a caller with permission to read the webhook secret. For a webhook's own secret it comes back only when a person owns the webhook, and only to that person; for one a key, an app or the organization owns, to nobody. The request it sends is signed either way. The test payload is signed exactly like a real delivery, so use it to validate your signature verification. It is a complete email.received event, the same shape a real delivery has, with example addresses and an email.id that starts with test-, so a handler can tell a test from real mail. Testing applies to HTTP endpoints only: there is nothing to call for a function-backed endpoint.
Because the test always sends an email.received payload, it confirms reachability and signing but cannot validate a specialized event_types subscription (such as email.bounced).
A 200 from your endpoint means it is reachable, not that mail is sent to it. The test is sent whether or not real mail would be, and the response says which apart from that: reachable is true when your endpoint answered with a 2xx, and delivery.would_deliver says whether real email.received events would be sent to it. When they would not, delivery.reasons lists every reason, each with a code and a sentence: the endpoint is turned off (disabled; the endpoint's disabled_reason says why), it is held pending an owner or admin decision after its creator's access was reduced (held), its owner cannot act now (acting_owner_suspended, which covers an expired key, acting_owner_revoked, acting_owner_left), its owner does not read mail, reads no received mail at all now, or sees headers only (acting_owner_permission, acting_owner_scope, acting_owner_depth; a caller who may not read API keys sees acting_owner_access instead, since these describe the owner's access), or it does not subscribe to email.received (event_type_not_subscribed). Which endpoint routing sends a given message to is decided per message: check a recipient with POST /v1/routes/simulate, and a received email against an endpoint with POST /v1/endpoints/<endpoint-uuid>/rules/test, which also judges the owner on that email's mailbox and the hold of the route it came through (and answers 503 if those cannot be read just now). A rule grant can still deliver an email outside the owner's mailboxes. delivery.events_paused_until is set while repeated failures have paused webhook events other than mail at this endpoint; received mail is still attempted.
Test sends are rate limited to 4 per minute and 30 per hour per organization. Successful tests, and tests against a URL on one of your verified domains, do not count against the per-minute limit.
Filters
Filters are allow/block lists evaluated on the sender address, applied in addition to any per-endpoint sender_whitelist / sender_blacklist rules. A filter has a type and a pattern:
type: "whitelist": matches the From address. A matching sender skips the blocklist and the spam threshold only when that From authenticates: DMARC-aligned SPF or DKIM, or, for a domain with no DMARC evaluation, a signature the sending provider attests for that domain. A message that got past a blocklist or the spam threshold only because of the whitelist, and then fails authentication, is not delivered and shows as rejected (sender_blocked). Mail sent through an email service provider matches on its From address, not its bounce address.type: "blocklist": matches the envelope sender (SMTPMAIL FROM) or the From address, and rejects the message.
When a blocklist rule and a whitelist rule both match, the more specific rule wins. An exact address ([email protected]) is more specific than a whole domain (*@example.com or example.com), which is more specific than a wildcard domain (*.example.com), which is more specific than *. So a blocklist entry for [email protected] still rejects that address even when example.com is whitelisted. When both rules are equally specific, the whitelist rule applies.
For allowlist-only mode, add whitelist rules for your trusted senders and a * blocklist rule to reject everyone else.
# block a specific sender org-wide curl -X POST https://api.primitive.dev/v1/filters \ -H "Authorization: Bearer $PRIMITIVE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "blocklist", "pattern": "[email protected]" }'
Patterns are normalized to lowercase and trimmed before storage, and may be up to 500 characters. New filters are created enabled.
Per-domain scoping
Pass domain_id to scope a filter to a single one of your organization's domains instead of the whole organization:
curl -X POST https://api.primitive.dev/v1/filters \ -H "Authorization: Bearer $PRIMITIVE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "whitelist", "pattern": "[email protected]", "domain_id": "<domain-uuid>" }'
Per-domain filters require a paid plan; org-wide filters (domain_id omitted or null) are available on all plans.
List, toggle, and delete
# list filters, newest first curl https://api.primitive.dev/v1/filters \ -H "Authorization: Bearer $PRIMITIVE_API_KEY" # enable or disable a filter (the only patchable field) curl -X PATCH https://api.primitive.dev/v1/filters/<filter-uuid> \ -H "Authorization: Bearer $PRIMITIVE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled": false }' # remove a filter curl -X DELETE https://api.primitive.dev/v1/filters/<filter-uuid> \ -H "Authorization: Bearer $PRIMITIVE_API_KEY"
PATCH only toggles enabled. To change a pattern or type, delete the filter and create a new one.
Webhook deliveries
Every attempt to deliver an event to an endpoint (an inbound email, or an opt-in sent_email.* event) is recorded as a delivery. The delivery history is your audit trail for what fired, when, and whether it succeeded.
A retry is recorded as a new delivery with its own id and attempt_count; an earlier attempt never changes. To follow a failing delivery, read latest_attempt in the list, or read any attempt by id: its attempts lists every attempt of that delivery. retry_scheduled says whether an automatic retry will try it again.
List deliveries
curl "https://api.primitive.dev/v1/webhooks/deliveries?limit=50&status=failed" \ -H "Authorization: Bearer $PRIMITIVE_API_KEY"
Supported query parameters:
| Parameter | Meaning |
|---|---|
limit | Page size, 1–100 (default 50). |
cursor | Opaque cursor from the previous page's meta.cursor, for pagination. |
email_id | Only deliveries for this inbound email. |
event_type | Only deliveries of this event type, for example sent_email.failed. |
sent_email_id | Only deliveries of sent_email.* events about this sent email. |
endpoint_id | Only deliveries to this endpoint. A caller limited to some mailboxes still sees only deliveries of mail it reads. |
status | One of pending, delivered, header_confirmed, failed, skipped_by_rules. |
date_from / date_to | ISO 8601 timestamps bounding created_at. |
Results are newest-first. The response meta includes total and a cursor for the next page (null when there are no more).
A delivery of an email_rule_request.* or email_rule_objection.* event (an opt-in event about a rule one member asked another for, or an objection one member made to another's rule) is listed, counted and read only by the person whose webhook it went to and by owners and admins who can list members, and only when they read every mailbox: a caller held to some mailboxes sees deliveries of mail it reads, and these carry none. Anyone else, an API key for the whole organization included, is answered as if it did not exist. These deliveries are never replayed.
A delivery of a credential.* event (the credential audit trail) is listed, counted and read only from a dashboard session, as the trail itself is: by people who can read the credential audit trail (credential_events:read), and by the person whose credential stream it went to while the stream is still theirs (not handed to someone else, and under the same membership). Either way only when they read every mailbox. Anyone else, an API key or an app acting for a person included, is answered as if it did not exist. These deliveries are never replayed.
Inspect a delivery
curl https://api.primitive.dev/v1/webhooks/deliveries/<delivery-id> \ -H "Authorization: Bearer $PRIMITIVE_API_KEY"
{ "id": "184293", "org_id": "7f2c1d84-0a5b-4c96-8e37-2b6d9f01c4a8", "email_id": "a0d6f29c-4e13-4b87-9c52-1f8a3e7b6d09", "event_type": "email.received", "sent_email_id": null, "endpoint_id": "c4a9e1b7-3d28-4f60-b915-8e2c7a0d6f31", "endpoint_url": "https://api.acme.dev/... (path and query hidden)", "status": "failed", "attempt_count": 3, "duration_ms": 812, "last_error": "HTTP 500: internal error", "last_error_code": "handler_5xx", "response_status": 500, "response_body": "internal error", "created_at": "2026-06-26T14:03:11.000Z", "updated_at": "2026-06-26T14:08:42.000Z", "email": { "sender": "[email protected]", "recipient": "[email protected]", "subject": "Disk usage warning" }, "latest_attempt": true, "retry_scheduled": true, "next_attempt_at": null, "attempts": [ { "id": "184101", "attempt_count": 1, "status": "failed", "last_error_code": "handler_5xx", "duration_ms": 640, "created_at": "2026-06-26T13:28:02.000Z", "response_status": 500, "response_body": "internal error", "retry_scheduled": false }, { "id": "184187", "attempt_count": 2, "status": "failed", "last_error_code": "handler_5xx", "duration_ms": 702, "created_at": "2026-06-26T13:33:05.000Z", "response_status": 500, "response_body": "internal error", "retry_scheduled": false }, { "id": "184293", "attempt_count": 3, "status": "failed", "last_error_code": "handler_5xx", "duration_ms": 812, "created_at": "2026-06-26T14:03:11.000Z", "response_status": 500, "response_body": "internal error", "retry_scheduled": true } ] }
Following retries. The attempts of one delivery share its endpoint and its event (or, for attempts recorded without one, its email). latest_attempt is true only on the one recorded last; a record of no attempt (attempt_count 0: a rule skip, a held send) is latest only when no attempt was made. Attempt numbers are not always increasing: a replay numbers past every attempt of the email, and automatic retries count their own passes. Failed deliveries may be retried automatically; retry_scheduled says whether one is coming. A retry decides the endpoint afresh (its rules, its owner's access, whether it is still on). A retry shows up as a new attempt once it is made. attempts (on the single read only) lists every record of the delivery in the order recorded, oldest first, each with what the endpoint answered that attempt (response_status, and response_body for a caller who reads message content, until the message's content is discarded); with more than 100, the newest 100.
Is a retry coming? A retry is a delivery pass for the email: it sends again to the endpoint the email's routing resolves then (its recorded route, or the current endpoint for its domain), unless the email already reached it, deciding it afresh. A rule-granted delivery is retried on a schedule of its own, to the same endpoint, deciding it afresh. On its latest attempt retry_scheduled is true while another attempt is to come, with next_attempt_at saying when it is due (it runs at the first retry pass at or after that time, and one runs every few minutes, so a time already past means it is due now). It is null while an attempt is under way (read it again shortly), while it waits on an owner who cannot receive mail or on a held endpoint (checked again hourly, and sent once that clears), when it is past its 7-day limit, or when that could not be read just now. It is false otherwise: once nothing will try it again, and once the endpoint is off or already has the email. retry_scheduled is null on every attempt of a delivery retried or settled on a schedule of its own that it does not report: a held one (its release settles it, including the attempts it makes itself), a pull attempt, and a sent_email.* or other event delivery. On the latest attempt that failed, is pending or was skipped in a way still owed, of an email that was not rejected and still has its content, to an endpoint that is still on, takes the event, has an owner the pass would deliver to (a revoked or suspended key, or an owner that may not receive the message, reads false) and has not received the email by another delivery, it is null while a delivery pass for the email is running (whether another follows is decided when it ends; read it again shortly), true when the email has another automatic pass to come (a scheduled retry, the reclaim of a pass that stopped, or the recovery of a first pass that did not finish), and false otherwise. Because a pass decides afresh, true promises a pass, not a delivery to this endpoint, and next_attempt_at stays null on every delivery that is not rule-granted. Every other attempt, including every attempt a later one followed, is false: no automatic retry of the email's delivery will try it again, which does not mean nothing else ever will.
status is delivered (2xx), header_confirmed (2xx plus a confirmation header from your handler), failed, or pending. last_error and last_error_code explain failures. event_type is the delivered event type (null for deliveries recorded before event types existed). A sent_email.* delivery has email_id and email set to null and sent_email_id set to the send it is about. endpoint_url names a webhook by its scheme and host only, followed by /... (path and query hidden) when its URL has more, since a path or query string can be the receiver's own credential; GET /v1/endpoints/{id} has the whole URL. For a function-backed endpoint, endpoint_url is an opaque function://<id> identifier rather than a callable URL. A redirect's location in last_error is named the same way. Where a webhook's answer repeats its URL's query string, a query parameter, a query value long enough to be a credential, or its userinfo, that part is stored as (hidden); the rest of the answer is kept as your handler wrote it.
For a webhook URL, an HTTP failure records the error.code from a Primitive API error envelope when your handler returns one, and otherwise handler_4xx or handler_5xx. The exact status is in last_error (for example HTTP 500: internal error).
What a webhook answered (the text after the status in last_error, response_body, and a code it chose) is shown only to a caller who can read that webhook with GET /v1/endpoints/{id}, since an answer can restate the URL's credential. Anyone else who can see the delivery, such as a member limited to some mailboxes reading a delivery to someone else's webhook, gets the status with a note (HTTP 404 (the receiver's answer is shown only to callers who can read this webhook)), response_body as null, and handler_4xx or handler_5xx in place of the webhook's own code. A deleted webhook's answers stay readable only to callers who read every webhook. An email's webhook_last_error is whichever endpoint failed last, so its answer is quoted only for a caller who can read every webhook it may have come from, a delivery still under way included; a Function's text keeps the Function's own rule when no webhook can be the source. Text the platform writes, such as a redirect that was not followed, a network error or a rule skip, is shown to everyone.
Deliveries to a function-backed endpoint can carry these last_error_code values in addition to the ordinary HTTP failure codes:
function_timeout: the invocation exceeded the 10-minute wall-clock window and was canceled. The delivery is retried on the standard schedule. See Functions for how the wall-clock and CPU limits relate.content_discarded: the stored email content was discarded before the delivery could complete, so there was nothing left to deliver.superseded: another delivery to the same endpoint already succeeded for this email, so this one was skipped rather than delivered twice.queue_dead_letter: the dispatch could not be completed after repeated attempts. Replay the delivery to try again.
Replay a delivery
After fixing a handler, re-send the stored payload to the endpoint:
curl -X POST https://api.primitive.dev/v1/webhooks/deliveries/<delivery-id>/replay \ -H "Authorization: Bearer $PRIMITIVE_API_KEY"
{ "delivered": 1, "failed": 0 }Replay of an inbound email delivery resends the original event's content unchanged, so the same email is not re-ingested, and refreshes only the delivery fields: the event id and delivery.endpoint_id for the endpoint it is sent to, delivery.attempt, delivery.attempted_at and the download links (see replays). A replay to the same endpoint keeps the original event id, so your handler's idempotency rules still apply. Replay re-sends to the original endpoint at its current URL. If its URL was changed since the delivery, the replay is checked as a new send there would be: you must be allowed to manage the endpoint, and it must still take the event type. An active original endpoint with enabled: false is rejected. A deactivated original with a recorded active successor follows that successor; a soft-deleted endpoint without one is rejected. If the original row no longer exists, replay may choose an eligible fallback endpoint. Replay supports inbound email deliveries and sent_email.* deliveries, and is rate limited per organization. A sent_email.* delivery is replayed with the same event id but a payload rebuilt from the send as it is now; it is rejected if the send was deleted or the endpoint is no longer an active http endpoint.
To re-deliver a whole inbound email (rather than one delivery row), see the replay flow in Receiving Mail.
Related pages
- Custom Routes: bind recipient address patterns to destinations.
- Receiving Mail: the end-to-end inbound flow.
- Webhook Payload: event types and field semantics.
- Signature Verification: verify the signed payloads endpoints and tests send.
- Functions: hosted inbound handlers.
- Connectors: managed integrations.
- Errors and Limits: error codes and plan limits.
- API Reference: the full v1 surface.