Error-handling runbook
The error-code reference lists every code the platform can emit. This guide turns that list into an operating procedure: classify the code, decide whether to retry with the same Idempotency-Key or fix the request, and know where the error surfaces when the HTTP response was 200. Run the one-step check under each dominant code before you change code, and use the decision table at the end as your triage shortcut.1. The error envelope
Every Orbit API error returns the same envelope:error.codeis the decision key — branch on it; never parseerror.messagein code.error.statusgives the coarse HTTP class.error.detailscarries the structured context that names the fix — which field failed, how many seconds to wait, which domain rule fired.meta.request_idis the server-side correlation id — include it when you escalate.meta.docs_urlresolves to the code’s anchor on the reference page.
2. Taxonomy by HTTP class
Async gates such as
QUIET_HOURS_BLOCKED look 4xx-shaped but behave like a clock-based 429: schedule the send inside allowed hours instead of failing the job.
3. Dominant codes and their remediation
VALIDATION_ERROR — fix the body, keep the key
The request body or query failed schema validation; the reply is 400 or 422 and details.issues lists each offending field (see section 4). Map the issues back to your form, fix the input, and retry with the same Idempotency-Key — replaying a key after fixing the body is exactly what the replay cache is for.
INSUFFICIENT_SCOPE — rotate the key, don’t toggle the endpoint
The API key authenticates but the token lacks the scope this endpoint needs (for example messages:write on sends). Treat 403 as a key/scope problem, not an endpoint outage: issue a new key with the needed scope in the dashboard (Developers → API keys), or have an admin add the scope, then retry. Keep subaccount key scopes minimal.
RATE_LIMITED — back off, don’t hammer
The tenant’s sliding-window limiter trips at 429. Read the Retry-After header (or details.retry_after_seconds on cap-style limits such as SMS_RATE_LIMITED) and send again once it expires. In the SDK the error is an OrbitRateLimitError, so branch on the class instead of matching the code string.
INSUFFICIENT_BALANCE — top up, then replay safely
The wallet pre-flight rejects with 402 before anything dispatches. Top up (or enable auto-top-up) and retry with the same Idempotency-Key: a 402 never dispatched, so reusing the key is safe. Balance mutations additionally guard themselves with DEDUCT_IN_FLIGHT so a retry cannot double-charge.
MESSAGE_SEND_FAILED — check the provider, not the payload
The send reached a provider and the provider rejected it (502). That is different from VALIDATION_ERROR: the payload was fine, so check the channel — sender registration, template status, channel health — then retry. If the channel dry-run is unhealthy, fix the channel before sending more traffic; if it is healthy, retry with backoff.
QUIET_HOURS_BLOCKED — schedule, don’t retry in place
The universal quiet-hours gate blocks non-voice outbound sends (SMS, MMS, WhatsApp, RCS, Email, Telegram, Viber, Instagram, Messenger, LINE, Apple Messages) during the recipient’s local restricted window. Treat it as a scheduling signal: queue the send for the allowed window rather than retrying immediately. The companion QUIET_HOURS_TIMEZONE_UNKNOWN means the recipient’s timezone could not be resolved — set an explicit timezone on the contact, or schedule into UTC-safe hours.
Suppression gates — respect the opt-out, route to re-permission
Suppression hits are consent outcomes, not send failures. When a contact sits on the suppression list (opted out, blocked, or bounced), fix it at the contact level — re-permission them through an opt-in flow, or remove the address from your audience — rather than retrying the send. Honoring suppression is a deliverability requirement, not just an error-handling rule.4. Walking the per-field issues list
VALIDATION_ERROR replies carry details.issues, a per-field array from the server-side schema check. Walk it and map each entry back to your form validation instead of showing the raw message:
field you map to a form input and a message you can show to the operator. Preserve the server’s field names — don’t re-derive them from the request payload.
5. Errors that surface after a 200
The envelope above describes synchronous rejections. Async surfaces — batch jobs, drip steps, and porting orders — accept the request and report failure later:- Batch jobs and drip steps — the create call returns 200/202; per-item failures land on the job status resource and on webhook events. Consume them with a webhook consumer and read the item-level failure there instead of polling the request log.
- Porting orders — a carrier-side rejection arrives after acceptance; poll the order (or consume its webhook) until
statusflips torejectedand read therejectionobject on the order payload for the carrier’s reason. - The message ledger — a 200 on the send endpoint means accepted; delivery failure shows up when you GET the message record as a
failedstatus with a providerrejectReason.
6. Idempotent retry after errors
The idempotency contract tells you when replaying a key is safe. Pair it with the code classification:
Terminal codes (403 scope, 402 balance, 409 conflict, suppression/consent gates) are terminal for this request shape — you fix a precondition, then retry. Retryable codes (429, 5xx, provider-rejected dispatch) are safe to retry with the same Idempotency-Key because the platform deduplicates on it.
7. Reading errors in the SDK and raw REST
The Node SDK throws anOrbitApiError hierarchy; the subclass is chosen by HTTP class, and .code holds the stable decision key:
isRateLimited, isClientError, isServerError, status) let you branch without matching code strings; requestId and docsUrl travel into every log line. Over raw REST, read error.code from the JSON envelope the same way. Log the correlation id on either path — it is what support needs to find the server-side trace.
8. The generated reference is a floor
The reference at Error codes enumerates the SDK-typed union of codes — the codes a compile-time client can plan for. At runtime, treat theerror.code in the response body as authoritative: new codes land between deployments before the typed union refreshes, and operational codes from channel providers can surface through the envelope without being in the generated table. Branch on the body, and use the reference plus the troubleshooting hub as the lookup layer, not the ground truth.
Decision table
Cross-links: Error codes reference, API error handling by example, Idempotency and safe retries, Consuming webhooks, Troubleshooting hub.