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
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | Body or query failed schema validation. |
| 401 | unauthorized | API key missing, malformed, or revoked. |
| 403 | forbidden / endpoint-specific | Authenticated but not allowed (e.g. recipient_not_allowed). |
| 404 | not_found | Resource doesn't exist or isn't visible to your org. |
| 422 | endpoint-specific | Semantically invalid (e.g. inbound_not_repliable). |
| 429 | rate_limit_exceeded | Check the Retry-After header (seconds). |
| 500 | internal_error | Always 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}/replayand 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.
/api/v1/emailsList inbound emails. Filter by domain, status, date.
/api/v1/emails/{id}Fetch one inbound email with full body, headers, and replies[].
/api/v1/emails/{id}Soft-delete an inbound email.
/api/v1/emails/searchFull-text search with snippets, facets, per-field filters.
/api/v1/emails/{id}/rawOriginal RFC 5322 bytes ( message/rfc822).
/api/v1/emails/{id}/attachments.tar.gzAll attachments as a single tar.gz archive.
/api/v1/emails/{id}/replayRe-deliver the inbound webhook to all active endpoints.
Rate-limited 10/min, 60/hr per org.
/api/v1/emails/{id}/discard-contentPermanently delete body and attachments; preserves headers and threading metadata.
Outbound mail
Send fresh mail and reply to inbound. See /docs/sending for concepts.
/api/v1/send-mailSend a new outbound message.
1k/hr, 10k/day per org. Body cap 256 KB.
/api/v1/emails/{id}/replyReply to an inbound email; recipient and threading auto-derived.
/api/v1/sent-emailsList outbound messages sent via the API.
/api/v1/sent-emails/{id}Fetch one sent email with full body, SMTP response, and DKIM info.
/api/v1/send-permissionsList the allowlist rules (managed zones, your domains, known addresses, member emails) currently in effect.
/api/v1/sendabilityPre-flight: would this recipient be allowed? No side effects, no rate-limit charge.
Domains
/api/v1/domainsList verified and unverified domain claims.
/api/v1/domainsClaim a domain. Optional enable_outbound auto-generates a DKIM keypair.
/api/v1/domains/{id}Toggle is_active or set per-domain spam_threshold.
/api/v1/domains/{id}Delete a domain claim.
/api/v1/domains/{id}/verifyRun DNS check and promote the domain to verified.
Filters
Allow/block rules applied at MX time, before webhooks fire.
/api/v1/filtersList allow/block rules.
/api/v1/filtersCreate an allowlist or blocklist rule, optionally scoped to a domain.
Rate-limited 20/min per org.
/api/v1/filters/{id}Toggle enabled.
/api/v1/filters/{id}Delete a filter.
Webhook endpoints
Manage where Primitive POSTs inbound emails.
/api/v1/endpointsList endpoints with health stats.
/api/v1/endpointsCreate a webhook endpoint. HTTPS-only, public-IP only.
/api/v1/endpoints/{id}Update url, enabled, domain_id, or rules.
/api/v1/endpoints/{id}Soft-delete an endpoint.
/api/v1/endpoints/{id}/testSend a synthetic test payload and return the receiver response.
4/min per org (lifted on 2xx).
Webhook deliveries
/api/v1/webhooks/deliveriesList webhook delivery attempts. Filter by email_id, status.
/api/v1/webhooks/deliveries/{id}/replayReplay a stored payload.
Shares 10/min, 60/hr quota with email replay.
Account
/api/v1/accountOrg metadata, plan, onboarding state.
/api/v1/accountUpdate spam_threshold or discard_content_on_webhook_confirmed.
/api/v1/account/storageStorage usage in bytes / KB / MB and email count.
/api/v1/account/webhook-secretFetch (auto-create on first call) the webhook signing secret.
/api/v1/account/webhook-secret/rotateRotate the webhook secret; the old value is invalidated immediately.
1 per 60 minutes per org.
Public utilities
No auth required. IP-rate-limited.
/api/v1/test-emailSend a quick test email to verify a self-hosted MX setup.
/api/v1/claim-subdomainClaim a *.primitive.email subdomain for a self-hosted install.
/api/v1/openapiMachine-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.