Receiving mail

Inbound mail arrives at Primitive's MX servers, gets parsed, hashed, and analyzed, and is then delivered to you in two ways: as a signed webhook and as a row in the REST API. You can use either, both, or only the API.

Webhook delivery

Add a webhook endpoint and every accepted inbound email POSTs to it with Content-Type: application/json and a Primitive signature header. Most orgs run a single endpoint; the API supports multiple with per-domain routing (see Endpoints below). The SDK handles verification:

import primitive from '@primitivedotdev/sdk';
const client = primitive.client({ apiKey: process.env.PRIMITIVE_API_KEY! });
export async function POST(req: Request) {
// Verifies the signature, parses JSON, validates the schema,
// and returns a normalized ReceivedEmail.
const email = await primitive.receive(req, {
secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,
});
console.log(email.sender.address, email.subject);
// Optional: reply within the same handler.
await client.reply(email, 'Got it.');
return Response.json({ ok: true });
}

The full payload schema is documented in Webhook payload. Signature details are in Signature verification.

Retries

If your endpoint returns non-2xx or is unreachable, Primitive retries up to 6 times over ~10 hours:

  1. Immediate
  2. +60 seconds (inline)
  3. +5 minutes
  4. +30 minutes
  5. +2 hours
  6. +8 hours

The event id stays stable across retries, so use it for idempotency. After all six attempts fail, the email moves to the dashboard logs as failing and waits for a manual replay.

Replay

From the dashboard logs page, you can replay any email or any specific delivery attempt. The same payload is re-sent with the same event id. After all automatic retries are exhausted, replay is the way to recover from a handler bug.

Programmatically: POST /api/v1/emails/{id}/replay or POST /api/v1/webhooks/deliveries/{id}/replay. Both share a 10/min, 60/hr per-org rate limit.

Idempotency

Track the event id (evt_ + 64 hex chars) and skip duplicates. Network retries, manual replays, and your own at-least-once processing all benefit. Always return 2xx for events you've already handled. An error response will be retried.

Delivery acknowledgment

Return any 2xx HTTP response after you process the event. Primitive treats that as the delivery acknowledgment; no SDK-specific confirmation header is required. Non-2xx responses or network failures enter the retry schedule above.

If you need to remove stored body content and attachments after processing, use the dashboard action or POST /api/v1/emails/{id}/discard-content. Headers remain so threading and audit metadata still work.

REST API: pulling inbound mail

You don't need a webhook to read mail. The REST API has:

EndpointReturns
GET /api/v1/emailsCursor-paginated list. Filter by domain_id, status, date_from/date_to, or simple search.
GET /api/v1/emails/{id}Single email with full body, headers, and an array of associated replies.
GET /api/v1/emails/searchFull-text search with snippets, facets, and a q DSL plus per-field filters.
GET /api/v1/emails/{id}/rawOriginal RFC 5322 message bytes as message/rfc822.
GET /api/v1/emails/{id}/attachments.tar.gzAll attachments as a single tar.gz archive.

Full details and other endpoints (delete, reply, replay, discard-content) are in the REST API reference.

Email lifecycle

  • pending: received, awaiting initial processing.
  • accepted: passed filters, queued for webhook delivery.
  • completed: webhook delivered with a 2xx response.
  • rejected: bounced at the MX layer (filter rule, blocklist, or hard fail). No webhook fires.
  • failing (display-only): accepted but webhook delivery has failed at least once and is still being retried, or has exhausted retries and is waiting on a manual replay.

Filters

Add allow/block rules from the dashboard or via POST /api/v1/filters. Filters apply at MX time, before webhooks fire. Per-domain or org-wide.

Endpoints

Each webhook endpoint exposes delivery counters, health metrics, and optional per-domain routing rules. Trigger a one-off probe with POST /api/v1/endpoints/{id}/test to send a synthetic payload and inspect the receiver's response.

Limits

  • Max inbound size: 5 MB during beta, expanding to 25 MB.
  • Raw email is inlined (base64) up to 256 KB; larger messages are download-only via content.download.url.
  • Download URLs (raw and attachments) expire 24 hours after delivery.

SSRF / egress

Webhook URLs must be HTTPS on port 443 to a public IP. RFC 1918 / loopback / link-local / documentation ranges are blocked at egress, and the resolved IP is pinned to the connection (DNS-rebinding-safe).