PristineSend
Get started
API Reference

Error codes

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.

Error format

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.

{
  "error": {
    "code": "missing_field",
    "message": "Field 'to' is required.",
    "param": "to",
    "request_id": "req_8f3c9a1b2d4e5f6071829304"
  }
}
FieldDescription
codeStable, machine-readable code from the closed set below. Branch on this.
messageHuman-readable, safe to show. May change — do not match on it.
paramPresent only on field-validation errors; names the offending field.
request_idAlways 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.

Request IDs

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.

Error codes

The complete set of code values, with the HTTP status each maps to. Some codes set extra headers (Retry-After on 429/503).

StatuscodeWhen
400invalid_requestMalformed or unparseable request body.
400missing_fieldA required field is absent. Sets param to the field name.
400invalid_fieldA field is present but invalid (bad email, wrong type, malformed cursor). Sets param.
400idempotency_key_invalidThe Idempotency-Key header was sent but empty, or longer than 255 characters.
401unauthorizedMissing, malformed, unknown, or revoked API key (never reveals which).
402overage_cap_reachedThe workspace transactional spend cap has been reached.
403account_not_approvedThe workspace is not yet approved for sending.
403account_suspendedThe workspace is suspended.
403sending_pausedSending is paused for the workspace.
403transactional_pausedTransactional sending is paused for review (does not affect campaigns or read endpoints).
403domain_not_verifiedThe send-gate found no verified sending domain.
403recipient_suppressedThe 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.
403sender_not_verifiedThe supplied from — or the sender_id's domain — is not verified for your workspace. On /send/batch this appears as a per-item error.
403plan_limit_reachedAdding this contact would exceed your plan's active-contact limit. Remove contacts or upgrade to add more.
404not_foundThe resource does not exist in this workspace (a non-UUID id also returns 404).
404sender_not_foundThe sender_id does not exist in your workspace (wrong or cross-tenant id). On /send/batch this appears as a per-item error.
409conflictA state conflict, e.g. a contact with that email already exists.
409idempotency_key_in_progressA concurrent request with the same Idempotency-Key is still processing. Retry shortly.
422idempotency_key_reusedThe same Idempotency-Key was sent with a different request body. The key is valid, but the params conflict.
429rate_limitedToo many requests — the per-key rate limit (1000/minute) was exceeded. Sets Retry-After.
502provider_errorThe email provider (SES) rejected the message. Generic message; detail is logged server-side.
503service_unavailableA dependency we need to account for the request (e.g. metering) is briefly unavailable. Retryable; sets Retry-After.
503rate_limit_unavailableThe 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.
500internal_errorAn 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.

Handling errors

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:

"color:#ff7b72">const res = "color:#ff7b72">await fetch("https://pristinesend.com/api/v1/send", {
  method: "POST",
  headers: {
    "Authorization": "Bearer ps_live_YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ to, subject, html }),
})

// Capture the request id "color:#ff7b72">from EVERY response (success or error) for support.
"color:#ff7b72">const requestId = res.headers.get("X-Request-Id")

if (!res.ok) {
  "color:#ff7b72">const { error } = "color:#ff7b72">await res.json()
  // error.code is stable; error.message is human-readable and may change.
  console.error(error.code, error.message, error.request_id)

  if (res.status === 401) throw "color:#ff7b72">new AuthError(error.message)
  if (res.status === 400) throw "color:#ff7b72">new ValidationError(error.message, error.param)
  if (res.status === 429) {
    // Honour Retry-After before trying again.
    "color:#ff7b72">const retryAfter = Number(res.headers.get("Retry-After") ?? 60)
    throw "color:#ff7b72">new RateLimitError(error.message, retryAfter)
  }
  // 402 / 403 are caller-state issues; 5xx are transient — retry with backoff.
  throw "color:#ff7b72">new ApiError(error.code, error.message)
}

"color:#ff7b72">const { id } = "color:#ff7b72">await res.json()