Skip to main content

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.code is the decision key — branch on it; never parse error.message in code.
  • error.status gives the coarse HTTP class.
  • error.details carries the structured context that names the fix — which field failed, how many seconds to wait, which domain rule fired.
  • meta.request_id is the server-side correlation id — include it when you escalate.
  • meta.docs_url resolves to the code’s anchor on the reference page.
For a fuller field walk, see API error handling by example.

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:
Each entry has a 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 status flips to rejected and read the rejection object 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 failed status with a provider rejectReason.
The rule: once the request is accepted, move your error-handling to the lifecycle surface (webhook or status poll), not the HTTP response.

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 an OrbitApiError hierarchy; the subclass is chosen by HTTP class, and .code holds the stable decision key:
Subclass accessors (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 the error.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.