API error handling by example
Every Orbit API error returns the same envelope: a machine-readablecode, a human-readable message, the HTTP status, and contextual details, plus a meta.docs_url that links straight to the error’s remedy. This guide walks that envelope, works through three real failures end to end, and gives you a retriable-vs-terminal decision table so your integration handles errors the same way every time.
For the full code list, see the Error Code Reference. For codes by domain with remediation notes, see Error Codes.
1. The error envelope
A failed request returns the same JSON shape regardless of which endpoint rejected it:
Two shape notes that matter in code:
detailsis optional. Legacy and triage envelopes may omit it — always guarddetails?.fieldrather than assuming the key exists.- 429 bodies are self-describing. A rate-limited response carries the back-off hint inside the envelope (
error.retry_afteron the global limiter,details.retry_after_secondson caps) and as aRetry-Afterheader — read whichever your HTTP client exposes.
2. Three worked failures with the fix in code
(a) INVALID_PHONE_NUMBER — a non-E.164 to on an SMS send
Sending to a number that isn’t E.164 (a bare digit string, a local-format number) is rejected with HTTP 422 before anything dispatches:
+{country-code}{national-number}, no spaces or dashes) and resend:
INVALID_PHONE_NUMBER as a validation failure to correct, not a send failure to retry:
INVALID_FROM_NUMBER (the from side) and INVALID_RECIPIENT — all three mean “the number shape is wrong for this send,” never a transient provider hiccup.
(b) RATE_LIMITED 429 — read Retry-After, back off exponentially
When your request rate exceeds the global limiter, Orbit answers with HTTP 429 and a body that tells you exactly how long to wait:
Retry-After: 4 header. Honor it before any exponential fallback — the server-set value reflects the actual bucket refill time, so a blind doubling may retry too soon (and re-429).
Node retry wrapper:
retry_after over your own doubling whenever it’s present — the limiter knows when its window opens; your client doesn’t.
(c) FREQUENCY_CAP_EXCEEDED — skip the recipient, don’t fail the batch
A frequency cap you configured (Settings → Frequency caps, or the Frequency Caps API) rejects an over-cap direct send with HTTP 429 and a details block naming the exact rule that fired:
- Never re-POST immediately. A capped recipient 429s again until
retry_after_secondselapses — an immediate retry just burns an API call. Wait the server-providedretry_after_seconds, not a fixed backoff: the oldest in-window send ages out then, and the slot reopens. - Batch sends skip by design. Campaign and drip sends don’t 429 per recipient — the capped recipient reports
status: "skipped", reason: "frequency_capped"(with the samefrequency_cap_idandretry_after_secondsfields) and the batch moves on. Handle the skip status in your result reconciliation rather than trying to pre-filter caps yourself.
3. Retriable vs terminal
Classify every error you handle into one of three buckets — this decision determines whether you retry, defer, or surface the failure to the caller:
The decision tree:
- Is
error.codein the permanent set you know are yours? (INVALID_PHONE_NUMBER,NO_SENDER_CONFIGURED,INSUFFICIENT_BALANCE,RECIPIENT_OPTED_OUT, …) → Terminal. Fix the input or surface to the caller; never retry the same body. - Is the status 429? → Deferred. Read
retry_after/retry_after_secondsand re-queue the send after that delay.FREQUENCY_CAP_EXCEEDEDis a per-recipient skip;RATE_LIMITEDis a global back-off. - Is the status 5xx? → Retriable.
INTERNAL_ERRORandSERVICE_UNAVAILABLErepresent platform-side conditions that resolve; retry with exponential backoff and a capped attempt count. If the original request was a send, reuse the same idempotency key so a retry can’t double-send. - Unlisted
error.codeon a 5xx? Treat it as retriable until the reference says otherwise — the platform returns 5xx only for server-side conditions, and preferring a cautious retry over dropping a recipient is the safe default.
4. From error to remedy via meta.docs_url
Every envelope carries meta.docs_url, a per-code link into the Error Code Reference:
- In developer tooling. Log or surface
docs_urlalongside the error so the person debugging lands directly on the code’s anchor instead of searching the reference. - In your own error dictionary. Map the codes you handle to the remediation you apply (
INVALID_PHONE_NUMBER→ normalize E.164;FREQUENCY_CAP_EXCEEDED→ defer recipient), and point the mapped entry at the same docs anchor for the human follow-up.
- Error Code Reference — every registered code, with HTTP status and cause.
- Error Codes — the common codes, pinned with the envelope shape.
See also
- Frequency caps — the rules behind
FREQUENCY_CAP_EXCEEDED - API integration — base URLs, sandbox, and the rate-limit headers
- Pagination —
INVALID_CURSORhandling for list endpoints