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 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 theX-API-Key header:
- Keys are per-tenant. Subaccounts get their own scoped keys.
- Keys have prefix:
dv_live_sk_(production) ordv_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.
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: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- Dashboard → Settings → Webhooks → Add endpoint
- Enter your HTTPS URL (HTTP rejected in production)
- Pick the event types you want (or subscribe to
*for everything) - Copy the signing secret (
whsec_...) — you need it to verify signatures
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:
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:
HTTP 429 with:
details.retry_after (seconds), not fixed backoff. Full detail: Rate Limits.
Error codes
All errors follow the same shape: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
EveryPOST that creates a resource (message, call, contact, campaign) accepts an Idempotency-Key header. Use it.
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:?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.
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:- Develop against sandbox with
dv_test_sk_*keys (orX-Test-Mode: truefrom the dashboard) - Stage against sandbox with your staging webhook URL (not production)
- Prod swap keys + webhook URL; everything else identical
Support
- API questions & bugs: developer-support@devotel.io
- Compliance (WhatsApp/10DLC/RCS): compliance@devotel.io
- Urgent production: in-app chat (Dashboard → bottom-right) — 24/7 for paid tenants, business hours for Free tier
- Status page: status.orbit.devotel.io
request_id from the failing response in any support ticket — it’s our fastest route to root cause.