The PristineSend API uses conventional HTTP status codes to indicate success or failure. Every /api/v1/* error response shares one envelope, and this page is the canonical reference other pages point to.
Every non-2xx response body is a JSON object with a single error object. The HTTP status carries the category; the machine-readable code carries the specifics.
| Field | Description |
|---|---|
code | Stable, machine-readable code from the closed set below. Branch on this. |
message | Human-readable, safe to show. May change — do not match on it. |
param | Present only on field-validation errors; names the offending field. |
request_id | Always present; mirrors the X-Request-Id response header. |
internal_error and provider_error always return a generic message — the underlying detail is logged server-side and never returned.
Every response — success and error — carries an X-Request-Id header (e.g. req_8f3c9a1b2d4e5f6071829304). On errors the same value also appears as error.request_id in the body. Capture and log it: it lets support trace a specific call.
The complete set of code values, with the HTTP status each maps to. Some codes set extra headers (Retry-After on 429/503).
| Status | code | When |
|---|---|---|
| 400 | invalid_request | Malformed or unparseable request body. |
| 400 | missing_field | A required field is absent. Sets param to the field name. |
| 400 | invalid_field | A field is present but invalid (bad email, wrong type, malformed cursor). Sets param. |
| 400 | idempotency_key_invalid | The Idempotency-Key header was sent but empty, or longer than 255 characters. |
| 401 | unauthorized | Missing, malformed, unknown, or revoked API key (never reveals which). |
| 402 | overage_cap_reached | The workspace transactional spend cap has been reached. |
| 403 | account_not_approved | The workspace is not yet approved for sending. |
| 403 | account_suspended | The workspace is suspended. |
| 403 | sending_paused | Sending is paused for the workspace. |
| 403 | transactional_paused | Transactional sending is paused for review (does not affect campaigns or read endpoints). |
| 403 | domain_not_verified | The send-gate found no verified sending domain. |
| 403 | recipient_suppressed | The recipient address is on your suppression (do-not-send) list. On /send/batch this appears as a per-item error rather than a whole-request 403. |
| 403 | sender_not_verified | The supplied from — or the sender_id's domain — is not verified for your workspace. On /send/batch this appears as a per-item error. |
| 403 | plan_limit_reached | Adding this contact would exceed your plan's active-contact limit. Remove contacts or upgrade to add more. |
| 404 | not_found | The resource does not exist in this workspace (a non-UUID id also returns 404). |
| 404 | sender_not_found | The sender_id does not exist in your workspace (wrong or cross-tenant id). On /send/batch this appears as a per-item error. |
| 409 | conflict | A state conflict, e.g. a contact with that email already exists. |
| 409 | idempotency_key_in_progress | A concurrent request with the same Idempotency-Key is still processing. Retry shortly. |
| 422 | idempotency_key_reused | The same Idempotency-Key was sent with a different request body. The key is valid, but the params conflict. |
| 429 | rate_limited | Too many requests — the per-key rate limit (1000/minute) was exceeded. Sets Retry-After. |
| 502 | provider_error | The email provider (SES) rejected the message. Generic message; detail is logged server-side. |
| 503 | service_unavailable | A dependency we need to account for the request (e.g. metering) is briefly unavailable. Retryable; sets Retry-After. |
| 503 | rate_limit_unavailable | The rate-limit check couldn't be evaluated on a cost-bearing or bulk request (deliverability check, batch send), which we fail closed rather than run unmetered. Retryable; sets Retry-After. Single sends fail open instead. |
| 500 | internal_error | An unexpected server fault. Generic message; safe to retry with backoff. |
The three idempotency_key_* codes only appear when you send an Idempotency-Key header — see Idempotent requests for how keys, replays, and these codes work.
Two limits are easy to confuse. overage_cap_reached (402) is a billing cap — you have used your plan's paid transactional allowance, so it clears by raising the cap or starting a new billing period, not by waiting. rate_limited (429) is a rate limit that clears with time — honour Retry-After and retry.
Branch on the HTTP status and error.code rather than the message, which may change. Retry 429 after Retry-After and 5xx with exponential backoff: