Skip to main content

API Integration

Orbit is API-first. Every feature you can click in the dashboard is backed by a REST endpoint you can call from your own code. This page is the map to the rest of the API reference — where to start, what to test against, and what to watch for in production.

Base URLs

There’s one base URL. Sandbox is not a separate host — it’s the same API, switched into test mode per request. You select sandbox by how you authenticate, not by changing the URL:
  • Server-to-server: use a test secret key (dv_test_sk_…). Any request signed with a test key runs in sandbox automatically.
  • From the dashboard (Clerk session): send X-Test-Mode: true. This header is honored only for dashboard sessions — a live API key (dv_live_sk_…) cannot flip into sandbox with it.
Sandbox mirrors production endpoints 1:1. Requests don’t move real money, don’t deduct credits, and don’t dispatch real messages (SMS/WhatsApp/RCS/email/voice are simulated, not sent). Simulated sends return deterministic delivery receipts so you can exercise both the success and failure paths.
Sandbox responses carry "test_mode": true in the meta block. Assert on that to confirm a request ran in sandbox — there is no separate environment header or hostname.

Authentication

Every request needs an API key in the X-API-Key header:
  • Keys are per-tenant. Subaccounts get their own scoped keys.
  • Keys have prefix: dv_live_sk_ (production) or dv_test_sk_ (sandbox).
  • Rotate keys anytime at Settings → API Keys — rotation is instant, old keys invalidate.
  • Leaked a key? Revoke immediately; Orbit does not charge you for traffic after the revoke timestamp.
Full detail: Authentication.

Channels covered by the API

Everything in the dashboard is available via API. Same payloads, same behavior. Each channel has its own endpoint and its own request schema under the /messages prefix — POST /messages/sms, /messages/whatsapp, /messages/rcs, /messages/viber, and /messages/email. There is no single channel-polymorphic /messages route and no top-level channel discriminator: the body shape differs per channel (an SMS send takes to/from/body, a WhatsApp template send takes a template block, and so on) rather than being switched by a channel field. Pick the endpoint that matches the channel you’re sending on.

Your first API call

The fastest way to confirm your key works:
Response — 202 Accepted. The send result is wrapped in the standard envelope ({ data, meta }); the message fields live under data, and meta carries the request_id you should log:
status reflects the persisted state at send time (sent once the provider accepts, queued/scheduled for deferred sends, test_sent in sandbox). There is no top-level cost_estimate on the send response — pull cost and segment counts from GET /messages/:id (or the delivery webhook) once billing settles. Sandbox sends add "test_mode": true to meta. Check status at GET /messages/:id or subscribe to delivery webhooks (recommended).

Webhooks

Webhooks are how Orbit tells your backend about events: a message was delivered, a call connected, an agent escalated a conversation, a number was ported in. You register a URL and we POST events to it. Setup
  1. Dashboard → Settings → Webhooks → Add endpoint
  2. Enter your HTTPS URL (HTTP rejected in production)
  3. Pick the event types you want (or subscribe to * for everything)
  4. Copy the signing secret (whsec_...) — you need it to verify signatures
Every Orbit webhook carries an X-Orbit-Signature header (the canonical signature). We also still emit X-Devotel-Signature with the same value for back-compat — older integrations can keep reading it. During a signing-secret rotation grace window a third header, X-Orbit-Signature-Next, carries the rotation-candidate signature. Verify the signature before trusting the payload. The header is Stripe-style, t=<unix>,v1=<hex> — not a bare hex digest:
Replay protection: reject any signature whose t (timestamp) is older than 5 minutes. Retries Orbit retries failed webhook deliveries on a fixed 6-step schedule: the initial delivery plus up to 6 retries (7 attempts total). Each retry fires a set interval after the previous attempt fails — 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours, then 24 hours — not a smooth exponential curve from a short base delay. Every delay gets up to 20% extra jitter on top, so a burst of failures against one endpoint doesn’t retry in lockstep and re-overload it on recovery. Because the last two steps are 8 and 24 hours apart, an endpoint that stays down keeps receiving retries for roughly 35 hours before the schedule is exhausted. Size your delivery monitoring around that full window — the largest gap between two attempts is a full day, not seconds. The event stays in your log either way, so you can replay it from Dashboard → Webhooks → Event log. A webhook is “successful” if your server returns any 2xx within 30 seconds. Anything else (timeout, 5xx, 4xx except 410 Gone) triggers retry. Return 410 Gone to permanently disable a single event (e.g., you’ve deprecated handling for that type). Dead endpoints If we see 50 consecutive failures, we auto-disable the endpoint and email the org admin. Re-enable it from the dashboard once you’ve fixed it. Full detail: Webhooks overview, Event catalog, Security.

Rate limits

Default limits apply per API key. Higher limits available on request. Rate-limit headers are on every response:
When you exceed the limit you get HTTP 429 with:
Use details.retry_after (seconds), not fixed backoff. Full detail: Rate Limits.

Error codes

All errors follow the same shape:
Error codes are UPPERCASE constants (e.g. INSUFFICIENT_BALANCE) — match on error.code exactly. Always log meta.request_id — support can trace it end-to-end. Common codes you’ll hit during integration: Full list: Error Codes.

Idempotency

Every POST that creates a resource (message, call, contact, campaign) accepts an Idempotency-Key header. Use it.
Orbit stores the response for 24 hours. Replay the exact same key + body → you get the cached response, not a duplicate send. Replay with a different body → 409 IDEMPOTENCY_KEY_REUSED. This is the single most important pattern for production reliability. Use it on every creation endpoint.

Pagination

List endpoints return cursor-paginated results:
Pass ?cursor=<meta.pagination.cursor>&limit=100 for the next page. No offset — cursor is stable against concurrent inserts. Full detail: Pagination.

Postman collections

Pre-built collections per channel — import one and you’re testing in 30 seconds. Each collection has variables for {{base_url}} and {{api_key}} pre-wired. Default environment is sandbox. Also available: OpenAPI 3.1 spec at https://api.orbit.devotel.io/api/v1/openapi.json — import into any codegen (openapi-generator, oats, orval, kiota).

SDKs

If you’d rather not hand-roll HTTP, we maintain official SDKs. They handle auth, retries, pagination, webhook signature verification, and typed models.
  • Node.js / TypeScript — generally available
  • Web (browser) — generally available
  • Python, Go, Java, PHP, Ruby, C# / .NET — in beta, pending publication to their package registries (PyPI, pkg.go.dev, Packagist, RubyGems, Maven Central, NuGet). The source lives in the monorepo today; until each is published you can’t install it from the package manager, so call the REST API directly — our cURL examples translate cleanly into any language.
Browser: use the Orbit Web SDK for client-side widgets (chat embed, voice browser calling). Never ship a secret key (dv_live_sk_… / dv_test_sk_…) to the browser — client-side code uses a publishable key (dv_live_pk_… / dv_test_pk_…). The widget then bootstraps a short-lived visitor session JWT by calling POST /widget/session with your public widget_id; every other widget request carries that token as Authorization: Bearer <token>.

Environments & promotion

Recommended integration flow:
  1. Develop against sandbox with dv_test_sk_* keys (or X-Test-Mode: true from the dashboard)
  2. Stage against sandbox with your staging webhook URL (not production)
  3. Prod swap keys + webhook URL; everything else identical
Sandbox retains data for 30 days then purges. Do not rely on it for long-term storage.

Support

Always include request_id from the failing response in any support ticket — it’s our fastest route to root cause.