Errors

Every error response from https://api.primitive.dev/v1 uses the same envelope so agents and SDKs can branch on machine-readable codes without parsing HTTP status text or scraping prose.

Envelope

{
  "success": false,
  "error": {
    "code": "unauthorized",
    "message": "Invalid or missing API key",
    "request_id": "req_01HZ8K3M2N7Q5R8S9T0V1W2X3Y"
  }
}
  • error.code is a stable machine-readable identifier. Agents and SDKs branch on this value, not on the human-readable message.
  • error.message is a short human-readable description. It can change between releases without warning. Do not parse it.
  • error.request_id echoes the server-issued request id and is also returned in the X-Request-Id response header. Include this in support escalations so we can find the failing request in our logs.

Validation errors additionally carry an error.details object listing the rejected fields:

{
  "success": false,
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation",
    "details": { "from": "must be a verified outbound domain" },
    "request_id": "req_..."
  }
}

Error codes

CodeHTTP statusMeaningRecovery
unauthorized401Missing or invalid bearer token. The response carries a spec-shaped WWW-Authenticate: Bearer realm="Primitive API", resource_metadata="..." header pointing at the protected-resource metadata.Acquire a token via the discovery flow at /auth.md, then retry. Do not loop.
forbidden403The bearer is valid but lacks the scope required for the operation.Reissue the token with the missing scope, or surface the missing scope to the human owner. Do not retry with the same token.
not_found404The resource id does not exist or is not visible to the current organization.Verify the id was returned from a previous call in the same org context. Do not retry.
validation_error400Request body or query parameters failed schema validation. error.details enumerates the rejected fields.Inspect details, fix the offending fields, retry once. Do not loop without changing input.
mx_conflict409A domain claim conflicts with an existing mailbox provider on the same domain.Surface the conflict to the user; pass confirmed: true in the request body to override.
rate_limited429Per-organization rate limit exceeded (default: 120 requests / 60 seconds, sliding window).Honor the Retry-After response header before retrying. Implement client-side back-off.
service_unavailable503A backing dependency is temporarily unreachable or misconfigured on our side.Retry with exponential back-off. Do not exceed 5 attempts; persist failures past that should escalate.
internal_error500An unhandled fault on the server side. The request_id is the entry point for our debugging.Retry once with back-off, then escalate with the request_id if persistent.

Rate-limit responses

429 responses carry:

Retry-After: 30

The integer is seconds. Agents should sleep for at least that many seconds before retrying the same operation. The body matches the standard envelope:

{ "success": false, "error": { "code": "rate_limited", "message": "Rate limit exceeded" } }

Idempotency

The /send-mail endpoint accepts an optional Idempotency-Key request header. When provided, replays of the same key return the original outcome (including the Idempotency-Key response header echoing the effective value) rather than firing a second send. Use this for any operation an agent might retry after a network failure.

If you omit the header, Primitive derives a key from the canonical request payload and returns it in the Idempotency-Key response header so you can use it on a follow-up retry.

Webhook signature failures

Webhook delivery failures appear in webhook event logs, not in API responses. See the signature verification guide for verifier logic.

Versioning and deprecation

The error envelope is part of the v1 contract. Breaking changes will only land in a new major version (/v2). See API versioning.