REST API

Base URL https://primitive.dev/api/v1. Bearer auth with prim_ keys. OpenAPI spec at primitive.dev/api/v1/openapi.

Authentication

curl https://primitive.dev/api/v1/account \
-H "Authorization: Bearer prim_..."

Generate, rotate, and revoke under Settings → API keys. Multiple keys per org are supported; revocation takes effect on the next request.

Response envelopes

Every response wraps the data in a stable envelope. Success:

{
"success": true,
"data": { /* the resource */ },
"meta": {
"cursor": "…",
"limit": 50,
"total": 1234
}
}

Error:

{
"success": false,
"error": {
"code": "validation_error",
"message": "from: expected string, received undefined",
"request_id": "f0d…",
"details": { /* endpoint-specific */ },
"gates": [ /* present on send-mail recipient denials */ ]
}
}

request_id is also returned as an X-Request-Id response header. Quote it in support requests and you'll save everyone a round-trip.

Error codes

StatusCodeMeaning
400validation_errorBody or query failed schema validation.
401unauthorizedAPI key missing, malformed, or revoked.
403forbidden / endpoint-specificAuthenticated but not allowed (e.g. recipient_not_allowed).
404not_foundResource doesn't exist or isn't visible to your org.
422endpoint-specificSemantically invalid (e.g. inbound_not_repliable).
429rate_limit_exceededCheck the Retry-After header (seconds).
500internal_errorAlways carries a request_id; quote it in support.

Rate limiting

Default v1 quota: 120 requests per minute per org, sliding window. Specific routes set their own caps:

  • POST /api/v1/send-mail: 1,000 / hour and 10,000 / day per org.
  • POST /api/v1/emails/{id}/replay and webhook delivery replay: 10 / min, 60 / hr per org (shared).
  • POST /api/v1/filters: 20 / min per org.
  • POST /api/v1/endpoints/{id}/test: 4 / min per org (skipped on 2xx receivers).
  • POST /api/v1/account/webhook-secret/rotate: 1 / 60 min per org.

429 responses include a Retry-After header.

Pagination

Cursor-paginated. Pass cursor from the previous page's meta.cursor; null means the last page. Default limit 50, max 100.

Idempotency

Send-mail accepts an Idempotency-Key header. See Sending mail for details.

Endpoints by area

Inbound emails

List, fetch, search, and manage received mail.

GET/api/v1/emails

List inbound emails. Filter by domain, status, date.

GET/api/v1/emails/{id}

Fetch one inbound email with full body, headers, and replies[].

DELETE/api/v1/emails/{id}

Soft-delete an inbound email.

GET/api/v1/emails/search

Full-text search with snippets, facets, per-field filters.

GET/api/v1/emails/{id}/raw

Original RFC 5322 bytes ( message/rfc822).

GET/api/v1/emails/{id}/attachments.tar.gz

All attachments as a single tar.gz archive.

POST/api/v1/emails/{id}/replay

Re-deliver the inbound webhook to all active endpoints.

Rate-limited 10/min, 60/hr per org.

POST/api/v1/emails/{id}/discard-content

Permanently delete body and attachments; preserves headers and threading metadata.

Outbound mail

Send fresh mail and reply to inbound. See /docs/sending for concepts.

POST/api/v1/send-mail

Send a new outbound message.

1k/hr, 10k/day per org. Body cap 256 KB.

POST/api/v1/emails/{id}/reply

Reply to an inbound email; recipient and threading auto-derived.

GET/api/v1/sent-emails

List outbound messages sent via the API.

GET/api/v1/sent-emails/{id}

Fetch one sent email with full body, SMTP response, and DKIM info.

GET/api/v1/send-permissions

List the allowlist rules (managed zones, your domains, known addresses, member emails) currently in effect.

POST/api/v1/sendability

Pre-flight: would this recipient be allowed? No side effects, no rate-limit charge.

Domains

GET/api/v1/domains

List verified and unverified domain claims.

POST/api/v1/domains

Claim a domain. Optional enable_outbound auto-generates a DKIM keypair.

PATCH/api/v1/domains/{id}

Toggle is_active or set per-domain spam_threshold.

DELETE/api/v1/domains/{id}

Delete a domain claim.

POST/api/v1/domains/{id}/verify

Run DNS check and promote the domain to verified.

Filters

Allow/block rules applied at MX time, before webhooks fire.

GET/api/v1/filters

List allow/block rules.

POST/api/v1/filters

Create an allowlist or blocklist rule, optionally scoped to a domain.

Rate-limited 20/min per org.

PATCH/api/v1/filters/{id}

Toggle enabled.

DELETE/api/v1/filters/{id}

Delete a filter.

Webhook endpoints

Manage where Primitive POSTs inbound emails.

GET/api/v1/endpoints

List endpoints with health stats.

POST/api/v1/endpoints

Create a webhook endpoint. HTTPS-only, public-IP only.

PATCH/api/v1/endpoints/{id}

Update url, enabled, domain_id, or rules.

DELETE/api/v1/endpoints/{id}

Soft-delete an endpoint.

POST/api/v1/endpoints/{id}/test

Send a synthetic test payload and return the receiver response.

4/min per org (lifted on 2xx).

Webhook deliveries

GET/api/v1/webhooks/deliveries

List webhook delivery attempts. Filter by email_id, status.

POST/api/v1/webhooks/deliveries/{id}/replay

Replay a stored payload.

Shares 10/min, 60/hr quota with email replay.

Account

GET/api/v1/account

Org metadata, plan, onboarding state.

PATCH/api/v1/account

Update spam_threshold or discard_content_on_webhook_confirmed.

GET/api/v1/account/storage

Storage usage in bytes / KB / MB and email count.

GET/api/v1/account/webhook-secret

Fetch (auto-create on first call) the webhook signing secret.

POST/api/v1/account/webhook-secret/rotate

Rotate the webhook secret; the old value is invalidated immediately.

1 per 60 minutes per org.

Public utilities

No auth required. IP-rate-limited.

POST/api/v1/test-email

Send a quick test email to verify a self-hosted MX setup.

POST/api/v1/claim-subdomain

Claim a *.primitive.email subdomain for a self-hosted install.

GET/api/v1/openapi

Machine-readable OpenAPI 3.0 spec for the v1 API.

Timestamps and IDs

  • All timestamps are ISO 8601 in UTC.
  • Most resource IDs are UUIDs. Webhook delivery rows use a numeric bigint.
  • Webhook event ids are evt_ + 64 hex chars; stable across delivery retries.
  • API keys are prim_ + 64 hex chars.