Note de langue
Lorsqu’une traduction n’est pas disponible, le contenu anglais est affiché comme solution de repli. Conservez les codes d’erreur, les chemins d’API et les blocs de code.Frequently Asked Questions
General
What is Orbit?
Orbit is Devotel’s Agentic Customer Communications Cloud (Agentic CCC) — one platform spanning 9 aaS pillars (CPaaS, CCaaS, UCaaS, AIaaS, NaaS, CSPaaS, RTC PaaS, CXaaS, CDPaaS). It provides APIs for SMS, WhatsApp, RCS, Viber, Email, and Voice — plus AI-native features like autonomous agents, visual flow builders, and intelligent routing.Do you really ship SAML SSO and SCIM provisioning, or are they roadmap?
Both are shipped, per-organization features — not roadmap items. SAML SSO configuration is owner/admin-managed (writes are owner-only) under Settings → Security and readable over the API by owner/admin:GET /api/v1/settings/sso returns the organization’s entity ID, sign-on URL, and whether SSO is enabled, with the signing certificate masked ([REDACTED] — the stored value is never returned); the fuller write surface is the GET/PATCH pair on /api/v1/settings/saml. SCIM federation uses a dedicated Bearer token with the org-scoped base URL (/scim/v2/{orgSlug}) configured under Settings → SCIM. The model behind both — what gets federated, the create → update → deactivate → deprovision lifecycle — is on Identity federation: SAML and SCIM. The setup prerequisites: an organization owner performs the configuration, and enablement is staged — enabled turns SSO on as an available sign-in method, enforced makes it the only method, so set enforced only once SSO works in production for your team and a second owner can still break-glass.
Two-factor authentication for dashboard login is likewise configured at the organization level: the Security page carries the operator 2FA cards (TOTP with backup codes, with the step-up challenge on disable), and an org-wide require-2FA policy under Settings → Security makes a second factor mandatory for every member on next login. Don’t confuse it with the Verify product — TOTP, passkey, and push factors for your application’s end users are a separate, API-only suite (see Verify overview). The per-member enrollment mechanics are in the Authentication section below — the answers there still apply unchanged under SSO.
What channels does Orbit support?
Orbit supports nineteen communication channels — fifteen core channels plus four APAC chat apps: Core- SMS — Global coverage in 190+ countries (SMS)
- WhatsApp — Business API with template and session messaging (WhatsApp)
- RCS — Rich messaging on Android devices (RCS)
- Viber — Popular in Eastern Europe and Southeast Asia (Viber)
- Email — Transactional and marketing email (Email)
- Voice — Outbound/inbound calls, SIP trunking, IVR, and AI voice agents (Voice)
- Fax — Send and receive faxes as PDF or TIFF documents, with delivery receipts (Fax)
- Video — In-browser video rooms from the API or dashboard, with recording and live broadcast (Video)
- USSD — Menu sessions over dial codes for feature phones and 2G (USSD)
- Push — Mobile and web push notifications over APNs and FCM (Push)
- Telegram — Bot messaging with text, media, and inline keyboards (Telegram)
- Instagram — Direct messages with text, media, quick replies, and story mentions (Instagram)
- Messenger — Facebook Page messaging with quick replies, personas, and message tags (Messenger)
- Slack — Workspace messaging via OAuth install, with slash commands and events (Slack)
- Wallet passes — Issue Apple and Google Wallet loyalty cards, coupons, and event tickets (Wallet passes)
- LINE — the dominant chat app in Japan, Thailand, and Taiwan
- KakaoTalk — Korea’s dominant messaging app (Alimtalk + Friendtalk)
- WeChat — China’s super-app (Official Account template messages)
- Zalo — Vietnam’s leading chat platform (ZNS notifications)
Where is Orbit hosted?
Orbit runs on Google Cloud Platform (GKE Autopilot) in theeurope-west1 region (Belgium). Data is stored in Cloud SQL PostgreSQL 16 with at-rest encryption.
Can I migrate from Twilio (or another CPaaS) to Orbit?
Yes — plan it as five steps, and treat the API surfaces as different, not drop-in compatible. Orbit is not a Twilio-shaped API behind a new URL: endpoints, request fields, and webhook payload shapes all differ, so every integration point needs a mapping, not a redirect.- Port your numbers in. Bring your existing phone numbers over with a standard port request — the checklist and per-country lead times are in Number porting. Port-out protection is a tenant-owned switch, so once you control it the Port-out blocked runbook covers a refused port in either direction.
- Map MessagingServiceSid to a messaging service. Twilio’s Messaging Service (the
MessagingServiceSidyou send against) maps to an Orbit messaging service — routing, sender pool, and delivery configuration in one named object. Create it and assign the sender pool in the messaging services console, then send against the service instead of a rawFromnumber. - Map your sender pool. Recreate the senders behind each Twilio service — long codes, short codes, toll-free, alphanumeric sender IDs — as the sender pool on the matching Orbit messaging service. Regulatory requirements travel with the sender (US 10DLC campaign registration, alphanumeric sender-ID pre-registration in the markets that require it), so register the senders on Orbit before cutover, not after.
- Update your webhook handling. Orbit signs every delivery with HMAC-SHA256 in the
X-Orbit-Signatureheader instead of Twilio’sX-Twilio-Signature, and the event payload shape differs from Twilio’s status callbacks. Verify with Webhook security and re-point your handlers at the Orbit event names in Webhook events. - Validate in sandbox before cutover. Run the whole integration — sends, inbound replies, webhook signatures, sender-pool routing — against sandbox and test mode first, then cut traffic over number by number.
How is each error-code family indexed across the platform?
An errorcode moves through four pages that each have one job:
- Glossary — tells you what the term itself means (DLR, sender ID, quiet hours) once the terms in a runbook are unfamiliar.
- Error codes reference — gives the full definition for a specific
code. Its From a code to a runbook table sorts every code into one of three retry classes:- Deterministic rejects (422 validation, tenant-owned pre-send gates) — never retry until the payload or tenant config changes.
- Transient faults (429 / 5xx) — retry with
details.retry_after, or exponential backoff when that field is absent. - Conditional retries — retry only once the named condition is met (stale SSH session, cold wallet balance, over-cap 10DLC campaign, etc.).
- Troubleshooting hub — indexes every runbook page by surface: messaging, numbers, voice, video, compliance/deliverability, webhooks, Verify, email, billing, account, platform, channels, and more.
- FAQ — answers the “why did my X happen” questions the runbooks don’t phrase directly.
PENDING_ACTION_*, COMPLIANCE_PROFILE_*, VIDEO_*, or WHATSAPP_CALLING_* code goes to the glossary for the vocabulary, the error-codes table for the retry class, the hub’s Video or Compliance accordion for the matching runbook, and falls to the FAQ for the common-question angle.
Why does my branded console fall back to the plain theme with DOMAIN_NOT_FOUND?
The pre-render lookups that resolve your custom domain to its organization (/public/resolve-domain by host, /public/resolve-org-domain by org, /public/branding for the login-screen logo) all return 404 DOMAIN_NOT_FOUND until the custom-domain record reaches active: never claimed, still pending on DNS, verifying on the managed certificate, or failed on issuance. While the lookup misses, the dashboard degrades to the neutral default theme instead of erroring out — fix the claim (re-verify from Settings → Branding → Custom domain) and resolution recovers on its own within the short edge-cache window. The full runbook is Troubleshooting: DOMAIN_NOT_FOUND on branded console and public resolve; the status lifecycle is in Custom domains and managed SSL.
Data residency & privacy
Where is my data stored, and is a DPA available?
All tenant data runs on Google Cloud Platform in theeurope-west1 region (Belgium) on Cloud SQL PostgreSQL with at-rest encryption, and each organization’s data is isolated in its own tenant schema. For GDPR purposes, a self-serve Data Processing Agreement (GDPR Art. 28) is available to execute in-platform, and the Trust Center carries the current data-residency and security posture. The full region and storage breakdown is on Data residency overview.
How do I exercise GDPR rights on my own customers’ data?
The DSAR guide walks you through data-subject access requests — export, redact, and delete requests for the personal data you hold in your tenant — and the GDPR posture guide maps which controls you own on the platform side.What certifications back the residency claim?
The Evidence binder holds the SOC 2, ISO 27001, GDPR, and HIPAA compliance packs, including the attestations behind the EU-residency claim.My erasure POST returned ERASURE_COOLING_OFF_ACTIVE — can I cancel or wait?
That 409 is a duplicate-filing refusal, not a failure: a pending or executing Article-17 erasure request already exists for the contact, and only one can be active at a time. You have three moves, all tenant-owned:
- Wait for the active request to execute. Each request carries a cooling-off window (7 days by default, per-tenant overridable) — read
cooling_off_ends_atonPOST /api/v1/contacts/:id/gdpr/erasure-requestor list them viaGET /api/v1/contacts/:id/gdpr/erasure-requests. When the window lapses, the scheduler hard-deletes the contact and writes the per-resource audit chain, and filing a fresh request for another contact is unaffected. - Cancel the active request. While it is still
pending(inside the cooling window),POST /api/v1/contacts/:id/gdpr/erasure-request/:requestId/cancel(orPOST /api/v1/compliance/dsar/erasure-requests/:id/cancelon the org-level surface) withdraws it — after which a new POST for that contact is accepted again. An optionalreasonrecords why the request was withdrawn. - Accept the terminal 409.
DSAR_NOT_CANCELLABLEis the terminal gate on the cancel path: the request no longer exists, or it already completed, failed, expired, or was cancelled — a cancel at that point is a no-op you resolve on your side, not a retryable error. Poll the request status instead of re-cancelling.
422 CONTACT_ERASURE_PENDING gate on sends — is on the pending-compliance and erasure gates troubleshooting page.
Why did my residency change return 409 RESIDENCY_LOCKED?
The residency pin (PUT /api/v1/compliance/data-residency) is locked, and a locked pin cannot be silently re-homed to another region — moving the pinned region would strand data already written under the old boundary, which is exactly the silent cross-border transfer GDPR Art. 44 forbids. Two readiness refusals sit around the same surface, and the details field tells you which one fired:
RESIDENCY_LOCKED(409) — the region-change call hit a configuration whoselocked: trueflag protects the existing pin. The deliberate path is the unlock call: an admin explicitly POSTs/api/v1/compliance/data-residency/unlock, runs the governed region migration, then re-locks. Skipping the migration (unlock, change, stay unlocked) defeats the control — treat unlock as an audited, out-of-band step, not a routine part of a region change.RESIDENCY_NOT_ENFORCED(409) — you POSTed/lockbefore anything is enforceable. Lock requires an already-pinned, already-enforced region; the bootstrap isPUT /api/v1/compliance/data-residencywithenforced: trueon a live region first, then lock it.RESIDENCY_REGION_UNAVAILABLE(409) — you asked to enforce a region whose POP and region-scoped storage are not provisioned yet. Advisory pins to a preview region are allowed (they register intent withenforced: false); what you cannot do is promise enforcement where the platform has no region-scoped storage. Either enforce the currently-live region, wait for the region to graduate, or pin advisorily and enforce later.
locked / enforced flags) before retrying: GET /api/v1/compliance/data-residency returns the singleton config per organization, and a 404 RESIDENCY_NOT_FOUND is the signal nothing is pinned yet. The full region catalog, availability states, and unlock-escalation path are on Data residency overview; the per-plane residency matrix the pin controls is on the same page.
Platform Concepts
What’s the difference between UCaaS and CPaaS?
CPaaS (Communications Platform as a Service) is a set of APIs — messaging, voice, video — that a developer integrates into their own application to add communication features. UCaaS (Unified Communications as a Service) is a finished internal-communications product — browser softphone, SIP trunking, PBX replacement — configured through a dashboard rather than code. Orbit ships both on the same tenant: build with the CPaaS APIs, or run your team’s own phone system on the UCaaS pillar.What’s the difference between an SMS gateway and an SMS API?
An SMS gateway is the underlying wire-protocol connection — traditionally an SMPP bind, an on-premises appliance, or a GSM-modem device — that relays messages to a carrier’s SMSC. An SMS API is the application-layer interface built on top of that gateway; Orbit’s isPOST /api/v1/messages/sms (see SMS). Most integrations call the SMS API and never touch the underlying gateway protocol directly.
What’s the difference between SSO, IdP-initiated vs SP-initiated SAML, and SCIM provisioning — and does Orbit ship all three?
The three terms answer different questions, and Orbit ships all of them per organization:- SSO (single sign-on) is the outcome: one login at your identity provider opens the dashboard. SAML 2.0 is the protocol Orbit uses to get there — on each login your IdP sends a signed assertion, Orbit verifies it against your stored signing certificate, and a dashboard session is minted.
- IdP-initiated vs SP-initiated names where the login starts. IdP-initiated starts on the IdP’s app portal (the user clicks the Orbit tile and is posted over with an assertion); SP-initiated starts on Orbit’s side — the user opens the SSO entry point
/auth/saml/{orgSlug}/login, is 302-redirected to the IdP to authenticate, and the IdP posts the signed assertion back to/auth/saml/{orgSlug}/callback. Both land in the same verified session. Orbit publishes the pairing for both: the public login entry point above, an SP metadata URL to import (/auth/saml/{orgSlug}/metadata), and an ACS callback URL you paste into the IdP — the exact values per IdP (Okta, Entra ID) are in the SAML enrollment guide. - SCIM provisioning answers a different question entirely — which users exist in your Orbit organization, not how they sign in. Your IdP (Okta, Microsoft Entra ID, OneLogin, any RFC 7643/7644 client) pushes user and group changes to Orbit’s SCIM base URL (
/scim/v2/{orgSlug}), so a person assigned the Orbit app exists as a member — with a role — before their first login. SAML without SCIM still works, but someone has to invite each user manually first; the protocols pair but never imply each other, and both are tenant-owned, owner-gated org configurations (Settings → Security for SAML, Settings → SCIM for provisioning).
Authentication
How do I get an API key?
- Sign up at orbit.devotel.io
- Navigate to Settings > API Keys
- Click Create Key
- Copy and securely store the key — it’s only shown once
What’s the difference between live and test keys?
Are public keys read-only?
Yes. Because a public key (dv_..._pk_) is meant to be embedded in client-side code, it is restricted to read-only scopes when you create it. Write and administrative scopes — messages:write, contacts:write, admin, the * wildcard, and the sensitive account reads billing:read / settings:read — are rejected with a 422.
For anything that sends or modifies data, use a secret key (dv_..._sk_) from your backend. Still treat any key you ship to a browser as publicly visible and grant it the minimum reads it needs.
Can I use the same key across multiple applications?
Yes, but we recommend creating separate keys for each application or service for better security and audit tracing. You can create unlimited API keys.Can I run SSO for dashboard login while still using API keys for automation — and are SCIM-provisioned users blocked from the API?
Yes to the first, and no to the second. SAML SSO gates dashboard login only: it decides how a person reaches the web dashboard, and its stages areenabled (SSO offered alongside password sign-in) and enforced (SSO is the only way in). API keys are an orthogonal credential class — minted per member under Settings → API Keys, sent as X-API-Key, and tenant-bound to your organization by construction — so automation keeps working on keys regardless of how the humans sign in, and an enforced SSO posture does not lock out your integrations. Session-expiration behavior under SSO is no special case: the minted dashboard session is the same JWT the rest of the dashboard runs on, and when it expires the member signs in again — through the IdP when SSO is on, by password where that method is still permitted.
SCIM-provisioned members are not blocked from the API either. Every provisioned user is a normal org member who can create and use API keys, and deprovisioning cooperates with that: when the IdP sends DELETE, Orbit revokes the member’s active sessions and API access in one cascade — the member’s own API keys are revoked too, and owned assets transfer to an owner. What the cascade deliberately does not do is rotate anything owned by the organization or your other members — keys held by teammates, shared integrations, sender identities stay exactly as they are, and deciding to rotate those is an owner/admin action, never an automatic step. Treat that as the offboarding checklist: audit what the departed member touched and rotate shared credentials from Settings → API Keys where your policy calls for it — SCIM determines who the account is, not which of the wider organization’s credentials change. The split between the three credential classes (session token, API key, SCIM token) is modeled on Authentication and session model; the SAML stages are in the enrollment guide.
Why can’t I revoke my current session from Settings → Sessions?
Revoking the session you are signed in with would end your own login while you are still using it — the device goes silent for you, and the session you actually meant to close stays open. To stop that, the revoke endpoint refuses self-revocation:DELETE /api/v1/settings/sessions/{sessionId} compares the session id that your current request is authenticated with against the target session id, and a match returns 400 CANNOT_REVOKE_CURRENT_SESSION with the message “You can’t revoke the session you’re currently signed in with. Use sign-out instead.” The Active Sessions card under Settings → Security only offers Revoke on your other devices, so you normally see this error only when an API client (or a stale dashboard tab) targets your own live session.
The correct move is the one the message names: use the normal sign-out flow to end the session you are on. Sign-out and revoke are two different paths — signing out ends your current session through the identity provider’s logout, while revoke targets a different session from a device that stays signed in.
One adjacent note for support tickets: if the platform cannot resolve your current session when the guard runs, the guard degrades to no self-match and a warning with the request id is logged — include the meta.request_id from the response envelope when you report unexpected revoke behavior so the log line can be found:
DELETE /api/v1/settings/sessions/{sessionId}, documented under Revoke one of your active sessions). The code and its behavior are in the Error Code Reference; the session model is on Authentication and session model.
Messaging
What phone number format does Orbit use?
All phone numbers must be in E.164 format:+[country code][number]. Examples:
- US:
+14155552671 - UK:
+447911123456 - Turkey:
+905551234567
How are SMS messages billed?
SMS messages are billed per segment:- GSM-7 encoding (standard characters): 160 chars per segment, 153 for multi-part
- Unicode (emoji, non-Latin scripts): 70 chars per segment, 67 for multi-part
How is my number warm-up ceiling computed?
The per-number warm-up ceiling follows a geometric curve that starts small and compounds daily, capped at 3,000 messages/day per DID:
The curve is the same default shape for every new DID: a day-0 base of 50 with roughly 35% daily compounding, hard-capped at 3,000. The ramp is tracked through four phases —
initial → ramp → steady → verified — and the engine gates every outbound SMS send: a send past today’s ceiling is refused with a 429 (DAILY_CAP_EXCEEDED when the day’s count is spent, WARMING_QUOTA_EXCEEDED when the growth curve is the binding limit, or NUMBER_MPS_EXCEEDED when you are bursting faster than the per-second rate).
Two properties can pull the ceiling down from the base curve, but nothing can lift it above:
- DLR-driven reputation: delivery receipts over a rolling 30-day window drive a 0–100 health score. An
excellent,good,fair, orunknowntier lets the ramp proceed at 100%; apoortier holds it at 50% of the curve; acriticaltier throttles it to 25%. Complaint-coded carrier rejections count heavily against the score. - Trust score: the 10DLC registration ecosystem assigns your brand a trust tier. A higher trust score moves the phase boundaries further out, but the per-day numbers inside a phase follow the same geometric ramp.
GET /api/v1/numbers/:id/warming — it returns the current warming_phase, today’s daily_cap versus the live current_day_count, the trust_score, and the warming_started_at anchor the curve compounds from. To plan a launch, check GET /api/v1/numbers/:id/warming/progression, which returns the warming_state and a 15-day forward forecast of the daily ceiling — you can map out your send volume against the ramp without polling day-by-day.
For the comparative chart across assets (email IP, sending domain, phone number, and alphanumeric sender ID), see How is warm-up different across an email IP, a phone number, and a sender ID?. The full ramp walkthrough, pacing guide, and quota-error handling are in the number warm-up guide; the carrier-reputation model and the closed-loop reputation feedback mechanics are in Sender warming and reputation.
What happens if a message fails?
Failed messages can be retried usingPOST /api/v1/messages/{id}/retry. The original message parameters are reused. Orbit also supports automatic retry through campaign settings.
Do I need 10DLC registration for US SMS?
Yes. All Application-to-Person (A2P) SMS to US numbers requires 10DLC registration through The Campaign Registry. See our 10DLC guide for step-by-step instructions.Why was my SMS rejected a few days after my number was registered?
That is the number warming ramp, not a registration failure. A newly acquired 10DLC number starts with a small per-day send ceiling that grows as the ramp advances (initial → ramp → steady → verified), and sends past today’s ceiling are refused with a 429 before any carrier is attempted: DAILY_CAP_EXCEEDED (the number’s daily cap is spent, resets at midnight UTC), WARMING_QUOTA_EXCEEDED (today’s growth-curve ceiling on a still-warming number), or NUMBER_MPS_EXCEEDED (you are bursting faster than the per-second rate — back off one second). Watch the ramp on GET /api/v1/numbers/:id/warming (live daily_cap vs current_day_count, warming_phase, trust_score) and GET /api/v1/numbers/:id/warming/progression (15-day ceiling forecast). Do not retry-loop inside the same day — the retry_after on a warming 429 points at the next UTC window, so re-route the overflow to an already-warmed sender instead. The carrier-reputation model behind the ramp is on Sender warming and reputation; the full decision table and escalation criteria are on Troubleshooting: number warming caps, and the warmup terms are defined in the glossary.
Why is my message stuck in queued and how do I unstick it?
queued means the message has not been handed to a sender or provider yet — the hold is on your side of the handoff, not carrier-side. Work Troubleshooting: message stuck in queued — that page owns the queued lane: the send ladder pending → queued → sending → sent → delivered, the full cause table, and the escalation path. List the stuck rows on the dashboard Delivery Log with direction=outbound&status=queued, or via GET /api/v1/messages?status=queued. The usual causes: an exhausted sender pool, an email warm-up cap, or provider connectivity trouble. Unstick it by clearing that hold — never by double-sending a copy (you can duplicate when the hold clears). The decoder above covers the whole ladder; a row parked scheduled, pending, or by your quiet-hours window belongs to the sister page — the routing note on the hub names each stage’s owner.
Why did my scheduled-message edit or cancel return 409 MESSAGE_NOT_EDITABLE / MESSAGE_NOT_CANCELLABLE?
Both verbs — PATCH /api/v1/messages/:id (edit) and DELETE /api/v1/messages/:id or POST /api/v1/messages/cancel-scheduled (cancel) — are gated on the row still being in scheduled at the instant you touch it. Once the send_at drain promotes the row (it moved on to queued/sending/sent, or someone else already cancelled it), the gate closes and the call returns 409 instead of silently rewriting a message that already left. Two worked refusals:
GET /api/v1/messages/:id) and reconcile against it — never blind-retry the same edit or cancel, because a 409 here is a deterministic state verdict, not a transient fault. If the message is already past scheduled and you still need that send to go out (or not), cancel what you can and re-create the send with corrected content or a new send_at. The full gate model is on Scheduled sends (send_at), the edit/cancel API walkthrough is in the Message scheduling guide, and the drain and promotion mechanics are on Scheduled send never fired. Both codes are enumerated in the Error Code Reference.
Why won’t a call route to my agent even though their status looks available?
Dispatch eligibility is a hard gate — the dispatcher rings an agent only when their queue-membership state isavailable, they are a member of the queue, and their skills cover it. Two mechanics make an “available” agent get skipped: the dispatcher reads the worst of the agent’s states across queues, so one leftover busy/wrapup membership elsewhere blocks the ring; and a stale toggle mapping away → paused invisibly parks an agent the UI shows on-shift. Check GET /v1/voice/agents/:id/status per queue. The model is on Agent presence and aux-code lifecycle and dispatch gating on The ACD queue model.
What happens when my APNs token returns Unregistered — does the send retry?
No. A per-device provider rejection (APNsUnregistered / an expired FCM token) is a deterministic refusal, so the platform does not retry it and you should not either. The send still returns 201: the affected device lands in notifications[] as status: "failed" with an error string naming the provider’s verdict. Tokens APNs/FCM report as permanently gone are pruned from your registry automatically, so the next send skips them rather than failing them again. Re-check GET /api/v1/push/notifications for the error per device and re-register the device token from the SDK — do not replay the same body. The common-error and per-device decoder table is on the Push channel page; the sandbox-vs-production note (a BadDeviceToken rejection driven by NODE_ENV, not by the token being invalid) is under Apple Push setup.
Can I preflight a push send against device capability flags before blasting the audience?
Yes, but you do it against the registered-device list, not a capability linter. There is no pre-send gate that validates capability flags (sound, badge, mutable-content) — those are resolved against the APNs category at send time. The preflight move is: callGET /api/v1/push/device-tokens for the target user (or resolve the device ids you hold) and drop any token whose platform you do not want (for example a web subscription you meant to exclude from a mobile-only blast), then send against the filtered device_token_ids. STOP / opt-out and disabled tokens are filtered server-side on every send regardless, so a user_ids broadcast reaches only deliverable devices; the capability-flag resolution itself is the notification-category expansion described on the Push fields section. If you want a break-glass confirmation that the audience is deliverable before you blast, check the user’s registered tokens explicitly — never rely on the send to surface a removed device.
How are per-device results on POST /api/v1/push/send aggregated?
The response is the aggregation: an immediate send returns 201 with notifications[] — one entry per matched device, each with its own id (push_…), deviceTokenId, status, and (on failure) the provider error. The data object also carries total, sent, and devices_targeted counters, so a broadcast reports its full fan-out in one response. GET /api/v1/push/notifications then returns the same per-device delivery log (newest-first, pageable with cursor/limit), and the push.delivered / push.opened webhooks carry the receipts per device id — so you can reconcile and audit per device rather than reconcile a single opaque verdict. The per-platform error strings (APNs credentials not configured …, Huawei Push Kit not configured, Web Push not configured) scope a missing credential to those devices only — they do not fail the whole send.
How does a VAPID rotation affect existing Web Push registrations?
A VAPID rotation invalidates every existing browser subscription — the public key the page subscribed against no longer matches, so every Web Push target starts failing until each browser callspushManager.subscribe against the new public key and re-registers the resulting subscription as a device token. Native APNs/FCM/HMS targets in the same send are unaffected. Rotate only when the private key is compromised; when you do rotate, treat every existing Web registration as gone and re-subscribe, because no in-place re-subscribe happens automatically. The per-device error on GET /api/v1/push/notifications flags the failing web target, and the keys are provisioned as DEVOTEL_VAPID_PUBLIC_KEY / DEVOTEL_VAPID_PRIVATE_KEY / DEVOTEL_VAPID_SUBJECT on the Web Push setup section.
Why is my wildcard push broadcast refused before anything dispatches?
Two deterministic pre-send gates abort a whole-org broadcast (user_ids: ["*"]) before APNs/FCM/HMS/Web Push are ever touched. BROADCAST_TOO_LARGE (422) means your deliverable audience — enabled device tokens minus push/all suppression opt-outs — exceeds the 100,000-device fan-out ceiling: split into explicit user_ids (the platform never truncates silently). CHANNEL_RATE_LIMITED (429) means the tenant’s per-channel velocity gate tripped on a sliding one-minute window: raises are owner-only via PUT /api/v1/settings/compliance/channel-rate-overrides (whole-map replace) for the dedicated push envelope, or PUT /api/v1/settings/compliance/fraud-caps for the fraud floor. Read error.details.channel (push, not sms) and request_id before either escalation. The full recovery runbook is on Push broadcast pre-send gates.
Why is my send refusing with CHANNEL_REQUIRED, CONVERSATION_MISMATCH, or an inbox-only label?
The deterministic channel- and conversation-naming pre-send refusals fire before the message reaches any provider. CHANNEL_REQUIRED (422) means the body had no channel, an empty string, or the literal "auto" — pass a valid enum such as sms, whatsapp, rcs, email, or voice explicitly (the unified send path no longer guesses, because guessing once billed callers for PSTN voice on an SMS send). CHANNEL_COMING_SOON (422) means the channel name is valid but points at a regional/beta channel not yet enabled for your tenant — open a support ticket to gate it in. CHANNEL_NOT_REPLYABLE (422) means the reply flow named the inbox-only triage labels agent or video — pick a real outbound channel from the composer. CONVERSATION_NOT_FOUND (404) means a supplied optional conversation_id resolves to nothing on your tenant; CONVERSATION_MISMATCH (422) means the id resolves, but it does not address this recipient and channel — drop the field to auto-resolve, or supply the conversation id whose recipient and channel match. The channel and conversation pre-send refusals runbook walks all five with worked envelope samples.
Sandbox & Testing
What’s the difference between a sandbox send and a live send, and how do I switch?
The key decides the environment — create a test key (Settings → API Keys → Create Key, prefixdv_test_sk_) and call the same send endpoints with it instead of your live dv_live_sk_. A sandbox send accepts and validates like a live call, then terminates at the test_sent status with delivery simulated: no carrier is ever contacted, no wallet balance is deducted, and nothing touches your carrier-side sender reputation or live compliance posture (10DLC registration checks, warming caps, sender-pool routing all stay a live-environment concern). The message.sent webhook fires with status: "test_sent" and metadata.test_mode: true, so you can run your full integration — endpoint contracts, webhook handling, state machine — end to end before a live message ever leaves. The status decoder on Troubleshooting classifies test_sent as expected in sandbox, and the mechanics are defined in the glossary under Sandbox / Test Mode.
Do sandbox messages cost credits?
No. Sandbox sends are free — nothing is deducted from your wallet, so you can test as long as you need without spending a credit. This is the “test the platform for free” the Billing section mentions; it refers to the sandbox environment, not to a pricing concession.Can I clear a sandbox attempt from history?
Yes. Atest_sent row is a normal message record, and it can be removed the same way any message row is: DELETE /api/v1/messages/:id. Use it when you want the Delivery Log to carry only live traffic — row count and request volume are the only differences between environments. (GDPR-grade PII purge, if your sandbox body contained real recipient data, is POST /api/v1/messages/:id/redact — owner/admin only.)
Operator Messaging Questions
Why is my SMS marked sent, but it never becomes delivered?
sent is a wire-intermediate state — it means Orbit submitted to the carrier, not that the carrier confirmed delivery. When no delivery receipt comes back inside the per-channel grace window (30 minutes on SMPP-backed channels), a scheduler promotes the row to submitted_no_receipt; a genuine delivered or failure DLR can still land afterward and overwrite it. Only delivered is carrier-confirmed. See the Delivery lifecycle concept and the submitted-no-receipt troubleshooting page.
Why does a message show failed when my recipient says it arrived?
Some carriers emit a delivery receipt and then a correction minutes later (common on some Indian and Brazilian routes), and Orbit honors the correction — a row can movedelivered → undelivered or delivered → failed. Apply webhook updates idempotently by message id instead of ignoring later transitions. The failure causes and per-status decoders are on Message undelivered or failed.
What’s the difference between failed, undelivered, rejected, and expired?
rejected is a platform-side pre-send refusal (the carrier was never attempted); undelivered means the carrier tried the handset and could not reach it; failed is a dispatch-time or classified carrier failure; expired means a DLR arrived after the receipt window closed, so the outcome is unknowable and the row is closed. The full table with per-status fixes is on Message undelivered or failed.
What’s the difference between a notify_id cascade receipt and the channel-specific DLR events?
They’re two different completion semantics for one send. The channel-specific DLR events (message.delivered, message.failed, and the rest of the family) fire per physical message hop — on a cascade send, each fired hop is a first-class message row with its own id and channel, and a late per-channel DLR just updates that one hop’s row. A notify_id receipt is the cross-channel aggregate: every hop of a POST /api/v1/notify send stamps the same notify_id, and GET /api/v1/notify/:notifyId reconstructs the whole chain and answers “did any hop reach this recipient” with one call, reporting whether pending fallback legs still exist and applying the platform’s channel-aware delivered predicate, so the result is a semantic verdict, not raw status strings glued together. Read the cascade verdict from the notify receipt; treat per-hop message.* events as drill-down into each leg’s detail — the two never compete, because the aggregate is a view over the same rows those events update. 404 NOTIFY_NOT_FOUND means the id isn’t stamped on any message row in your tenant. The cascade model is on Cascade failover policy, the waterfall semantics on Notify cascade cost model, and the event family in Webhook Events.
Why did my send return 422 TEMPLATE_NOT_APPROVED when the look-up said the template was approved?
The gate fires on the exact rendered variant, not the template headline you remember approving. Three causes cover nearly every unexpected 422 here:
- A sibling variant is still in review. You approved the WhatsApp variant, but the send is rolling to the RCS variant (or vice versa) whose provider-side status is still
pending. The gate blocks whenever the resolved variant’s status is anything but approved. - The approval never landed back in Orbit. The provider approved it today and your cached template record still carries the old
status— the gate keys on the stored status, so the send refuses until a fresh read re-syncs it. - A campaign targets a per-channel variant that is approved on one channel but not the rendered one. The per-channel variant renderer picks the variant; if that variant is still in review, the whole send 422s before dispatch.
GET /api/v1/messaging/templates/:id(per channel when the template is multi-channel) and walk each variant you might render — confirm the variant that will actually render carriesapproval.status: "approved".- Wait for the approval to land (provider side), then retry the send. Identical replays fail identically while the status is
pending/rejected/disabled. - Do not resubmit the template early to hurry it — each re-submit re-enters provider review from scratch, so you end up in line again instead of one approval ahead.
INVALID_TEMPLATE is a separate gate: it fires when the send’s rendered shape can’t resolve (variable placeholders that can’t be substituted) — the fix is on the variable-resolution side, not approval. And NO_VARIANT_FOR_CHANNEL is the sibling variant-shape gate (the template exists but carries no variant for the rendered channel): the fix is authoring the missing variant / extending fallback_chain, covered on Troubleshooting: template variant missing.
How do queued, undelivered, and failed fit into one lifecycle ladder?
Read them as one funnel, not three unrelated statuses — and the failure-queue fallback as the legitimate end of the failing leg. queued is a platform-side hold with full dashboard and API visibility (direction=outbound&status=queued on the Delivery Log, GET /api/v1/messages?status=queued); diagnose the hold with the enqueued-empty-states checklist. undelivered is the carrier-tried outcome — and on a cascaded send it is the state that triggers the fallback hop (a brand-new send on the next chain channel under the same message_group_id, per the cascade failover policy). failed is the exhausted-retries outcome, and the row carries the classified error code plus the carrier’s raw error_code/error_message to explain the stop. The undelivered-versus-failed split is the diagnostic that decides whether the recipient or your dispatch needs the fix; the per-cause decoder is on Message undelivered or failed, and the full ladder mapping is in the Message status transition rules concept. If the row is closing late with no visible failure at all, check DLR retry and late-receipt recovery.
Why do my push notifications fail per-device with Unregistered / 410 / UNREGISTERED on some targets — and how do I retire those tokens?
A push send returns 201 and grades every device individually, so an expired token shows up as one status: "failed" entry in the notifications[] list, not as a top-level error. The provider-code string in that row (Unregistered, BadDeviceToken, UNREGISTERED, subscription is gone, 410/404/403) tells you whether the failure is permanent. When it is, the platform deletes the dead device-token row inline, so the next send skips it; the client must re-register a fresh token through the SDK before it can be targeted again. If a capability flag (sound, mutable_content, interruption_level, badge) ships but the device stays quiet or cropped, it is a device-side settings or entitlement mismatch, not a provider fault — preflight the flag. A runbook for all five failure classes (APNs 410, FCM UNREGISTERED, capability-flag drift, user_ids ownership collisions, and VAPID rotation) is on Push token expiry troubleshooting, and the per-device response contract is on the Push channel page.
Why did my WhatsApp template get rejected?
Rejection is either a policy violation (a category mismatch, or promotional content in a utility/authentication template) or a formatting error (unpaired*bold* markers, wrong language selected, sample variables with no context). Read metadata.rejection_reason on the template record, fix the content, and resubmit — the rejection categories map 1:1 to the WhatsApp content policy. The full fix workflow per status is on the WhatsApp template troubleshooting page.
What does WHATSAPP_SESSION_EXPIRED mean on a failed WhatsApp send?
Meta’s 24-hour customer-service window for that recipient has closed, and WhatsApp only accepts template messages outside it — your free-form send was refused. Waiting does not re-open the window; only a recipient reply does, so do not retry the same free-form body. Gate the composer on GET /api/v1/messages/whatsapp/window-status, re-send the content as an approved template, and treat the recipient’s reply as the signal to resume free-form. The cause table, the pre-flight endpoint, and the what-not-to-do list are on the WhatsApp session expired runbook, and the window model narrated end to end in the 24-hour window guide.
Why did my WhatsApp send fail with 429 WHATSAPP_TIER_LIMIT_EXCEEDED when I’m nowhere near any per-key rate limit?
That 429 is Meta’s daily-recipient tier, not Orbit’s per-second throughput throttle. Meta caps how many unique recipients a WhatsApp Business account can reach per day, starting low on new accounts and raising the tier as quality stays high — so a burst that passes every per-second and per-key limit still fails once the day’s unique-recipient count crosses your tier. It resets on a daily-recipient clock, so back off for hours, split large audiences across days, and keep quality high to get tiered up. The limiter families are compared in the rate-limit and cooldown taxonomy (Family 2), and Troubleshooting: rate limits has the per-code tables.
Why did my WhatsApp template get paused (Meta 132016 / auto-paused)?
Meta pauses a template when its quality metrics drop — recipients are blocking or reporting it — and sends against it returnWHATSAPP_TEMPLATE_PAUSED. Orbit also auto-pauses a template locally when Meta reports a RED quality score on it and you opted that template into auto-pause, which fires the whatsapp.template.auto_paused webhook event. A drop in overall account quality surfaces separately as WHATSAPP_ACCOUNT_QUALITY_LOW: sends still work, but Meta reduces your messaging tier if it persists. Check the template status on the template record and account-level posture on GET /api/v1/brand-identity/status; the fix workflow is on Troubleshooting: WhatsApp templates.
Why is my whole WhatsApp Business Account returning WHATSAPP_ACCOUNT_LOCKED (Meta 131031)?
Meta locked the account outright after a sustained policy issue — a hard stop on every send, unlike the quality-drift WHATSAPP_ACCOUNT_QUALITY_LOW (sends still work) or a credential failure (WHATSAPP_CONNECTION_INVALID). No in-platform toggle lifts a lock: recovery is a Meta Business Support appeal plus fixing the violation, so first review recent templates against the WhatsApp content policy and the GET /api/v1/brand-identity/status quality surfaces, then appeal in Meta Business Manager. Do not disconnect and re-connect expecting a reset — the lock follows the account. The full recovery flow, including what not to do, is on Troubleshoot the WhatsApp connection (Account lock section).
Why is my 10DLC campaign stuck in review?
Campaign review typically takes 1–5 business days, and the top-levelstatus is only the CSP-level decision — an individual carrier can still be in REVIEW even after the CSP approves. Poll GET /api/v1/compliance/10dlc/campaigns/:id/status and read the mnoStatuses per-carrier map before assuming the campaign is fully live. The status lifecycle is in the 10DLC registration guide.
Why was my 10DLC campaign re-approved but sends still return 422?
A re-approval confirms the registry verdict, not that your traffic qualifies yet. Read the mnoStatuses per-carrier map on GET /api/v1/compliance/10dlc/campaigns/:id/status again — a 422 after the top-level status flips to APPROVED usually means an individual carrier (e.g. 10017 T-Mobile, 10035 AT&T, 10095 Verizon) is still in REVIEW, or that a new vetting pass reset your brand vetting score and moved your qualified throughput tier back down, so the declared volume no longer fits the tier’s daily cap. Re-run POST /api/v1/compliance/10dlc/preflight before resubmitting to catch the deterministic rejection patterns up front. The per-carrier status map, the throughput-tier guide, and the preflight linter flow are in the 10DLC registration guide.
How do I see my overall brand-trust posture, not just 10DLC?
GET /api/v1/brand-identity/status returns a unified trust score (0–100) across 10DLC, toll-free, WhatsApp Business, RCS, branded calling (RCD/CNAM), and per-number regulatory KYC — plus a prioritized nextActions list pointing at the dashboard surface where each gap is fixed. The response is fail-soft: a degraded source subsystem only drops that one channel out of the denominator rather than failing the whole read. The model and the field-by-field response breakdown are on the Brand identity & trust score concept page; the endpoint contract is on the Brand Identity API reference.
What is the trust score on the Brand Identity page, and how do I raise it?
The trust score is a cross-channel rollup — the share of your regulated surfaces that carry an approved registration, weighted equally across 10DLC, toll-free verification, WhatsApp Business, RCS, branded calling (RCD/CNAM), and per-number regulatory KYC — computed asverified / applicable channels on a 0–100 scale. It differs from brand vetting: vetting is one registry’s judgement of one 10DLC brand, while the trust score reports everything you have registered everywhere, with a prioritized nextActions list pointing at each gap. Raising it means clearing each channel’s registration — and on 10DLC specifically, a registered brand also unlocks higher carrier throughput. The endpoint contract is in the Brand Identity API reference, the model on the Brand identity & trust score concept page, and the throughput upside in the 10DLC registration guide.
Why don’t Messenger postbacks fire webhook events?
Button taps, m.me referral links, and ad clicks are received and processed by Orbit but are not dispatched to your webhook, so an automation waiting for amessage.received event on them will never trigger. Outbound lifecycle events (message.sent, message.delivered, message.read, message.failed) do fire normally. The exact contract is on the Messenger channel page.
How do Messenger personas work in Orbit?
Personas let your agents reply on the same connected Page under named identities (for example, “Sarah, Sales”) instead of the Page’s name. Add them from Settings → Channels → Messenger after connecting the Page, and manage them throughGET/POST /api/v1/settings/channels/messenger/personas (see the settings endpoints). The connect and onboarding steps are on the Messenger channel page.
Why does my SMTP AUTH fail with 535?
The relay returns a single 535 reply for every auth failure, so a wrong username and a revoked key look identical on the wire. Two checks have to pass: the username must be exactly the literal string apikey, and the password must be a full, active tenant API key — the same key that gates the HTTP send endpoint. Re-issue or rotate the key under Settings → API Keys if the first check passes and the failure persists. The full connection matrix is on the SMTP relay page.
Why did my fax fail — and am I billed for it?
A failed fax (busy, no-answer, poor line quality) is never billed — fax is on-success-charge, so your wallet only deducts when the carrier confirms a successful transmission. Outcomes arrive on the channel-agnosticmessage.delivered / message.failed webhook events with channel: "fax". See the Fax channel page.
Why did my API return DECRYPTION_FAILED?
An encrypted field or payload (AES-256-GCM envelope, enc:v1) failed to decrypt on read. The usual causes: a key rotation left stale ciphertext behind, an authTag mismatch means the ciphertext was tampered with, or the master key is missing from the environment. This is usually not retry-safe — re-throwing the same ciphertext returns the same error. Open a support ticket with the request-id (support@devotel.io) so the data can be re-encrypted onto the current key, and avoid immediately retrying the same encrypted value. The full runbook — including the BYOK-enforced 409 BYOK_KEY_UNAVAILABLE sibling and its cause table — is on BYOK key unavailable / decryption failed, and the key lifecycle it reads is on Customer-Managed Keys (BYOK). The write-side failure (ENCRYPTION_FAILED — IV or authTag generation failed) and this read-side code are both listed in the Error Code Reference.
Why did my email test-send fail on the shared test-mode account?
EMAIL_TEST_RECIPIENT_NOT_VERIFIED means you sent email on the shared test path to an address your tenant hasn’t explicitly OTP-verified. Shared test mail only goes to recipients you verified with a one-time code — a gate that mirrors the WhatsApp test recipient list. Verify the recipient with the OTP flow and re-send; move to live keys when you need unrestricted recipients. The gate code is in the Error Code Reference, and sandbox mechanics are covered in the Sandbox & Testing section of this page.
Why did my send return 422 SENDER_POOL_EMPTY?
The sender pool resolved fine but its sender_dids array has no members — you routed traffic through a pool with no senders in it. Add at least one DID, short code, or alphanumeric sender ID to the pool before sending. The full fix walkthrough is on Fix an empty sender pool; the sender pools guide and sender resolution concept cover pool mechanics.
Why does a US recipient see a phone number instead of my alphanumeric sender ID?
US and Canada carriers reject the shared alphanumeric sender, so the implicit default for a+1 recipient swaps to a platform phone number. Pass an explicit from or a sender_pool_id to pin the sender identity. The fallback chain and per-corridor rules are on sender resolution.
Why does my Kakao send return 422 VALIDATION_ERROR?
KakaoTalk Biz Message has two message types with different required content, and the 422 tells you which gate you missed. Alimtalk (the default kakao_message_type) is template-only — it rejects any send without an approved template_name (message_template on campaigns) — while Friendtalk requires a non-empty free-form body (with an optional media_url image). Check the metadata.kakao_message_type on the request: if you meant Friendtalk, set it explicitly (the default is alimtalk); if you meant Alimtalk, get the template approved through your Kakao reseller and pass its code — on POST /api/v1/messages/kakao the direct send carries Friendtalk only, and Alimtalk goes through the Campaigns API with channel: "kakao". The cause-and-fix table is on the KakaoTalk channel page.
Why does my LINE webhook never fire?
Orbit receives LINE inbound events only after you point LINE at its ingest route — the URL is set in the LINE Developers Console, outside Orbit. Adding a webhook endpoint under Settings → Webhooks subscribes your receiver, but nothing reaches it until the channel itself delivers inbound. The fix is step 4 of the LINE onboarding flow: after pasting your Channel access token + secret under Channels → LINE → Connect, open the channel’s Messaging API tab in the LINE Developers Console, set the Webhook URL tohttps://api.orbit.devotel.io/api/v1/webhooks/inbound/line, and enable Use webhook. All tenants share that single URL — Orbit resolves the receiving tenant from the LINE channel id (destination) and verifies the signature against your stored channel secret before forwarding. If events still don’t arrive, re-check the console toggle and confirm the channel access token hasn’t rotated (LINE_INVALID_TOKEN at connect; MESSAGE_SEND_FAILED on the send path).
Why is my WeChat or Zalo send returning CHANNEL_NOT_CONFIGURED?
The 503 means no credential was connected for your organization and no cluster-wide platform default is set. Check Settings → Channels → WeChat (or Settings → Channels → Zalo): if the credential field is empty, the fix is to connect your own per-organization credential — a WeChat Official Account access token for WeChat, a Zalo OA token for Zalo (per an operator-level platform default, per-org credentials take precedence when both exist). Paste the token there and re-send; both channels store it encrypted and never echo it back through the API. The per-channel pages walk the credential object end to end — WeChat and Zalo. (KakaoTalk gates the same way when its three keys are missing — see KakaoTalk configuration.)
Why did my email send get rejected with a 403 “domain not verified”, and which DNS record do I fix?
The provider refused because DNS verification behind your sending domain regressed — verification is not a one-time check, and the platform re-validates the SPF, DKIM, and DMARC records on a daily health check. When a record drifts (a TTL expires into a changed answer, someone edits the zone at your registrar, or a key rotates), the domain slides back to unverified: the DNS status widget on the Email dashboard flips one or more per-record traffic-light chips (SPF, DKIM, DMARC) yellow or red, a bell notification flags the regression, and sends from that domain start failing at the provider with a 403 “domain is not verified” rejection while the Delivery Log row stays on your side. Read the chip that went yellow or red to learn which record drifted, fix that record at your DNS host to match the expected value, then re-verify on demand — the interactive re-check re-reads the record and re-registers the domain with the provider, which is what clears the 403, so expect sends to resume only after the re-verify passes. The full recovery loop — per-record diagnosis, checking the history before you edit, the on-demand re-verify, warm-up posture while the domain is down, and resuming sends — is on Troubleshooting: email DNS verification drift and recovery. If the rejects are recipient-side (bounces or spam complaints after the provider accepted the send), work the email bounces and spam complaints page instead.Can I route inbound LINE/Kakao/WeChat/Zalo into the same inbox?
Yes. All four APAC channels arrive on the standardmessage.received webhook with their own channel value (line / kakao / wechat / zalo) on the normalized inbound envelope, so one webhook endpoint subscribed to message.received (or ["*"]) receives every one of them. In the dashboard, inbound APAC traffic folds into the same team inbox your other channels use — see Inbox setup — and the APAC channels onboarding guide closes with the inbox path its fold-in notes imply. The sender-resolution model behind “which sender answered” is on Sender resolution. The one inbound nuance is LINE: follows, postbacks, and reads are verified and acknowledged on the ingest route but never forwarded to your webhook — if your flow must react to a button tap, react inside Orbit (flows, journeys), not in your webhook receiver (see LINE channel page).
Why does my Lookup response say coming_soon on SIM swap, Identity Match, or Live Activity?
coming_soon is a per-field status on the Lookup response, not a lookup failure — it means the field is in the public catalog but the upstream provider feed behind it isn’t contracted yet, and the dip was skipped for just that field. Read the dataPackages map entry by entry: each one carries a discriminated status (available, coming_soon, not_implemented, or error) with a reason string and an intended provider. The carrier-network fields — sim_swap and identity_match (CAMARA SIM Swap and KYC Match over GSMA Open Gateway) — return coming_soon until a CAMARA operator is wired for your deployment; live_activity and the CNAM/caller-name fields return it when their upstream returned no usable signal or isn’t configured. Replay won’t change the verdict until the vendor work lands, and the verdict is deliberately fail-closed — a missing operator answer never falls through as a false match. The per-field status matrix is on the Number Lookup page, and the runbook for the whole-request failure case is on Troubleshoot number lookup failures.
What’s the difference between LOOKUP_FAILED and a per-field coming_soon?
They’re two different layers of the same API. LOOKUP_FAILED is a request-level 502 — the whole lookup could not answer because both the primary HLR provider and the fallback were unreachable (or no provider is configured), so the route fails before any per-field verdicts are produced; retry with backoff and follow the number-lookup runbook. A 200 with one or more coming_soon entries in dataPackages is the opposite case — the base dip succeeded, and per-field isolation judges each requested field independently, so a field whose upstream isn’t contracted reports coming_soon or error explicitly while every other field still answers. Build your integration on the per-entry status; never treat the presence of the map as a guarantee that every field answered.
Why does identity_match return not_available verdicts even when I passed the attributes?
not_available is a per-attribute result on the identity_match field, not an error — it means the mobile operator holds no value for that one attribute in its KYC records, so it can’t assert a match either way. The field matches the identity attributes you supply (identity_name, identity_given_name, identity_family_name, identity_birthdate, identity_email, identity_id_document) against the operator’s own records and returns a data.matches map with a true / false / not_available verdict per attribute plus an aggregate data.overall_match (null when any verdict is indeterminate) — the operator’s underlying PII is never returned. Treat not_available as “unknown”, not as a failed check; a gate that hard-fails on it would block real users with thin operator records. Selecting the field with no attributes returns a per-field error (identity_attributes_required) instead, and a coming_soon entry until a CAMARA operator is wired. The attribute list and verdict semantics are on the Number Lookup page.
Inbox & Operator
What’s the difference between a conversation, a ticket, and a thread?
A conversation is the live message record the inbox keeps per customer across channels — SMS, WhatsApp, email, web chat, voice calls — with inbound and outbound messages ordered into one thread. A ticket is the unit of tracked work an operator creates (from the dashboard or over the API): it survives a thread’s idle close, carries a subject, priority, assignee, and anopen → pending → resolved / wont_fix lifecycle with an audit trail. Use a conversation while the unit of work is a live exchange; move to a ticket when the unit is a case your team owns until resolution. The everyday-operator model is in Conversation operators; the full thread/ticket split — including when a ticket is the right move — is in Operate the Inbox Tickets queue. If by “thread” you mean a customer asking the same question: a thread is just a conversation’s message history, not a separate object.
How do keyword rules dispatch an inbound message?
A keyword rule is a per-tenant row evaluated against every inbound (MO) message body on one channel (sms, whatsapp, rcs, viber, email), and at most one rule fires per message — rules evaluate in creation order with active = true as the gate. The match types (exact, contains, starts_with, regex) choose the comparator, and the action (reply, opt-out, opt-in, forward_agent, trigger_flow) decides what a match does: send an auto-reply, write a consent change, hand the turn to a deployed AI agent, or start a flow. The full matching model — legacy alias normalization, precedence, and the consent writes — is in the keyword-rules model, and the form-first setup plus API walkthroughs are in Auto-reply rules with Keyword rules worked examples.
How do I share a canned or quick reply across my whole team?
Canned responses are one org-wide library: an owner or admin creates each entry (a shortcut, a title, a body, an optional channel scope) under Settings → Canned responses, and every agent then inserts it in the inbox composer by typing the shortcut. For sequences that are more than a single reply — reply plus assign, tag, status change, follow-up — use Macros instead; a macro can carry either personal or team-shared scope. The library model and the scope split are in Macros and canned responses.A macro runs — so what exactly is a macro versus a canned response?
A macro is the canned-response body plus an ordered chain of side-effect steps — assign, tag, status or priority change, snooze, follow-up task, or an AI-agent escalation — run as one confirmed action, with variable placeholders that resolve against the live conversation at run time. The canned-responses/ macros model has the full split, and the Macros and canned responses guide is the authoring walkthrough.When does a conversation auto-close, and can I change that?
Auto-close is a tenant-owned, opt-in control — it ships off. When an owner or admin enables it (Inbox → Settings → Auto-close stale conversations), a background sweep runs every 10 minutes and closes conversations with no new customer message for the configured idle window: a whole number from 1 to 90 days (default 7), applied to the statuses you allow (open, pending, or both). Every auto-closed row is markedclosed_reason: "auto_stale", fires the same conversation.closed webhook a manual close fires, and writes the audit event conversation.auto_closed_stale — so reporting splits platform-closed volume from agent closes. Configuration and the sweep’s exact semantics are on Auto-close stale conversations. Tickets are unaffected by the conversation auto-close sweep; each closed ticket carries its own resolved-reason lifecycle.
When does double opt-in drain my pending-consent pool — and why aren’t my first-party opt-ins being marked verified?
A confirm-by-reply handshake holds a contact at pending until the recipient replies YES; first-party opt-ins that were begun but never confirmed never becomeopted_in — that is the “why aren’t they verified” answer, and the same mechanic drains your un-approved pool. The flow has three states you can read: pending (a confirm-request was recorded and the reply is still awaited), opted_in (the affirmative reply converted the pair into a confirmed grant), and the pool of contacts whose handshake was never even begun. Only sms and email can auto-deliver the confirm-request (an SMS reply or the double-opt-in confirmation email — the email-side variant — where the recipient confirms from the link); every other channel stays record-only, so on those channels the pending pool only drains when the recipient replies through your stack.
Work the drain in two reads, and do not confuse them: the contact’s consent timeline (per-contact, per-channel, the two timestamps handshake evidence is built on — prompt then reply) tells you how far each contact got; the suppression table is a separate list and is not a substitute for re-consent — an opted-out contact needs an explicit re-consent, the suppression layer does not feed the pending pool. Start or confirm handshakes on /compliance/double-opt-in, use Opt-in disclosure templates for the copy the confirm-request uses, and read pool state on the contact’s consent timeline rather than the suppression list. This is a tenant-owned control — nothing on Orbit starts a handshake on its own.
What does “Reconnect Microsoft 365” on Settings → Presence mean?
A presence tile rides an org-level OAuth grant, and Reconnect Microsoft 365 (or Reconnect required on any provider tile) means that grant no longer works — Microsoft revoked or expired the refresh token (a password change, admin consent removal, or conditional-access policy change all revoke it), or the admin consent step was never completed. While the badge is up, presence sync from Teams and Microsoft 365 Calendar has stopped: calls ring as if the source were off, using whatever status you set yourself in the dashboard header. The fix is a tenant-owned org-admin action: open Settings → Integrations, reconnect the Microsoft 365 integration, and complete the Microsoft consent screen — the tile clears within about a minute for every member at once, no per-user re-toggle needed. The full badge-to-cause-to-fix map is on Presence federation degraded states; setup is in the presence federation guide.How do I assign a conversation to a specific agent or squad?
Three layers, from manual to fully automatic:- Manual — any teammate can open the conversation and pick an assignee from the assign-control roster (eligible teammates only).
- Routing rules — owner/admin-authored
if conditions → then assignrules evaluated in priority order on every inbound conversation, assignable to a user, a team, a round-robin pool, a skill-matched agent, or an AI agent. Configure and dry-run them under Inbox → Settings → Routing; see Routing rules. - Digital queues — when what you want is “the longest-idle eligible agent with the right skills,” not a fixed teammate: a queue maps a channel to a skill set and SLA target, and the inbox assigns accordingly. The model is on Digital queues — skill-based ACD.
What is a supervisor takeover, and which codes come back when I seize control?
A takeover is the supervisor intervention that moves reply control of a conversation from the assigned agent (or AI agent) to a supervisor — the caller stops waiting on one side of the desk and picks the conversation up themselves. Voice and digital (inbox) takeovers are separate surfaces:POST /api/v1/voice/supervisor/takeovers for a live call, and the POST /api/v1/conversations/:id/takeover/{start,send-reply,end} set for a digital conversation. The refusal codes you meet fall in two groups — state you can resolve on your side, and platform failures you retry once:
TAKEOVER_ALREADY_ACTIVE(409) — a takeover is already open on the conversation;detailsnames thetakeover_idandsupervisor_idholding it. Read the open takeover, advance or cancel it, then start again.TAKEOVER_FORBIDDEN(403) — the caller neither owns the active takeover nor holds an owner/admin role to recover it. The right response is the takeover owner ending their session, or an owner/admin releasing it — not a credential retry.NO_ACTIVE_TAKEOVER(409) — a send-reply or release went to a conversation with no live takeover; whatever took control before is gone, so the action has no target.TAKEOVER_NOT_ALLOWED(422) — the conversation is in a terminal state (closed,archived,snoozed); there is nothing live left to intercept.DIGITAL_TAKEOVER_START_FAILED,DIGITAL_TAKEOVER_REPLY_FAILED,DIGITAL_TAKEOVER_END_FAILED,DIGITAL_TAKEOVER_GET_FAILED(500) — the digital takeover start, reply, release, or read failed at the platform; retry once with the request id, and escalate if the same conversation keeps refusing.- Voice-side
SUPERVISOR_TAKEOVER_*500s — the same pattern on the voice bridge; retry once, then report with the call id.
My ticket attachment upload returned 503 ATTACHMENT_STORAGE_UNAVAILABLE — is the file too big or the wrong type?
Neither — that code fires after size and content intake checks pass, so the file is exonerated. It means the storage backend the attachment bytes land in timed out or returned a 5xx during the write. Re-upload the same file with a short backoff; a transient incident clears within the first couple of retries. If the same file keeps failing at backoff, escalate with the meta.request_id from the failed response and the sha256 checksum of the bytes — support pins the exact attempt in the storage log instead of guessing from the filename. Do not strip the message off the thread or rename/re-encode the file: the fault is scoped to storage, and a renamed variant breaks the checksum you will send. The full cause split and retry matrix are on the attachment storage unavailable runbook.
USSD
What is USSD and when do I reach for it over SMS?
USSD (Unstructured Supplementary Service Data) is the menu-over-dial-code channel: the subscriber dials a short code (for example*384*1#), the network opens a synchronous session, and every screen is one request/response round trip. Reach for it when your audience is on feature phones or 2G with no data plan — the de-facto reach channel in West-Africa and other emerging markets (Africa’s Talking / Infobip parity). Model it as a menu tree of screens: the USSD channel page has the endpoint map, and the USSD session model explains the stateless engine behind it.
Why did my menu save return 422?
PUT /api/v1/ussd/menu validates the tree before persisting it, and a 422 means one of three invariants failed: a broken root or option next reference (the pointer does not resolve to an existing node, or root isn’t among nodes), a duplicate node id, or a non-DTMF option key (keys must match ^[0-9#*]+$ — a handset only relays digits, #, and *). The error details name the failing reference so you can fix the pointer and re-submit; the save is fully rejected, so nothing partial is persisted. The full node/option table is on the menu shape reference.
Why does the callback never advance the session?
The callback URL is wrong, or your own handler (stacked on top of Orbit’s) is storing session state it doesn’t need. Orbit’s public callback isPOST /api/v1/ussd/callback/:tenantId — point the aggregator (Africa’s Talking, Infobip) at https://api.orbit.devotel.io/api/v1/ussd/callback/<your_tenant_id>, accepting JSON or form-encoded bodies. On every step the aggregator replays the full accumulated input (text: "1*2" on the third step after pressing 1 then 2), so your handler must re-derive the current screen from that accumulated text each time — never from stored session state. A subscriber who pressed 1 then 2 gets back one CON/END exchange per step:
END Invalid selection., and an unconfigured menu answers END Service is not available. — both close the session cleanly instead of leaving the subscriber hanging.
How do I test a menu before giving the aggregator a shortcode?
POST /api/v1/ussd/simulate runs the exact same pure function the live callback runs, so a simulated walk is a real regression test, not an approximation. Pass text (the accumulated *-joined input; empty on the initial dial) and optionally an inline menu to preview an unsaved tree — without an inline menu, your tenant’s saved menu is used. The response returns action (CON/END), message, node_id, and raw (the exact wire body — e.g. END Your balance is 1,250 NGN.). Walk the whole tree offline, then point the aggregator at the callback URL. See the simulate walk.
Do USSD sessions hit my webhooks?
No — there is noussd.* event type in the catalog (Webhook Events), so a subscription waiting on one will never fire. Each session completes over the callback POST + its synchronous CON/END text response, and telemetry lives on that callback axis — the aggregator’s own request logs, plus the sessionId/phoneNumber correlation it supplies. Any follow-up you trigger on the terminal screen (a confirmation send, a log record) should be keyed on sessionId so a carrier-level callback retry can’t double-fire it. The completion-signal model is on the USSD session model page.
Wallet passes
Why does my wallet pass update return 409 CONFLICT, and why does a duplicate issue return 200?
Two different edges of the same lifecycle. A voided pass is terminal — any update or re-void against it returns409 CONFLICT with a reason like already_voided, and the only recovery is to issue a replacement pass. A duplicate issue is the opposite move: POST /api/v1/wallet-passes/issue is idempotent, so a retried request with the same idempotency_key (or Idempotency-Key header) replays the first issued pass with replayed: true and 200 instead of minting a second pass. The full state machine (active accepts, voided rejects, expired renders expired in the wallet while the ledger stays active) and the recovery table are on the wallet pass lifecycle troubleshooting page; the channel guide’s common errors table lists the same codes.
Why does the holder’s phone show stale content after my wallet pass update succeeded?
generation is a pass-changed signal, not a version to diff — each accepted update increments it by exactly one, and reads fold the pass’s full event history at request time, so a fresh GET /api/v1/wallet-passes/:id always returns the exact current count while a cached read does not. When two clients issue or update the same pass, compare generation across polls on the same pass (never across issuers) and treat the highest observed value as canonical. To force a stale phone to refresh, re-issue once under the same idempotency_key — the replay folds to the pass’s current state — and re-derive the holder’s save link from the fresh GET; the platform rebuilds the Google save_url and Apple payload on every read, so the new link always points at the latest content. The wallet pass lifecycle troubleshooting page walks the drift and the cache-busting recovery.
Flows
Why does my flow execution stay at waiting?
waiting is not stuck — it means the run parked on a long delay node and resumes automatically at its scheduled time; the node shows as a running step in the trace while it waits. To un-park it early, call POST /api/v1/flows/executions/:id/resume with the delay node’s id and the run continues from the node wired after it. The per-node decoder (steps[].status — completed / running / failed / skipped / pending) is on Flow Executions → status values, and the full read of a problem run is on Troubleshooting: a flow execution that failed.
Why did one node inside the run fail while the others completed?
That is thecompleted_with_errors run state: the failing node’s error routed over its error edge (or fell through to the default edge), and the rest of the flow finished normally. Read the run’s error field — the convention is Last failed node <node_id>: <message>, so the node id prefix tells you where to look and the step’s recorded input shows the resolved values it actually received. One dashboard nuance: the status filter pills only split running/completed/failed, so filter by Failed and scan for the warning-toned Completed with errors badge. The fail-vs-route decision table is on Troubleshooting: a flow execution that failed.
Why did my flow’s send node reject with an E.164 error?
to must be a valid E.164 phone number is the send node’s recipient gate — the resolved to value was a malformed national number, or a {{variable}} that resolved empty. Open the failing step and read its recorded input to see exactly which value arrived, then fix the upstream mapping or the trigger data that feeds it; a retry alone fails the same way because the gate is deterministic. The cause row and fix are on Troubleshooting: a flow execution that failed.
Why doesn’t my template appear as a send-node body?
A reusable content template only serves channels it has variants authored for — the per-channel copy (body, media_urls, subject/body_html, WhatsApp components, RCS suggestions) lives on the template’s variants map keyed by channel. When a send asks for a channel with no variant, and the template’s fallback_chain finds no substitute, the render refuses with 422 NO_VARIANT_FOR_CHANNEL; a WhatsApp template that is draft, paused, or rejected on Meta’s side also won’t send. Author the missing variant on the template, then re-run. The variant-resolution walk and author-the-missing-variant fix are on Troubleshooting: template variant missing.
Can I test a flow without spending wallet?
Yes, two ways.POST /api/v1/flows/:id/simulate dry-runs a flow against an audience and projects the per-step drop-off funnel, channel mix, and predicted cost — without enrolling a contact or emitting a send; the builder’s Test/Forecast toolbar action runs the same projection. For an end-to-end rehearsal, run the flow with a dv_test_sk_ key (or the dashboard’s Test mode toggle): sends terminate at test_sent with delivery simulated, and no wallet balance is deducted. The test-mode mechanics are on Sandbox, test mode, and provisioning.
How do I debug an execution that loops or times out?
timeout means the run exceeded the per-flow wall-clock cap of 300 seconds; a runaway loop instead trips the step guard and fails with exceeded maximum of <N> steps — possible runaway flow. Both point at the flow definition, not a node: open the failing trace and read the steps array in execution order — the recorded per-step input shows where the path started cycling or which node burned the time. Wire the cycle’s exit through a Condition, or break the loop — then re-run. The trace-reading workflow is on Flow Executions, and the decode table for both messages is on Troubleshooting: a flow execution that failed.
Sender identity & routing
Which sender type should I register — 10DLC, toll-free, short code, or alphanumeric sender ID?
Pick the sender type by destination country and use case; the four sender forms differ on registration gate, throughput, cost, and two-way support. Orbit supplies the registration surfaces for every type — which posture you file, and which sender you send on, is a tenant-owned decision.
Selection rules by region:
- US (and effectively Canada): A2P SMS over a
+1destination requires either a registered 10DLC campaign or a verified toll-free sender — carriers reject alphanumeric sender IDs outright. The 10DLC registration guide walks brand + campaign filing, and the per-carrier throughput tiers are set by your Brand Vetting / Vetting Score. A rejected campaign returns per the 10DLC campaign rejection runbook. Toll-free alternative: file the TFV verification flagged by TFV — a blocked filing surfaces asTFV_REQUIRED, covered on the toll-free TFV runbook. - Sender-ID pre-registration markets (India TRAI DLT, Turkey BTK, Saudi CITC, UAE TRA, others): file the alphanumeric sender before sending; the country matrix is checked by the opt-in strict sender-ID gate — see Strict Sender / Strict Sender-ID and the compliance sender-ID FAQ. In those markets an alpha sender is the identifier customers recognize; in markets that don’t accept alpha, the platform falls back to a numeric sender.
- High-volume marketing in the US: a Short Code is the carrier-leased 5–6 digit sender with the highest sustained throughput — pick it when per-day volume outgrows the 10DLC throughput tier your vetting score qualifies for.
- One-way brand-name sends outside the US/CA: an alphanumeric Sender ID names your brand in the sender field — pick it for OTP and alert traffic where a reply path isn’t needed.
What is strict sender-ID mode and why do my sends now 422 SENDER_INVALID_FOR_DESTINATION?
Strict sender-ID mode is an opt-in tenant control that rejects a non-conforming alphanumeric sender pre-send instead of letting the carrier reject it later. Default is off (permissive): any alphanumeric sender is accepted, and when a destination-country rule would have flagged it, the send response carries a non-blocking sender_deliverability_advisory on the message metadata instead of an error. When you enable the mode (the sms_sender_id_strict key on your organization settings — flip it via the dashboard sender-ID settings or support), pre-send validation re-checks every outbound alphanumeric sender against the destination country’s format/prefix/length rules, and a violation returns 422 SENDER_INVALID_FOR_DESTINATION before any wallet deduction. So the post-toggle 422s are expected behavior: the rule set that used to hard-reject is being applied again because you asked for it.
That checks (per destination country) include:
- Length — most countries accept 2–11 characters; India mandates exactly 6 letters (TRAI DLT), so an 8-letter sender like
ORBIT123returnswrong_length_exactfor an IN destination. In permissive mode the same send is accepted with a warning advisory, and the carrier’s verdict is reflected in the failure reason. - Restricted prefixes — countries like Turkey (BTK), Saudi Arabia (CITC), UAE (TRA), and India (TRAI) reserve government/bank prefixes (
GOV,BANK,STC,MOH,SGK, …). A sender likeBANKNOWto a Turkish destination returnsrestricted_prefixin strict mode. - Character set — alphanumeric and internal spaces only; a leading/trailing space returns
leading_trailing_space, and non-Latin scripts are rejected. - Alpha-unsupported destination — US/Canada carriers reject alphanumeric senders outright (see the US-recipient question above), so a
+1destination under strict mode fails the request — send to US/CA recipients from a phone number or short code instead.
alpha_undeliverable_us_ca, alpha_unsupported_country, sender_format_nonconforming) map 1:1 to the strict-mode rejects and are all tenant-owned controls — Orbit does not mandate them (see Compliance posture). The response includes the violating country, sender, rule code, and the violating prefix as structured details. Example: a send from "MYBRAND NAME" to the US is accepted permissively with a critical advisory; a send from "BANKNOW" to Turkey returns 422 SENDER_INVALID_FOR_DESTINATION with matchedPrefix: "BANK".
Why did my send fail with 422 SENDER_NOT_OWNED, and what’s the fix flow?
SENDER_NOT_OWNED is an always-on (not strict-mode gated) numeric-sender ownership check: a from like +12125550198 that resolves to a different organization’s number is refused pre-send, because the Devotel softswitch trusts the originator field and recipients would attribute the message to that number’s true owner. The fix flow, whichever leg of resolution you came from:
- Send from a number your org owns — an active DID you purchased or ported in, or a number you verified through the caller-ID register → confirm journey (see Number identity & caller ID), passes the gate with no extra step.
- Or use a non-numeric identity your tenant owns — the platform default
(Devotel)sender, or an approved alphanumeric sender ID, skips the numeric ownership lookup entirely (subject to destination-country alpha rules; US/CA reject alpha outright, above). - If the code reads
503 SENDER_OWNERSHIP_CHECK_UNAVAILABLE, don’t change anything. That sibling means the ownership registry read was transiently unavailable and the gate failed closed — retry the same send after a short backoff instead of reworking your sender.
SENDER_POOL_EMPTY (422) is the sibling covered above — the routed pool’s sender_dids array has no members; it is unrelated to ownership. The resolution chain and fallback order are on sender resolution and the sender pools guide. For the full ownership-gate decode — the cause table, the verify flow, why the 503 sibling intentionally fails closed, and a ticket-ready error envelope — see the runbook Sender-ownership blocked.
What does “strict” versus “permissive” mean on this page, and throughout the docs?
Throughout the compliance and sender-identity docs, “strict” names a tenant-owned, opt-in gate — a control you enable because you prefer a pre-send reject over a carrier-side failure. “Permissive” names the platform default: accept the send, surface a warning advisory when a destination rule would have flagged. That phrasing pattern holds for sender-ID rules and for suppression/opt-out gating — see Compliance posture FAQ for the toggle map. In all cases the underlying error codes and their status (422) are enumerated in the Error Code Reference and the retry-safe fix path is on the send endpoint you used.When does a send go over an aggregator versus a direct route — and can I mix the two on purpose?
An aggregator route is a wholesale trunk with carrier-approved, high-volume connectivity to hundreds of destination operators — the path a send takes when Orbit’s Devotel wholesale softswitch terminates it. A direct route is a carrier you licensed yourself and connected as a BYO (bring-your-own) SMPP carrier. The choice is made per send, per destination, by the least-cost-routing (LCR) policy — never per message body or sender:- Sender resolution picks the identity (sender pool, explicit
from, or the fallback chain). - LCR upstream selection ranks the Devotel wholesale route against your connected BYO carriers for that destination — by the composite score of cost, delivery quality, bind health, and a sticky last-successful-route bonus.
- Delivery proceeds over the top-ranked route; route health and circuit breakers then meter it continuously.
trafficType: "cost" orders BYO carriers strictly by cost, the default quality mode ranks by the composite score, preferByo: true puts every active BYO carrier ahead of the Devotel wholesale entry, and candidateOrder pins an explicit list. Preview any policy before saving it with the dry-run route quote (Developer → SMPP → Dry-run route quote, or POST /api/v1/messaging/smpp/carriers/route-quote) — it names the winning route per destination E.164 without moving traffic. A send you expected to take your BYO carrier but that instead exits on the Devotel route is almost always a missing country scope on the carrier profile, or an unhealthy bind demoting it below the incumbent. The full scoring model, policy fields, and dashboard panels are on Least-cost routing (LCR) policy; the aggregator reach is on the SMS channel page.
How do I see every conversation and call with a customer in one place?
Open Interactions in the dashboard — it merges messaging conversations (SMS, WhatsApp, email, and the rest) with voice calls into one recency-ordered list you can filter by channel, interaction type, status, contact, or date range and export to CSV. The search is available to owner, admin, developer, and viewer roles; the CSV export is gated to owner/admin/developer with thecontacts:read scope. See the Interaction Search guide.
What do MMS_NANP_ONLY, NOT_SMS_CAPABLE, TFV_LINT_BLOCKED, and LOA_NOT_SIGNED mean?
Four sender/asset preflight gates, all refusing before anything queues or submits. MMS_NANP_ONLY (422) means the MMS payload named a recipient outside the North American Numbering Plan — drop the media and cascade to SMS for that recipient, and keep MMS on +1 destinations only. NOT_SMS_CAPABLE means the sender you picked has no sms capability for the destination country — it is in the platform’s permanent retry-suppression set, so fix the sender (pre-check with GET /api/v1/numbers/country-capabilities), never the retry. TFV_LINT_BLOCKED (422) means the pre-submit content lint refused your Toll-Free Verification filing — fix the sample messages named in error.details.lint, test them against POST /api/v1/numbers/tfv-lint, then re-submit. LOA_NOT_SIGNED (409) means a hosted-messaging order cannot submit to the carrier until you sign the LOA with POST /api/v1/numbers/hosted-messaging/:id/loa/sign. The per-code cause tables, a decision tree, and ticket-ready envelope samples are on the sender/asset preflight gates runbook.
Number Lifecycle
Why is a number’s capability set (voice, SMS, MMS, two-way) different per country — and where do I check before buying?
Number capabilities are per-country because the carriers that provision the country expose different line types and feature support: the UK may be rich in local voice-capable numbers but have zero SMS-capable mobiles; US SMS capability only exists on 10DLC-registered long codes; toll-free buckets exist in many countries with no two-way SMS at all. Orbit probes every country’s inventory separately per line type (mobile, local, toll-free) × capability (voice, SMS, two-way SMS) across every upstream carrier that serves it — so the capability matrix is a property of the destination country’s carrier stock, not a global flag you toggle. Check it before you search or buy: the dashboard’s Numbers → Buy a number picker pre-dims empty capability buckets per country offGET /api/v1/numbers/country-capabilities?country=<ISO> — call it yourself for the same reality check (mobile_sms, local_voice, toll_free, …, clamped at 100 per bucket; two_way_sms counts only numbers confirmed to send and receive). For regulated countries the same pre-check surfaces requires_registration, so a purchase isn’t refused at checkout for a compliance reason you could have known. The full count semantics and response shape are on Country capabilities; the regulatory leg is on Regulatory Preview.
Why didn’t my number purchase go through?
A failed purchase has four distinct shapes — work through them in order:402 INSUFFICIENT_BALANCE— the wallet pre-flight rejected the call before any carrier order. The response names both amounts (required_cents/available_centsin the errordetails), and a bulk purchase checks the wallet against the summed per-number cost. Top up and retry: Settings → Billing → Auto top-up prevents a repeat.- Inventory race — a
409 NUMBER_ALREADY_TAKENmeans another claim reached checkout between your search and purchase. The dedicated question below has the full retry semantics. - Compliance-pool eligibility — a regulated country (
requires_registration: trueonGET /numbers/available) rejects the purchase with422 COMPLIANCE_PROFILE_REQUIREDor422 COMPLIANCE_PROFILE_NOT_APPROVEDunless you pass an approved profile id ascompliance_profile_id. Check eligibility up front with Regulatory Preview. pending_compliancehold — the purchase succeeded but the number sits parked until an approved compliance profile is attached. Attach one withPOST /api/v1/numbers/:id/attach-compliance-profile; if the grace window lapses first, the auto-release sweep refunds the captured monthly cost to your wallet (see the question below).
number.purchased for the success signal and number.released_compliance_timeout for a compliance-deadline release (the full event catalog is in Webhook Events) — no purchase_blocked event exists, because failed purchase attempts are never charged and come back synchronously in the response above. The full checkout walkthrough is in Buy and provision numbers; pending gates are covered in Troubleshoot a pending number or Sender ID.
Why did my number purchase return 409 NUMBER_ALREADY_TAKEN?
That 409 is an inventory race, and it is deterministic — not a billing error, not a carrier failure, and not something you wait out. Two claims for the same DID raced: a different tenant, or two of your own concurrent purchase calls, both passed GET /api/v1/numbers/available, and checkout serialized so only the first claim won. The loser got this response before a carrier order was placed, so no charge was taken and the DID claim was never registered — which is what makes the retry below safe. (The same race can also surface as 409 NUMBER_NO_LONGER_AVAILABLE; either way, the number was claimed between your search and your buy.)
Retry by re-running the search, then re-buying:
- Call
GET /api/v1/numbers/availableagain — inventory moves by the minute, and a row you saw is not a reservation. - Pick a still-listed number from the fresh result.
- Call
POST /api/v1/numbers/buywith that number. The response returnsdebited_centsonly for the rows that succeeded; any row that lost the race comes back in thefailed[]array with the error code.
POST /api/v1/numbers/buy-bulk call returns 200 and reports each number in succeeded or failed[] — gate on succeeded.length, not the status code. Failed rows are never charged (debited_cents counts only succeeded rows), so re-submitting exactly the failed set in a new call is billing-safe. Do not replay the whole batch — re-send only the failed E.164s.
To stop racing at all, rehearse the two calls back-to-back at checkout time: fire GET /api/v1/numbers/available immediately before the buy call and accept one of the nearest results instead of a number you cached minutes ago. If a buy-bulk batch matters to you, re-run the pre-flight inside the same request window.
The destination runbook for the whole checkout-failure lane — inventory races, the quarantine nuance on your own recently-released numbers, and the post-checkout 502 NUMBER_PROVISIONING_FAILED rollback — is Troubleshoot number purchase failures; the endpoint contract itself is on Numbers overview.
Why did my bulk purchase come back with CARRIER_RATE_LIMITED rows?
A POST /api/v1/numbers/buy-bulk batch processes row by row. When one row hits a carrier-side rate limit (HTTP 429), Orbit stops pulling new rows — every remaining row comes back in failed[] marked with error.code = "CARRIER_RATE_LIMITED" without another carrier call, so a partially-completed batch is a normal, recoverable shape. The response is still 200 OK, so gate on succeeded.length, never the status code. Retry only the failed[] slice (exactly those E.164s) in a new call a few seconds later — failed rows are never charged, and re-running the whole batch re-attempts rows that already succeeded. If the same pattern recurs after a handful of spaced retries, escalate to support with the exact meta.request_id, the batch order_id, and the carrier from error.details.carrier when present. The full runbook is Troubleshoot CARRIER_RATE_LIMITED on bulk number purchase.
Why did my number suddenly disappear and my wallet get refunded?
That is the compliance-deadline auto-release sweep: a regulated-country purchase sits atpending_compliance until an approved compliance profile is attached, and if the grace window lapses first, Orbit auto-releases the number and refunds the captured monthly cost to your tenant wallet. The release fires a distinct number.released_compliance_timeout webhook event (see Webhook Events), separate from the operator-initiated number.released, so you can branch on a number lost to a missed deadline. The full state map and recovery path are on Troubleshoot a pending number or Sender ID and the Number status map.
My number is pending_compliance — how do I unblock it?
A number at pending_compliance is blocked from sending or receiving until an approved compliance profile is attached to it — attach one with POST /api/v1/numbers/:id/attach-compliance-profile and the carrier webhook flips it to active. If the deadline passes first, Orbit auto-releases the number (see the auto-release question above). Two write-time gates keep the profile from ever reaching approvable state: 409 COMPLIANCE_PROFILE_LOCKED when you edit or re-submit a profile that is already in review, and 422 COMPLIANCE_PROFILE_INCOMPLETE when required fields or documents are missing pre-submit — both are answered in the Compliance section below. The entrance point for the whole pending-gated surface — status vocabulary, recovery paths, and the auto-release sweep — is Troubleshoot a pending number or Sender ID.
How do I re-verify in time and avoid losing a dormant number to the auto-release sweep?
attach-compliance-profile is not the only unblock path. The Number lifecycle page carries the full recovery toolkit: re-submitting a rejected or expiring compliance profile (the re-verify round), reclaiming a parked number inside its grace window with POST /api/v1/numbers/:id/reclaim (returns 409 when the window has lapsed or the number is not parked), re-trying a failed carrier release with POST /api/v1/numbers/:id/retry-release, and re-attaching a partial messaging profile or voice connection with POST /api/v1/numbers/:id/repair-provisioning. Attach the approved profile — or complete whichever of those re-verify paths fits — before the grace deadline lapses; once the sweep runs, the only recovery is a fresh purchase, and the number.released_compliance_timeout webhook is the signal to route into that workflow. Both the attach and the reclaim path are tenant-owned controls you trigger on your own schedule.
I tried to send to a contact and got 422 CONTACT_ERASURE_PENDING — what lifecycle owns it?
The contact is inside an in-flight Article-17 erasure: a pending or executing erasure request exists for them, and the platform refuses every outbound send to that contact until the window resolves — a campaign recipient is marked skipped with reason: "erasure_pending" (no wallet deduction held on it), and a direct API send gets the structured 422 back with details.contact_id so you can deep-link to the contact. The gate is tenant-owned: it is your erasure request blocking your own send, not a platform compliance verdict.
Two resolutions, both operator actions from your side: cancel the erasure from the contact’s profile — POST /api/v1/contacts/:id/gdpr/erasure-request/:requestId/cancel — and sends to that contact unblock immediately, or let the cooling-off window (7 days by default) run out; the scheduler then hard-deletes the contact, after which any send to the phone number recreates nothing and behaves like a send to an unregistered recipient. Do not retry-loop the 422 — the refusal is deterministic for the life of the request. The cancel and wait options, plus the terminal DSAR_NOT_CANCELLABLE gate, are in the erasure question above, and the send-side codes are in the Error Code Reference.
Number identity & caller ID
Why did my API call reject a phone number with an E.164 validation error?
A non-E.164 number is refused before anything is dispatched — a deterministic422 (or 400 on some surfaces) with a VALIDATION_ERROR-class code and a message naming the failing field, for example "The 'to' field must be a valid E.164 phone number". The gates all enforce the same shape — + followed by 2–15 digits with a non-zero country code — and they run pre-send (nothing is billed, nothing is dispatched): the send endpoints (POST /api/v1/messages/sms and the per-channel variants), the caller-ID register (POST /api/v1/voice/caller-ids/verify), voice dial endpoints, flow send nodes (to must be a valid E.164 phone number), and the LCR route-quote. Because the gate is deterministic, retrying the same body fails identically — fix the value first.
Normalization is a client-side step: accept the national format a user typed, normalize to E.164 (a libphonenumber port in your stack) at collection or import time, and pass only the normalized form on your API surface. When a stored value can’t be trusted, resolve it with the lookup surface — POST /api/v1/numbers/lookup returns valid: false for anything unparseable and the national format of anything that parses (see Number Lookup; the data-model split between a validated number and an enriched number is on Data model). On a refused send, the Delivery Log row carries the number as-sent plus the validation code, so the audit trail names exactly which value your integration passed. The code family is in the Error Code Reference, and the shared E.164 shape is answered above in messaging. The whole decode-pre-flight-fix walkthrough — including JS/Python E.164 formatter snippets and the from_number source-resolution post-check — is on the validation gates runbook.
Why was my send rejected with a validation gate code?
Four codes sit in this deterministic family, all refusing pre-send with nothing billed and nothing dispatched — and all named in the Error Code Reference’s Validation section:INVALID_PHONE_NUMBER (a phone field isn’t E.164), INVALID_FROM_NUMBER (the from_number source id you passed is missing, disabled, or malformed), MISSING_REQUIRED_FIELD (a required field is absent from the body), and INVALID_EMAIL (the email recipient address is malformed). The error envelope always tells you two things: code says which gate tripped, and details.field / details.value echo the exact value your integration sent. Any mention of these codes — the FAQ and the Error Code Reference’s From a code to a runbook routing table both do — resolves to one destination: the validation gates runbook, which decodes each envelope, gives the GET /api/v1/numbers/lookup/{phoneNumber} pre-flight and the E.164 formatter snippets, walks the per-code rejected-and-corrected request body, and ends with the from_number source-id resolution post-check. Fix the named field and re-send exactly once — a retry loop on an unchanged payload is the most common way this family gets worse, not better.
Where does the logo or brand name a recipient sees come from?
It depends on the channel — there is no single “brand asset” that renders everywhere:- Voice — CNAM. A 15-character uppercase displayed name the handset pulls from the carrier LIDB lookup per call. Register it per number with
PUT /api/v1/numbers/:id/cnam; a sibling subaccount cannot read or mutate another tenant’s registration. Full contract: CNAM & Caller ID. - Voice — RCD branded calling. A verified brand name (up to 40 characters), a square logo, and a reason-for-call line rendered on the incoming-call screen, carried cryptographically inside the signed STIR/SHAKEN PASSporT. It requires A-attestation on the from-number (the number must be an active platform-owned number), so an external caller ID never carries it; when RCD drops, the recipient still sees the attestation result and any CNAM name on file. See Branded Calling (Rich Call Data).
- RCS — the agent’s brand card. The business name, logo, and color a recipient sees in the rich-messaging thread come from the RCS agent registration you submitted (
brand_logo_urlon the agent registration, a public HTTPS-hosted square image) — verified or launched agents only. See the RCS channel page. - SMS sender IDs. No logo exists on SMS; presentation is the alphanumeric Sender ID itself, which several countries require you to register per destination before traffic delivers — see Sender-ID Registration.
GET /api/v1/brand-identity/status — the model is on Brand identity & trust score.
Why did my outbound voice/SMS refuse with UNVERIFIED_CALLER_ID?
The pre-send gate rejected the from because the number is neither an active platform-owned DID on your org nor a row you have verified as your own. The refusal happens before any carrier dispatch, so no wallet deduction is taken. You have three fixes:
- Send from a number your org owns — an active DID you purchased or ported into Orbit passes the gate with no extra step.
- Verify the number you already control — register it once and it becomes selectable like an owned number (the register → challenge → confirm journey is the next question).
- Pick from the picker, not from memory — the softphone caller-ID dropdown and the Make a Call dialog list every owned DID and verified caller ID your dial-time gate would accept, each labelled Active / Pending activation / Verified / Pending verification — anything shown as non-selectable is the same thing the gate would refuse.
UNVERIFIED_CALLER_ID applies to both voice and SMS — the gate is the same ownership/verification check on the outbound from for either channel. The code and its siblings (VOICE_CHALLENGE_FAILED, VOICE_CALLER_ID_REJECTED) are in the Error Code Reference under Voice; the send-side contract is in the Voice API reference.
What is a verified caller ID, and how do I get one through register → confirm?
A verified caller ID is an external number — a mobile, landline, or partner desk line you control outside Orbit — that you claim ownership of by proving you can answer it. It is not a number purchase: you never buy the routing, nothing ports, and the number stays on its current carrier. The journey is:- Register —
POST /api/v1/voice/caller-ids/verifywith the E.164phone_number, an optionalfriendly_name, andchannel: "sms" | "voice". Orbit sends a one-time code to that handset (TTS readback on voice, a text message on SMS). - Confirm —
POST /api/v1/voice/caller-ids/confirmwith theverification_idand the code. A correct code flips the row toverifiedand opens the number as an outboundfrom. - Retry the challenge when it fails — a wrong code, an elapsed TTL, or an exhausted attempt budget leaves the row
expired; re-send the code withPOST /api/v1/voice/caller-ids/:id/resend. If the challenge dispatch itself fails, the endpoints raiseVOICE_CHALLENGE_FAILEDwith the infrastructure detail redacted — re-run the challenge (register or resend) rather than debugging the redaction.
pending → verifying → verified, or expired when the challenge burns out; resend re-arms it). The endpoint contract with example responses is on Manage outbound caller IDs.
Keep three look-alikes separate:
- Verified caller ID vs 10DLC — 10DLC is a US messaging-carrier registration of a brand + campaign for SMS deliverability. Verifying a caller ID proves you control a number’s handset for outbound presentation; it does not register any messaging campaign.
- Verified caller ID vs STIR/SHAKEN — attestation grades how the carrier network trusts the call, not who you are. An owned Orbit number attests at level A; a verified external caller ID attests at B at best (with a delegate certificate registered), because ownership is what earns A.
- Verified caller ID vs CNAM — verification picks which number shows; CNAM sets what name carriers display next to it.
What is the trust registry, and what does its lookup answer?
The trust registry is the public, per-organization opt-in lookup endpoint that answers “is this caller or agent really this brand, and what is it delegated to do?” from outside your tenant. One unauthenticated GET (/api/v1/public/.well-known/ans/registry/lookup?org=<slug>&agentId=<id> or &ans=<uri> or &phone=<e164>) composes the verification signals Orbit already computes for your organization into one resolve: the brand-identity rollup and trust score, the STIR/SHAKEN attestation tier (resolved with the same logic the dial path stamps on the egress INVITE, so the registry reports the egress-time decision rather than a guess), and the delegated capability scopes of your active API keys (read through the default-deny scope ledger — key ids, hashes, and key material never cross the boundary).
Publication is a tenant-owned control per organization: enable the public trust-registry flag, and the lookup answers; leave it off, and unknown organizations, opted-out organizations, and malformed lookups all collapse to the same 404 AGENT_TRUST_NOT_FOUND envelope, so a probe can neither enumerate tenants nor learn who publishes. Responses are capability and verification metadata only, edge-cached for 60 seconds — short enough that a newly revoked agent or key drops out within a minute.
The platform-side anchor this registry complements is the ANS (Agent Name Service) Trust Card — the signed, domain-anchored identity document served at /.well-known/ans/trust-card.json that declares Orbit’s own ANS URI, endpoint surfaces, and Ed25519 signing-key set, so a resolver can verify an outbound request from Orbit’s agents against published keys. The full model — why caller identity needs a verifiable anchor, the four-step card verification flow, and how to raise your organization’s attestation posture — is on ANS Trust Card and trust registry.
Why did my caller-ID rewrite get rejected on my own SIP trunk with VOICE_CALLER_ID_REJECTED?
Your trunk’s outbound caller-ID rule set rejected the value — it is neither on the trunk’s allowedCallerIds list nor an org-owned DID. The guard exists so a rewrite rule cannot spoof an arbitrary number as your FROM. Two fixes, both tenant-owned:
- Add the number to the trunk’s allow-list — set
allowedCallerIdson the SIP-trunk create/update call (the SIP trunks endpoints) so the rewrite target is permitted. When the list is empty the gate falls back to “all org-owned DIDs” — a rewrite can target any DID your org owns, but never an external number; populate the list when you need to present a number you don’t own through Orbit. - Or verify the number — an org-owned DID, or a number you verified through the register → confirm journey above, passes without touching the trunk’s list.
How do I rotate or remove a verified caller ID?
Removal is a tenant-owned, self-serve delete:DELETE /api/v1/voice/caller-ids/:id (or the revoke action on Voice → Caller IDs in the dashboard) moves the row to revoked and the number immediately stops being a selectable outbound from. Effects to plan around:
- In-flight sends — messages and calls already dispatched keep the caller ID they left with; the delete gates new sends only. Any send attempted after the revoke with that
fromgets theUNVERIFIED_CALLER_IDrefusal above. - Rotation — to rotate, register the replacement number through the verify → confirm journey first, switch your senders (dialer campaign
caller_id_e164, softphone per-agent default, sender pools), and only then revoke the old row. Re-registering a previously revoked number re-claims it through a fresh OTP challenge — there is no un-revoke, so don’t treatrevokedas a pause. - Expiry re-verification — a row with a future re-verify deadline (
expires_at) can move to a re-verify-required state; when that happens the challenge needs to be re-run withresend, same as anexpiredrow.
Number Porting
My port seems stuck — where do I look?
CallGET /api/v1/numbers/porting/:id/timeline. It expands the flat porting status into a structured per-stage view — submitted → validating_loa → carrier_review → foc_assigned → foc_scheduled → completed — so you can see exactly which stage the port is sitting on. A hold at validating_loa is a document issue (check and re-sign the LOA); a hold at carrier_review means the losing carrier hasn’t released the order yet, either of which is actionable for your support ticket.
If no stage has advanced after 14 business days, snapshot the timeline response and open a support ticket with it — the per-stage detail goes straight to the carrier escalation rather than a fresh investigation from scratch. See Number Porting for endpoint detail and the Port a number end-to-end guide for the full LOA/FOC flow.
Does Orbit support inbound porting from any carrier?
Yes — you bring a losing carrier’s number onto Orbit through the self-serve port-in flow (pre-check → LOA upload/sign → submit → per-stage tracking). Completion depends on the country and losing carrier: carrier-published ranges run roughly 7–14 business days in the common bands (with a wider 5–15+ spread across all geographies). The carrier-assigned FOC date on the timeline endpoint is the authoritative estimate — an expectation, not a guarantee.The number I ported out got rejected — what do the codes mean?
The port-out rejection family splits by who refused and when:401 PORT_OUT_PIN_MISMATCH— your own PIN gate refused it. Port-Out Protection is a tenant-owned per-number PIN, and when it is enabled the release call must carry the matchingport_out_pin; a missing or wrong value is rejected before anything is dispatched to the carrier, so a bad PIN never burns a carrier attempt. The PIN is 4–32 characters, hashed at rest, and never returned by any endpoint —GET /api/v1/numbers/:id/port-out-protectiononly ever exposes{ enabled, set_at }. Set or rotate it withPOST, and disable it withDELETEon the same path; if the PIN is lost, rotate to a fresh one (there is no recovery flow). Protection is per-number opt-in, so a batch behaves numerically: only the protected numbers demand the PIN, and unprotected numbers auto-pass.409 CONFLICT— the number is not in a portable state (port_out_pending,released,suspended,parked), or another port-out for the same number is already in flight. The in-flight claim is atomic, so a409almost always means “someone already submitted this” — check the list endpoint before retrying.422 PORT_OUT_NOT_SUPPORTED— this number’s carrier has no self-serve port-out adapter. Nothing was dispatched; run the port in the carrier’s own portal instead.502 CARRIER_PORT_OUT_FAILED— the carrier rejected or errored the order. The number’s local status rolls back to its prior state, so nothing is stranded in a phantom pending — the number stays yours and a corrected submission is safe to re-run.
Someone says a number was ported away from my tenant — how do I freeze outgoing ports and vet the request?
Handle it like the Port-Out / Losing Carrier concept lays out. Freeze first, validate second, release last:- Freeze. Set (or rotate) a Port-Out Protection PIN on the number with
POST /api/v1/numbers/:id/port-out-protection. With protection on, every outgoing port submission for that number is refused withPORT_OUT_PIN_MISMATCHunless the caller presents the PIN — an attacker holding only your API credentials cannot complete the release. Enable it ahead of time on the numbers whose loss would hurt; nothing about a legitimate port-in to you is affected. - Validate. In the US, carrier-to-carrier port validation is bounded by CPNI rules — you owe the gaining carrier a response to their validate-ack cycle only, and only for account data the customer genuinely authorized. Confirm the request actually came from the subscriber (signed authorization matching your account records, the gaining carrier’s OCN on the request) before you act on it.
- Release. Present the PIN with the port-out payload only once the authorization checks out; until then the mismatch gate holds.
PORT_OUT_PIN_MISMATCH (401) is the PIN gate above; CARRIER_PORT_OUT_FAILED means the carrier rejected or failed the order — the claim rolls back and the number stays yours; and PORT_OUT_NOT_SUPPORTED (422) marks a number on a provider with no self-serve port-out adapter, which moves through support instead. Theft-by-port (slamming) is exactly the fraud class the PIN gate exists to stop — the step-by-step runbook for all three rejections is Troubleshoot port-out rejections, and the full ownership model is Port-out lifecycle and ownership model.
What is the maximum message size?
Suppression & opt-outs
Why does a message fail after a recipient replied STOP?
A STOP reply puts the recipient on your opt-out suppression list, and every later send to them is refused pre-send withRECIPIENT_OPTED_OUT (see the Error Code Reference). Suppression is a tenant-owned control — Orbit prevents you from messaging an opted-out recipient again, and you can review the resulting opt-out records through the Opt-Out Lists API or the Opt-Outs API.
A send does not re-check the list you manage manually — a recipient moves off suppression only through an explicit re-consent: a fresh opt-in via the Consent API, their own START reply, or a Preference Center opt-in (see Removing a suppression (re-opt-in)).
See Opt-Out & Suppression Lists and Send Gates for the full mechanics.
How do I bulk-import an existing STOP list?
Two synchronous paths, no background import job needed:- Suppression-list CSV import — upload one CSV of suppressed addresses (≤ 25 MB, ≤ 100,000 rows) to
POST /api/v1/compliance/suppression-list/import. It deduplicates against your existing list and returns per-row results, so re-uploading an overlapping file is safe. See Bulk CSV import. - Batched opt-outs API — if your export is contacts keyed by channel,
POST /api/v1/contacts/optouts/bulkaccepts up to 500 rows per call and is idempotent: rows already opted out come back asskipped, not errors. See the Opt-Outs API.
Does opt-out work per-channel?
Yes — list semantics are per-channel. Each suppression entry carries a channel scope, and phone and WhatsApp addresses default to scopeall — a STOP signal on a phone number suppresses every channel reachable on that number — while email addresses are scoped to email; a channel column on a bulk CSV import overrides the default per row. Because that scope is per-tenant and per-row, you decide how broadly a recipient’s opt-out reaches; the recipient chooses which channels to leave (see How suppression happens).
The full scope set is all, sms, voice, whatsapp, email, push, telegram, messenger, rcs. Per-contact state lives on the contact’s channel_preferences (see the Opt-Outs API).
See Opt-Out & Suppression Lists and Send Gates.
Voice
What codecs does Orbit support?
Orbit supports OPUS, PCMU (G.711 µ-law), and PCMA (G.711 A-law) — negotiated OPUS → PCMU → PCMA in that preference order per leg, with OPUS recommended for the best quality at lower bandwidth. The trade-offs are defined in the glossary entry Codec (Voice Codec).How do DNIS routes override inbound routing?
They don’t override it — they sit under it. Resolution for an inbound call runs in a fixed order, and the first hit wins: a per-number route on the dialed DID always beats every DNIS pattern rule, and a DNIS rule only fires when no per-number row exists. Among your DNIS rules the resolver ranks them deterministically — lowestpriority first, then the longest (most specific) pattern, then the oldest rule — and fall-through beyond that is your org default, then the safe default. So “overriding” a DNIS pattern on one line means adding a per-number route for that DID; the pattern keeps covering the rest of the pool untouched. Manage both on Voice → DNIS pattern routing and the inbound-routing surfaces; the full precedence chain is on Inbound Voice Routing and the DNIS pattern routing guide.
Can I bring my own SIP trunk?
Yes. Connect your existing carrier via SIP trunking. Orbit supports TLS/SRTP encryption and standard SIP authentication. The create/manage endpoints (create, list, get, update, delete) are in Voice API → SIP trunks, alongside the per-trunk health snapshot and route-quality views. When a trunk fails, the platform keeps calls moving:- It re-checks the trunk on a cadence with live SIP pings, and the verdict lands back on the trunk row — a trunk that stays unregistered stops being offered new calls.
- It walks the trunk’s designated failover chain and picks the next healthy route in your trunk pool.
- Per-trunk route-quality scoring (answer ratio and capacity utilization over the 24 h / 7 d windows) ranks trunks so you can pull a bad route out of rotation while you fix it.
- If no usable failover is configured, dispatch falls back to the Devotel wholesale softswitch — outbound keeps terminating.
Why did my voice call connect but there’s no audio?
SIP signalling and RTP media are separate paths — a call can answer with audio broken even when the signalling is clean. The top causes:- NAT or SIP ALG on the customer’s firewall — the far endpoint advertises a private address in SDP, or a SIP ALG rewrites media addresses wrong, so RTP flows inbound but not back. Disabling SIP ALG is the single highest-hit-rate fix.
- SRTP keying mismatch on TLS trunks — the carrier declines SRTP or negotiates a keying mode the trunk did not accept; calls connect, one or both directions go silent.
- Codec mismatch — signalling completed but no common audio codec was negotiated, so the
200 OKcarries no audio guarantee. (See Codec (Voice Codec) in the glossary for the OPUS / PCMU / PCMA trade-offs.)
Why do some outbound calls reject with VOICE_TRUNK_HOURLY_CAP / CONCURRENT_CAP / CPS_CEILING?
Your own SIP trunk enforces per-trunk capacity gates on top of the org-level fraud guard: an hourly call budget (hourlyCallCap), a concurrent-call ceiling (maxConcurrent), and a fixed 50-calls/second burst ceiling. A trunk can register cleanly and still reject calls once one of these trips — the concurrent gate answers SIP 486 Busy Here so your PBX knows to retry later rather than fail over. Read the live gauge vs your configured cap on GET /api/v1/voice/sip-trunks/:id/utilization and raise the cap on the trunk’s perTrunkLimits when load is genuine. Full symptom → check → fix matrix: Troubleshooting: SIP trunk capacity gates.
Why did outbound calls stop using my SIP trunk?
When the trunk’s last registration probe reads anything other thanregistered, the dial path stops offering it new calls: it walks your configured failover chain for a registered spare, and with no usable candidate it falls back to the Devotel wholesale softswitch so calls keep terminating. Re-test the trunk (POST /api/v1/voice/sip-trunks/:id/test or Test trunk in the dashboard), read lastRegistrationError for the named cause (DNS, 401 on rotated credentials), and confirm the failover chain points at a registered trunk. Older client integrations may show this as a 503 SIP_TRUNK_UNREGISTERED — the current gate falls back rather than refusing. Full runbook: Troubleshooting: SIP trunk — unregistered at dial time.
Why did my outbound call reject with 422 VOICE_DNO_BLOCKED?
Orbit’s origination-time Do-Not-Originate (DNO) gate rejected the resolved from because it matches an entry on your organization’s DNO list (or the platform env baseline baked into the deployment) — the pattern a DNO entry encodes is an invalid, unallocated, inbound-only, or spoof-bait caller ID (spoofed government/bank lines, inbound-only toll-free, unassigned ranges) that must never be presented as a calling-party number. The gate runs before any billing hold or dispatch, so the refuse takes nothing from your wallet. Two numbers to keep straight: this is a refusal on your own list, and it is a tenant-owned control — the per-organization list ships empty, so a 422 here means someone in your organization configured the override (or your deployment’s operator curated the env baseline).
The list reconciles a platform env baseline with a per-organization override in one of three modes, all living at organizations.settings.voice.dno_override (owner/admin write, PUT /api/v1/settings/general — or the organization’s voice settings in the dashboard): extend (default — your entries add to the baseline), replace (your entries replace the baseline entirely), subtract (your entries are removed from the baseline). Fix paths, in order:
- Correct the
from— readdetails.matched_prefixon the error envelope to see which entry fired, and re-dial from a number you legitimately own. - Adjust the list if the block is wrong — when a baseline entry wrongly matches a number you legitimately own, carve it back with the
subtractmode; when your own entry is too broad (short prefix hitting a whole range), tighten or remove it.
UNVERIFIED_CALLER_ID, which is the ownership check on the from (see the caller-ID question above). The config guide is Do-Not-Originate (DNO) caller-id blocking, triage of the code and its sibling voice gates is on Troubleshooting: voice destination and emergency blocks, and the code family is in the Error Code Reference.
How do AI voice agents work?
Orbit’s voice agents combine:- STT (Deepgram Nova-3) — converts caller speech to text
- LLM (Anthropic Claude) — generates a response
- TTS (Cartesia Sonic 3.5) — speaks the response back
Why does registering a voice-inference provider key return 422 VOICE_CREDENTIAL_PROVIDER_REJECTED?
PUT /api/v1/voice/voice-provider-credential/{kind} and POST /api/v1/voice/voice-provider-credential/{kind}/rotate both probe the credential live with the provider before persisting it. If Cartesia (TTS) or Deepgram (STT) refuses the key — typically with a 401 or 403 — Orbit returns VOICE_CREDENTIAL_PROVIDER_REJECTED and does not save the key, so it can never flip to active or get enforced. The fix is always on the provider side: re-issue or re-scope the key in the provider’s own dashboard, re-copy it without whitespace, and retry. A key that works in the provider console but fails here usually lacks the permission the voice pipeline needs (transcription scope for Deepgram, synthesis scope for Cartesia). The full cause table, envelope sample, and ticket checklist are on Troubleshooting: voice-model credential rejected.
Why did my voice-clone creation return 422 VOICE_CLONE_CONSENT_REQUIRED?
A voice clone trains a custom TTS voice from a real person’s recorded audio, so the gate refuses the creation until you prove two consent classes: recording consent on the source call (an active consent_state='granted' receipt for the recorded call the sample comes from — the audio itself never existed lawfully without it) and a voice-owner attestation (the cloned person’s signed voice release or written consent, described in the free-text attestation plus the biometric_consent_acknowledged flag). Uploading a raw sample (POST /api/v1/voice/clones/upload) skips the recording-consent receipt but carries the same voice-owner attestation bar; a clone designed from a text prompt never trains on a real voice, so neither consent class applies. Once the clone exists, dialing with it is a separate gate — outbound calls that speak a cloned or synthesized voice need the recipient’s own written consent (see FCC AI voice written consent). The full enrollment flow — attestation fields, one- and two-sided consent classes, watermarking, and cloning-credit pricing — is on Enroll voice clones from calls.
Why did my voice call reject with FCC_AI_VOICE_WRITTEN_CONSENT_REQUIRED even though the recipient consented verbally?
The FCC’s declaratory ruling 24-17 classifies AI-generated, synthesized, and cloned voices as “artificial or prerecorded voice” under 47 CFR § 64.1200(a)(1)(iii), so an outbound call that will play a TTS playback, a cloned voice model, or an AI voice agent needs the recipient’s prior express written consent — a spoken “yes” on a prior call doesn’t qualify for that content class. The gate runs pre-dispatch, so the blocked call is never billed. The remedy is to record the recipient’s written consent through POST /api/v1/compliance/consent (or Contacts → Consent in the dashboard) before dialing, attaching the written proof with the consent_proof_url field — see the consent API for capture and revocation mechanics. The check reads a consent record either on the dedicated consent type fcc_24_17_written_consent or via a lawful_basis hint in the record metadata, and the record must be granted, not revoked, and in an opted-in state. The error details name the classified content type (synthetic or cloned) so you know which content class the call belongs to. This and the other consent gates are tenant-owned controls — you decide which legal bar your traffic answers, and Orbit enforces the records you keep (see Compliance posture FAQ). Human-voice calls (click-to-call, bridged agents) fall outside the ruling and pass with no consent record. Until a written-consent record exists, blocking is the compliant outcome, not a bug.
Why did my MCP server registration return 422 INVALID_MCP_SERVER_URL?
The registration write-time SSRF guard rejected the server URL — the platform can never let an agent’s tool calls egress into a network address a public caller would resolve internally. The guard fails a candidate URL on any of: non-HTTPS scheme, a loopback or private/CGN address (127.x, 10.x, 172.16–172.31, 192.168.x, 100.64–100.127), a link-local cloud-metadata host (169.254.x), an internal-only hostname suffix, or a DNS record that resolves to any of those — the DNS-rebind class is checked at register time, not deferred to call time. Supply a public HTTPS endpoint, and use the 422 details (which echo the resolved address class) to confirm what tripped. The OAuth variant INVALID_MCP_OAUTH_TOKEN_URL applies the same guard to the OAuth2 grant’s token_url on the same registration payload — the token endpoint is validated identically to the server URL. A 409 MCP_SERVER_NAME_CONFLICT is a different failure: server names are unique per agent, so rename the new server or update the existing one — the conflict never overwrites the earlier registration. All three codes are in the Error Code Reference under Agents.
Why does my call to an emergency number return EMERGENCY_CALLING_NOT_SUPPORTED, and can a tenant lift the block?
No — the block is a platform hard guard, not a tenant-owned toggle. Every outbound voice call to a US/NANP emergency short code (911) — or to 112, 999, or 000 for the rest of the world — is rejected pre-flight with 422 EMERGENCY_CALLING_NOT_SUPPORTED: no carrier dispatch happens, no per-minute cost is billed, and no Public Safety Answering Point (PSAP) is contacted. The gate fails closed for US +1 recipients, and no tenant setting, support request, or API parameter lifts it. E911 (PSAP-aware routing with registered dispatchable-location information) is on the platform roadmap but not yet shipped — until it is, dial emergency services from a regular mobile or landline phone. Ask support to put you on the E911 notification list if that roadmap matters to you; the full behavior, plus the venue and example data the error details carry, is on the Emergency calling page.
A separate, correctly confused gate sits on the same quiet-hours surface: when a US (+1) recipient’s timezone cannot be resolved, outbound voice fails closed by default — the org’s unknown_timezone_policy governs that behavior:
Set the policy under Settings → Compliance → Quiet hours. Unlike the emergency-calling block above, this one is tenant-owned — the difference matters for triage: the quiet-hours gate you can relax, while
TCPA_DIALING_WINDOW_BLOCKED / TCPA_FEDERAL_DIALING_WINDOW_BLOCKED (the hard-guard TCPA dialing-window family on the troubleshooting page) is a platform hard guard you can only schedule around. The emergency-calling block and the federal TCPA window are both platform hard guards; the unknown_timezone_policy toggle is the one lever in that family you own.
Why did my hot-desk sign-in reject with 409 HOT_DESK_RACE?
Two sign-ins — for the same device, or the same user — landed within milliseconds of each other, and the loser of the race is asked to retry. The device ends up bound to exactly one winner either way. Retry once after a short back-off: if the reject persists, something else (a stuck session) is holding the device — check the session board before hammering the endpoint. The decision tree for every hot-desking 409/410/404 is on Troubleshooting: hot-desking sign-in rejects; all five codes are in the Error Code Reference under Voice.
Why does hot-desk sign-out return 410?
410 HOT_DESK_ALREADY_SIGNED_OUT (or 410 HOT_DESK_ALREADY_RELEASED on the admin release path) means the session row is already terminal — the earlier sign-out, expiry, or displacement won. Sign-out is idempotent, so this is a report, not a failure: flip your UI to signed-out first, then re-check GET /api/v1/voice/hot-desking/me to confirm what is actually bound. A 404 HOT_DESK_NO_ACTIVE_SESSION means you were never signed in.
How do I see who is on which device right now?
GET /api/v1/voice/hot-desking returns the organization’s active sessions, newest first and capped at 200 — the same list that powers the Voice → Hot-desking board in the dashboard. Use it to answer “is this phone taken?” before a walk-up attempt or an admin revoke (POST /api/v1/voice/hot-desking/:id/revoke, owner/admin only). The endpoint contract is in the Voice API reference → Hot-Desking.
How do reservations interact with a walk-up sign-in?
A reservation on the booking calendar blocks anyone else from claiming that desk during the booked window: a walk-up sign-in is held for the booker until the 15-minute no-show grace runs out, then the desk opens back up. Conflicts resolve at booking time with409 HOTELING_DOUBLE_BOOKED, not at sign-in. The reservation model and the no-show sweep are in the hot-desking guide.
How do DNIS routes override inbound routing?
They don’t — a DNIS pattern is the fallback layer under your per-number configuration, and the precedence order is fixed. Create DNIS routes under Voice → DNIS routes (/voice/dnis-routes), and precedence there is shown right on the row. At call setup, resolution runs in order:
- A per-number route on the exact dialed DID always wins, no matter how low a pattern’s priority sits.
- Among matching DNIS patterns, priority ascending, then the most specific pattern, then the oldest entry, breaks the tie.
- With no per-number route and no pattern match, the org default destination answers, so a call never ends unrouted.
How do paging groups reach desk phones?
A paging group holds one-tap broadcast members of two kinds: a SIP extension (a registered desk phone the page rings directly) or a teammate on the in-browser softphone (delivered as an in-app page). Create the group under Voice → Paging groups (/voice/paging-groups) or with POST /api/v1/voice/paging-groups, then trigger the announcement with POST /api/v1/voice/paging-groups/{id}/page.
The desk-phone reachability rule: the page only reaches a SIP extension whose credentials are live-registered (the page delivery report counts drops when a device is offline or unregistered). Device registration is the prerequisite — provision them under Voice → Extensions with QR-code or config-file provisioning and track live registration there; the full flow is Voice extensions (SIP credentials), and the group mechanics, member limits, and paging-versus-ring-group trade-offs are in Broadcast paging with paging groups.
What does the STT playground measure?
Before you commit a speech-to-text vendor to a voice agent, the STT Playground (/voice/stt-playground) transcribes one short clip — recorded from your microphone or uploaded — against every STT vendor Orbit recognizes and reports each vendor’s transcript, confidence score, and processing latency side by side. A vendor that is recognized but not yet provisioned is shown as Not yet available rather than a fabricated result. Nothing you record or upload is persisted server-side — the clip is ephemeral, so the playground is safe for a quick comparison. The full walkthrough is in STT Playground: compare speech-to-text vendors.
What is VAQI and how is it computed?
VAQI (Voice Agent Quality Index, Voice → Voice Agent Quality at/voice/vaqi) aggregates every completed voice-agent session in a selectable window (24 h, 7 d, or 30 d) into per-session-mechanics rollups, auto-refreshing every 30 seconds:
- Latency — time to first byte (TTFB) and turn gap, each reported as a mean plus p95 across per-session averages, with component cards that split the time into average STT, LLM, and TTS latency so a regression names its stage.
- Turn-taking — average caller↔agent turns per session.
- Barge-in — the share of turns where the caller interrupted the agent, and the clean-yield success rate of those interruptions (the agent stopped and yielded instead of talking over the caller).
GET /api/v1/voice/vaqi.
What happens when an agent presses the panic or distress button during a live call?
The softphone sendsPOST /api/v1/voice/agents/me/distress from the authenticated agent session. Orbit first persists a durable, per-tenant incident row, then forces recording on for that call regardless of queue policy, publishes agent.distress.raised to the omnichannel supervisor stream, and appends an audit row with the severity, reason, and recording outcome. The event pages supervisors on the Supervisor live monitoring surfaces; dismissing the alert does not remove the incident or its audit evidence; both rows follow the tenant’s standard retention windows, and pressing the button does not consume credits. The /me/ scope prevents an agent from raising an alert for another agent. See the Agent distress alert model for the sequence and retention details, and the Voice API reference for the endpoint contract.
Why is my agent stuck in wrap-up even after the call ended?
A disposition gate blocks the agent’sbusy → available flip until a wrap-up row exists for the call — the agent posted (or their softphone auto-posted) none, so the queue refuses to release them back into dispatch. The three refusal codes decode different layers of that gate:
DISPOSITION_REQUIRED(422) —POST /api/v1/voice/agents/:id/statuswithstate: "available"was refused because the queue hasrequire_disposition: trueand no disposition row exists for the agent’s most recent terminal call. The fix is the missing write: an agent or supervisor POSTs/api/v1/voice/queues/:queueId/calls/:callId/disposition(idempotent on(queue_id, call_id)— safe to retry), and the status flip is then accepted. A background sweep will auto-flip a still-stuck agent after the queue’s wrap-up ceiling — but writing the disposition is the resolution, not waiting out the sweep.DISPOSITION_CODE_NOT_FOUND(404) — your disposition POST named adispositionId/dispositionCodethat is not an active row in that queue’s wrap-up reason catalog. Per-queue catalogs are tenant-owned: an owner/admin re-adds the code withPOST /api/v1/voice/queues/:queueId/dispositions, and agents pick it again fromGET /api/v1/voice/queues/:queueId/dispositions.DISPOSITION_TAG_NOT_FOUND(404) — your POST named atagSlug/tagIdthat does not resolve to an active row in the tenant-wide tag catalog. Tags are a separate catalog from the per-queue wrap-up codes — the 404’sdetails.missingSlugs/details.missingIdsnames exactly which ones missed, so re-add the tag (or fix the slug you sent) on the tag-catalog surface, not the queue catalog.
Why did flagging a call for malicious-call trace return 400 MCID_INVALID_DIRECTION or 409 MCID_CALL_TOO_OLD?
Both codes are eligibility gates on the same operation — POST /api/v1/voice/mcid/:callId — so decode them by which gate refused you. 400 MCID_INVALID_DIRECTION means the call you named was not inbound. Malicious Call Identification (the *57 feature code on NANP, a regulated UCaaS service) exists to let a callee report a threatening or nuisance caller and preserve the originating identity for law-enforcement or an abuse team — flagging is therefore inbound-only, and an outbound call id is refused before any record is written; the flag path itself never originates or signals a leg of its own, so all outbound traffic continues to exit only through the Devotel softswitch. Re-check the call’s direction on the call detail page before flagging. 409 MCID_CALL_TOO_OLD means the call fell outside the recent-eligibility window — flagging is bounded to the trailing seven days, because the carrier-side CDR/SIP trace trail that makes a trap-trace actionable for an abuse team degrades with age, and a stale historical call can’t be retroactively “traced” once that trail has aged out. Flag nuisance calls promptly: from the call/recording detail page in the dashboard, or by dialing *57 from the receiving softphone — both write the same tamper-evident audit entry. A flag that landed already returns the existing record on a re-POST (idempotent), not an error. The endpoint surface — flag the call, read its state, and pull the owner/admin regulated export — is documented in Voice API → MCID.
Why did my payment-capture step return 403 KBA_VERIFICATION_REQUIRED?
The sensitive action you called — payment capture, PII disclosure, or an account change on a live call — gates on a verified knowledge-based-authentication session for that call, and none was verified at request time. Three states decode to the same 403, so check the session with GET /api/v1/voice/kba/:callId before changing anything: pending — the challenge was started (POST /api/v1/voice/kba/:callId/start) but the caller’s answers were never submitted or a factor mismatched with attempts still left; submit the correct answers on /verify and retry your action. failed — the bounded attempt budget (three by default) burned out and start now answers 409 KBA_LOCKED; the only way to re-open the gate is a deliberate POST .../start body of { "restart": true }, which starts a brand-new challenge — no stray retry routes around the lockout. expired — the verified session outlived its TTL (four hours on the per-call ledger), and status computes at request time so a stored verified flag past expiry reports as expired, never as verified; run the challenge again while the caller is still on the line. On the stateless token pair the same gate reads via POST /api/v1/voice/kba/session/check on your session_token — those tokens carry a 15-minute signed expiry and must be minted per call. Session semantics, factor selection, and the attempt budget are on KBA caller verification; the end-to-end runbook with curl for both endpoint families is Verify a caller with KBA on a live call.
Video
Why did my video poll fail to launch — or why did a vote come back with an error?
Polls in a video room ride the room’s realtime data channel, so launch and vote failures share one cause class: the room’s realtime connection could not carry the message. Distinguish the two codes by which action refused:VIDEO_POLL_LAUNCH_FAILED(500) —POST /api/v1/video/rooms-scheduled/:id/pollsfailed to broadcast the launch to the room. Launch is a host moderation action, so before retrying confirm the caller holds a host-capable role: owner/admin (or a participant minted with thehosttier) — the write guard refuses other keys before the broadcast is even attempted. The refusal happens before any room state changes, so no phantom poll exists: fix the caller’s key, then retry once after a short backoff.VIDEO_POLL_VOTE_FAILED(500) —POST /api/v1/video/rooms-scheduled/:id/polls/votefailed to carry one participant’s vote. Voting is open to any participant, and the platform stamps the voter identity from the authenticated caller, so a refusal is a transport or room-state problem, not a permissions one. Have the participant re-fetch the room (the poll may have closed in the meantime), then re-submit the vote once.
request_id from the response meta. Poll syntax and the close flow are in the Video API reference; all Video codes are in the Error Code Reference.
Why can’t I ban or unban a video participant?
Three codes cover the ban surface, in order of where they trip:- Check the caller’s role first. Ban, unban, and the ban roster are host moderation actions — owner/admin only. A participant key or viewer-tier session is refused by the write guard before any ban state is touched; run the same call from an owner/admin key (or a
host-tier token) instead of retrying with the same credentials. VIDEO_PARTICIPANT_BAN_FAILED(500) — the ban (POST /api/v1/video/rooms-scheduled/:id/participants/:identity/ban) was refused after the role check, persisting nothing. The identity may already be on the ban list — read it first withGET /api/v1/video/rooms-scheduled/:id/bans, then ban only an identity that isn’t already barred. The siblingVIDEO_PARTICIPANT_UNBAN_FAILED(500) on theDELETEpath is intentionally idempotent: unban returns success even when no ban existed, so treat a double-unban as harmless and a 500 there as transient — retry once, then escalate with the room id.VIDEO_BAN_LIST_FAILED(500) — the ban roster read itself failed. The read is retried automatically against short-lived connectivity blips before this code surfaces, so a 500 here means the read kept failing; retry once, then open a ticket with therequest_idfrom the responsemeta.
Why did my RTMP egress fail to start?
VIDEO_RTMP_EGRESS_START_FAILED (500) on POST /api/v1/video/rooms-scheduled/:id/egress/rtmp means the start did not complete — but check the destination before you retry:
- Verify the destination URL is reachable and streaming is enabled. Confirm the
rtmp_urlyou sent answers from the streaming platform (YouTube, Twitch, or your CDN), that the stream/key pair is active and the broadcast is enabled on that platform’s control surfaces, and that the URL and the stream key are not swapped — the URL carries thertmp://scheme, the secret-bearing key goes in its own field. - Retry the start. The failure is atomic: either the egress was never accepted, or the platform stopped the upstream egress before returning the error, so no orphaned stream keeps rendering your room — a retry is safe and starts fresh. A refusal that survives ten minutes of retries is platform-side; escalate with the
request_idfrom the responsemeta.
DELETE /api/v1/video/rooms-scheduled/:id/egress/rtmp/:egressId when the broadcast ends — an egress left running keeps rendering an idle room to a live destination. The full refusal table (SERVICE_UNAVAILABLE when the streaming control plane is unconfigured on your deployment, destination refuse shapes) is on Troubleshooting: video room lifecycle failures, and the request contract is in the Video API reference.
Why does my in-meeting live transcript show unavailable?
VIDEO_LIVE_TRANSCRIPT_UNAVAILABLE (422) on POST /api/v1/video/rooms-scheduled/:id/ask — the in-meeting AI assistant (“ask AI during the meeting”, catch-me-up) — means the assistant has no usable live transcript to ground an answer in yet. The answer is grounded only in the meeting’s captured speech so far, so two causes cover nearly every 422:
- Too early in the meeting. No finalized caption segments exist for the room yet, or the captured speech is under the minimum length the assistant requires before it will spend an answer. Speak — or let the meeting run — and try the question again once the first exchanges have been transcribed.
- Captions are off or not flowing. Live captions power both the room’s transcript surface and this assistant. Turn captions on for the room and verify the live transcript renders for participants (the voice gateway’s realtime caption edge has to be delivering); if the transcript surface itself stays empty for a speaking room, treat that as the incident, not the assistant’s 422 — escalate with the room id.
VIDEO_LIVE_ASSIST_LLM_MALFORMED (502) is different: the transcript loaded fine but the answer came back unparseable — retry the question once. The contract for /ask, and the summary counterpart (VIDEO_TRANSCRIPT_UNAVAILABLE on POST /api/v1/video/sessions/:id/summary), are in the Video API reference; all Video codes are in the Error Code Reference.
Verify / OTP
Which channels deliver an OTP, and which are factor channels?
Orbit Verify splits channels into two groups. Delivery channels (sms, whatsapp, email, voice, viber, telegram, rcs, flashcall) carry a code to the recipient over a carrier. Factor channels (totp, push, backup_code, sna, silent, magic_link) validate possession of a secret or device with no code delivered; factors are enrolled and verified through their own /verify/factors/*, /verify/push/*, and /verify/passkey/* endpoints. A factor value such as totp sent to POST /api/v1/verify/send is short-circuited with a 422 and points you to the matching factor endpoint. The full split and per-channel behaviour are on the Verify overview.
Why did my Verify send get refused with 422 VERIFY_LINE_TYPE_BLOCKED?
The Verify send runs one tenant-owned line-type guard before the wallet deducts and before any carrier is attempted, when your org’s line-type policy (landline_sms, landline_voice, voip_sms, voip_voice, each block / warn / off) says the recipient’s classified line type is not eligible for the channel you named. The guard reads the Number Lookup verdict for the destination: a landline (fixed-line) verdict blocks an SMS-factor send when your landline_sms slot is block, and a voip verdict blocks either channel when the matching slot is block. If the lookup’s line-type field itself answered per-field error or coming_soon while the send named voice, the guard refuses rather than gamble on an unknown line — and because it refuses pre-send, no wallet deduction happens and no OTP is dispatched. The response details name the verdict: line_type, channel, and policy_slot.
Fix it by changing the payload or moving factors — never replay the identical body:
- Pre-flight the line type with
POST /api/v1/numbers/lookuprequesting theline-typefield; send to a destination whose verdict is voice-capable / SMS-capable for the channel you want. - If the destination is fixed, switch factors: use
smsorwhatsappinstead ofvoicewhen the line can’t take a call, or raise the policy slot towarn(send proceeds, warning webhook fires) when a hard block is more than you need. - Loosen the tenant policy only deliberately — an owner can change a policy slot from
blocktooff/warnunder Settings → Security when a whole class of recipients should be allowed through; the default oflandline_sms: "warn"and everything elseoffis the starting point.
What’s the bulk-send limit for OTP sends?
POST /api/v1/verify/bulk accepts an array of up to 1,000 recipients that share one channel and optional verify profile. The HTTP status mirrors the batch outcome: 201 when every recipient was sent, 207 (Multi-Status) on partial success with one result row per recipient, and 400 when every recipient failed. Each recipient runs through the same send pipeline as a single /verify/send call. See Verify API reference → Bulk send.
Why did my co-browse session fail to start or leave the customer stuck?
Three classes of failure account for most stuck co-browses: the session never reachedactive — COBROWSE_NOT_ACTIVE on the join POST means the session wasn’t started (ask the visitor to start co-browsing from the chat widget), or the join body failed validation and the request was refused with 422; or the customer’s browser blocked the frame — a Content-Security-Policy / X-Frame-Options block, or an edge PoP dropping the data-channel socket; or a guided-control packet hit the 1 MB dispatch ceiling — the CUSTOM_TOOL_RESPONSE_TOO_LARGE (502) code from the Error Code Reference. Read engagement errors from the customer side’s activity feed and the conversations activity endpoint to pin the stage: either re-issue the join after a valid session exists, ask the visitor to adjust the frame policy, or trim the control packet under 1 MB. See the Co-browse API reference for the full workflow.
Why does my dialer conference dial-out leg time out while the SIP trunk is healthy?
A healthy trunk only covers the outbound-to-PSTN acceptance — the dial-out leg can still time out after the trunk pool accepted the call, because the leg between carrier acceptance and the conference join is decoupled. The carrier verdict fires as thevideo.dial_out.failed webhook — the softswitch declined the INVITE, the callee was busy/unreachable, or the ring timeout hit before answer. Diagnose by two routes: re-test with a direct batch-dispatch dial outside the conference session (POST /api/v1/dialer/campaigns/:id/batch-dispatch), or read the trunk-health endpoint /channels/voice/sip-trunks-troubleshooting — the ranked GET /api/v1/voice/sip-trunks/health rows and the failover walk tell you whether the route picked the next healthy trunk or the leg timed out mid-flight. If it keeps failing while trunk health is green, either raise the trunk’s capacity headroom or pull it out of the rotation until re-verified — the full mode/pacing matrix is on the Dialer API page.
Why did my agent end-to-end latency jump after morning content while the API answer is OK?
A200 OK on the agent API only covers the request-response gate — it does not cover the downstream queue pressure. When end-to-end latency climbs after a morning content spike (a new golden set, a larger prompt, or a corpus re-ground push), the agent waits on the cross-worker eval queue: voice eval runs sampled mid-peak, per-production-worker limits, and the LLM-judge groundedness queue piling up. The signal to check is the evaluation queue depth — GET /agents/voice-eval/runs?status=pending (filter by status=pending to find queued runs). A queue full of pending rows while completed rows lag signals the eval workers have stalled or a spike has outrun the worker pool: raise sample_percent back down or defer a re-ground batch until after the peak window. See the continuous production eval sampling page for the full workflow.
Why did my AI agent fail to join the conference, or refuse on a 409?
Three classes cover the AI-agent-conference join path: the room already carries an agent —POST /api/v1/voice/conferences/:id/ai-agent is refused with 409 CONF_AI_AGENT_ALREADY_ACTIVE and DELETE must clear the joining/active row before a re-add; the call addresses an agent that isn’t there — whisper or remove on a room with no joining/active row returns 404 CONF_AI_AGENT_NOT_FOUND; or the join succeeded but the leg never bridged — the attachment row on GET /api/v1/voice/conferences/:id/ai-agents sticks in joining or flips to error. That last class has no PSTN leg involved, so trunk health is not the story. Read the row’s metadata.error, remove it, re-add once, and escalate with the conference id if it recurs. The full cause table — and what not to try (re-join loops re-fire the 409) — is in Troubleshooting: conference lifecycle failures.
Why was my video room recording deemed degraded?
Thevideo.recording.degraded webhook (parity-RTC-R1, fired as a quality verdict alongside video.recording.completed) means the recording finalized but failed at least one QC check — the reason map includes file_size_min (zero_byte_file / truncated egress), duration_min (short_duration), mime_type_match (video_corrupt / audio_only mismatch), finalize_window, and duration_drift (the pipeline dropped the tail). The file itself is still attached; recording_url is set and the artefact is usable, but the quality gate stands. Start a fresh recording on a live room from POST /rooms/:id/recording/start — no attendees need re-inviting, the new file replaces the degraded entry in the artifact list. If a re-record keeps failing, re-run the scorer with POST /recordings/:id/qc/run and check the room’s recording_enabled policy against your minimum-duration setting before blaming the pipeline. See the Recordings page for the full workflow.
What does 429 VERIFY_RESEND_COOLDOWN actually mean?
VERIFY_RESEND_COOLDOWN (429) is the per-recipient OTP resend cooldown tripping — you asked to re-send a code to the same recipient before the cooldown window elapsed. Honour the Retry-After and wait, or treat it as a signal to stop spamming resend; it is distinct from the org-wide and per-key rate layers. Code expiry and resend mechanics are on the Verify overview; all error codes are in the Error Code Reference.
Why does my account get a 429 VERIFY_RESEND_COOLDOWN and how long until I can try again?
The 30-second resend cooldown is a fixed per-recipient window on every verification code send, and it applies to your tenant between keys and applications — it is not a per-key limit you can alter. The cooldown arms on /verify/send and re-checks on POST /api/v1/verify/:id/resend, so a send immediately followed by a resend trips it too. The response body tells you exactly how long to wait: details.retry_after_seconds carries the remaining seconds, details.cooldown_seconds the full 30-second window, and details.recipient_masked the masked destination. The window runs while the recipient exists and a send just left, so a string of cooldown rejects against one destination usually means a double-submit or a resend handler firing /send again instead of /resend — fix that at the client, not at the API.
Use the remaining seconds to drive your retry: read details.retry_after_seconds, disable the resend button, and count down until it expires before you call /verify/:id/resend again. Because the 30s cooldown qualifies the hourly cap, a 429 VERIFY_RESEND_COOLDOWN never consumes the recipient’s hourly budget. You can see the values Orbit uses per recipient on the Verify dashboard under Verify → Configuration → Profiles: the profile form carries the per-recipient hourly cap and the related rate-limit knobs, and org-level defaults are the fallback when a profile’s rateLimitPerHour is unset. The full send/resend mechanics are on the Verify overview and the cause table on Troubleshooting: verify OTP. All Verify error codes are listed in the Error Code Reference.
What do BINDING_REQUIRED / BINDING_MISMATCH mean on Verify?
A binding is the device/token association a factor run must carry across enroll → send → check. BINDING_REQUIRED fires when a binding-bearing route is called without its binding — typically POST /api/v1/verify/check on a verification that was minted with a PSD2 SCA sca_binding (amount, payee, transaction_id) that the check did not replay, or an SNA /device-bound send that omitted its device token. BINDING_MISMATCH fires when the check replays a token that does not match the one the send stored — a regenerated device identifier, a reformatted sca_binding, or a passkey ceremony on a different origin / rpId than the enrollment pinned. In both cases the gate refuses deterministically: retrying the same body returns the same code, so mint a fresh verification with a stable token, or replay the exact object the send captured. The runbook is Troubleshooting: Verify binding gates; the factor enrol and verify mechanics are in the Verify factor suite page.
What does a 429 on Verify mean?
A429 with RATE_LIMIT_EXCEEDED means one of three rate-limit layers tripped: the per-API-key limit, the org-wide per-recipient limit, or the per-recipient brute-force lockout that stops code-guessing attacks. Honour the Retry-After header when present and retry — don’t resend immediately. Profile-level velocity caps layer on top of the org-wide limit when configured. The layers, and every other verify error code, are mapped in the Verify error matrix and the Rate Limits guide.
How do code expiry and max attempts work?
A direct send expires after 600 seconds (10 minutes) by default; the TTL is not a send-body parameter — setexpirySeconds on a verify profile and pass its profile_id to change it, up to a 60-minute ceiling. max_attempts (1–10, default 3) caps how many /verify/check calls can guess a code, after which the verification fails and needs a fresh send. A /check that arrives after expiry returns 410 EXPIRED_TOKEN and a wrong code returns 422 with the remaining attempts. Both knobs, plus every other send/profile parameter, are documented on the Verify overview and in the verify profiles guide.
Deliverability
What does the email IP-warmup plan do automatically?
GET /api/v1/email/warmup-plan returns a computed day-by-day ramp: the daily send caps that grow a fresh sending IP or domain from a conservative day-1 volume (default 50/day) up to your target daily volume, at a default ~50%/day growth factor. Pair it with GET /api/v1/email/warmup-status, which reports which ramp day your account is on, today’s recommended cap, how much has already gone out, and the remaining headroom. Planning a bulk send? GET /api/v1/email/warmup-enforcement turns a requested batch size into an accept/defer decision against today’s live cap. All three endpoints are documented in the Email API reference.
What should I do while a sender or domain is still warming up?
Respect the daily cap: throttle or gate the send instead of pushing full volume — the warmup-enforcement endpoint exists to make that one authoritative accept/defer decision. The same ramp principle applies outside email: a newly provisioned long code, short code, or alphanumeric sender ID ramps volume gradually because carriers have no history to judge it by, exactly like a fresh IP on email. Whether email deliverability or messaging/voice sender reputation, the rule is to start with a small daily cap to engaged recipients with full SPF/DKIM/DMARC in place. The glossary has both definitions — IP Warmup for email, and Warmup for sender registration.How is warm-up different across an email IP, a phone number, and a sender ID?
All four are the same carrier-trust ramp applied to a different asset, but only the first three have a live per-asset dashboard:- Email IP — the three endpoints above (
warmup-plan,warmup-status,warmup-enforcement) compute the day-by-day IP ramp and gate bulk sends against today’s live cap. See Email API reference. - Sending domain (FROM domain) — domain reputation is DKIM/SPF-bound, so it is portable across IPs and does NOT reset when the sending IP rotates. The same three endpoints gate it separately from IP: a fresh domain on a warmed IP still starts from a conservative day-1 volume. See DNS drift troubleshooting.
- Phone number (10DLC long code, short code) — a four-phase ramp (
initial → ramp → steady → verified) with its own endpoints:GET /api/v1/numbers/:id/warming(livedaily_capvscurrent_day_count,warming_phase,trust_score) andGET /api/v1/numbers/:id/warming/progression(15-day ceiling forecast). Over-cap sends are refused pre-send with429 WARMING_QUOTA_EXCEEDED. The full flow is in the number warm-up guide. - Alphanumeric sender ID — no endpoints to poll: the ramp is manual. You hold volume low to engaged recipients and raise it over days, because mailbox providers and carriers have no history to score a fresh sender ID by.
Why was my SMS flagged with MESSAGING_TR_URL_STRIP_VIOLATION on a Türkiye destination?
Türkiye’s BTK Decision 2025/DK-YED/412 (effective 1 April 2026) blocks A2P SMS containing URLs when the sender is not registered domestically in Türkiye — the carrier strips the entire payload at delivery, so the recipient sees nothing while the send still reports submitted. Orbit detects this condition on the send path and stamps a non-blocking advisory on the message metadata as tr_url_strip_warning rather than refusing the request, but the historical 422 MESSAGING_TR_URL_STRIP_VIOLATION rejection can still surface on older integrations. Two workarounds, both tenant-owned: move the URL-bearing content onto MMS (the URL rule targets SMS; MMS payloads carry links differently), or register a domestic TR sender so the +90 traffic clears the BTK check — either way, drop links out of SMS bodies destined for Türkiye until the sender is registered. The code is listed in the Error Code Reference.
Why did my toll-free number stop sending with TFV_REQUIRED?
US carriers silently throttle toll-free A2P SMS from numbers that have not completed Toll-Free Verification (TFV), so the send gate refuses a US-bound SMS/MMS from your toll-free sender with 422 TFV_REQUIRED until its tfv_status reads approved. The recovery step is the TFV submission form on Settings → Compliance: submit it, then wait out the carrier review (typically 1–3 business days) while the status sits at pending. A rejected status means the previous submission failed — amend the use-case and sample messages and re-submit. The gate only applies to your own toll-free numbers sending to US recipients over SMS/MMS; other channels and destinations are unaffected. The code is listed in the Error Code Reference.
Why did my SMS bill for 2+ segments instead of 1?
SMS bills per segment, and two multipliers stack on a body you thought of as one message. First, single-part capacity: GSM-7 (the standard alphabet) fits 160 characters, but once a message goes multi-part every segment also carries a 7-byte user data header so handsets can reassemble it — per-segment capacity drops to 153, so a 161-character ASCII body bills as 2 segments, priced as two sends. Second, the encoding flip: one character outside the GSM-7 alphabet reclassifies the entire body as UCS-2 (Unicode), whose single-part limit is 70 characters and whose per-segment concatenated capacity is 67. The two effects combine: a 150-character ASCII blast is 1 GSM-7 segment; append one emoji and the whole body flips to UCS-2, the emoji alone counts as two UTF-16 units, and the 152-unit body bills asceil(152 / 67) = 3 segments — one glyph tripled the line item.
The characters that flip encoding are the ones GSM-7 has no slot for: emoji, non-Latin scripts (Cyrillic, CJK, Arabic, Greek letters outside the small GSM alphabet subset), plus smart punctuation and invisible zero-width characters pasted from a document. Orbit normalizes the accidental offenders — curly quotes to straight quotes, em dashes to hyphens, zero-width spaces stripped, decomposed accents recomposed — before counting, so paste-from-a-doc noise does not change your bill. A deliberate emoji has no GSM-7 substitute, so it still flips the whole body; and nothing bills past 10 segments (1,530 GSM-7 characters or 670 UCS-2 code units) — longer bodies arrive trimmed with a truncation flag.
To read the count billing actually used, check the same fields per-segment pricing multiplied against: the send response on POST /api/v1/messages/sms carries segments and encoding, and GET /api/v1/messages/{id} returns them on the stored record. To read the count before anything is billed, preflight with POST /api/v1/messages/estimate — pass channel: "sms", to, and body, and it returns the same encoding and segments plus a tenant-priced estimated_cost, while sending nothing. The full arithmetic — the capacity table, the normalization pipeline, and worked examples — is on SMS segments and encoding; the per-segment rate the count multiplies against is in the pricing and throughput reference and on the pricing page.
Why does my CDP ingest call cap out with CDP_PAYLOAD_TOO_LARGE?
The HMAC ingest surface (POST /cdp/v1/:ingest_id/{track,identify,page,screen,group,alias,batch}) and the developer event-ingestion endpoint refuse payloads larger than the inline byte ceiling CDP_INLINE_MAX_BYTES — 1,048,576 bytes (1 MiB) — with 413 CDP_PAYLOAD_TOO_LARGE (or EVENT_PAYLOAD_TOO_LARGE on the developer surface). Because signature verification depends on the exact bytes received, the platform refuses the bytes outright rather than truncating them. Shrink the payload below 1 MiB and retry: split a large /batch envelope into several calls, move bulk imports onto POST /api/v1/cdp/file-ingest (which accepts up to 200 rows per call, chunked by the caller), or trim oversized properties. The split is safe to retry — chunk the data, then re-send. Both codes are listed in the Error Code Reference.
AI Agents
How does the agent marketplace review, install, and payout flow work?
A builder (any organization) publishes a template — an agent, flow, or tool — and it moves through a review gate before anyone outside the builder’s organization can install it. The author flips the listing fromdraft to pending_review; a platform reviewer then approves it (→ published, which lists it in the public catalog) or rejects it back to draft with a written reason that lands on the author’s listing row. Authors can only move between draft and pending_review — the two verdict states belong to reviewers, and self-publishing returns a 403.
Installing is a one-time copy, not a live link: the clone lands inside your own tenant and becomes your resource, so the builder’s later edits never mutate your agent and there is no auto-upgrade path. Paid listings settle at install — the price is deducted from your wallet before the copy is created, and the builder’s organization is credited its net share (minus the platform fee, computed with the same rev-share math the preview endpoint reports). The charge and the payout either both complete or the install fails, so a failed install never leaves a charge behind; the builder payout is skipped for self-installs and for free or built-in templates.
A moderation takedown pulls a live listing out of the catalog for a recorded reason, but anything already installed keeps working — the copy in your tenant is independent of the listing. The full lifecycle is on the marketplace concept page, the operational walkthrough in Browse, install, and publish, and the pre-built agent templates in the Agent Marketplace reference; the endpoint contract lives in the Marketplace API reference.
What LLM models can I use?
Orbit is Anthropic-only for chat and reasoning — agent conversations and text generation run on Anthropic’s Claude models. Embeddings are the one exception, generated with OpenAI’stext-embedding-3-large. Tenants don’t connect their own model-provider keys. Supported models:
claude-opus-4-7— top-tier reasoning for complex agentsclaude-fable-5— vision-enabled (image and PDF input); the intended platform default, but currently export-suspended and unavailable for completionsclaude-sonnet-4-6— balanced quality / cost; the model a new agent is assigned at creation timeclaude-haiku-4-5-20251001— low-latency classification + cheap tasks
claude-sonnet-4-6 unless you select another model — that is the per-agent create-time default, which is distinct from the platform runtime default. claude-fable-5 is the intended platform runtime default once it returns from export suspension; until then the platform runtime default falls back to claude-opus-4-7. You can change an agent’s model at any time.
Can agents use external tools?
Yes. Define custom tools with JSON Schema parameters. The agent will invoke tools during conversations to look up data, take actions, or fetch information from your systems.Why did my MCP server registration return 422 INVALID_MCP_SERVER_URL?
The write-time SSRF guard rejected the URL — it must be public HTTPS end to end (no http://, no loopback/private/CGN addresses, no link-local cloud-metadata hosts, no internal-only hostname suffixes, and no DNS record that resolves to any of those). The 422 response carries a failure_code naming which class tripped; the OAuth variant INVALID_MCP_OAUTH_TOKEN_URL applies the same guard to the grant’s token_url, and a 409 MCP_SERVER_NAME_CONFLICT means the server name is already registered for that agent. The full runbook is Troubleshooting: MCP server registration rejected; all three codes are in the Error Code Reference.
Why did my A2A task fail or my discovery setting reject?
Discovery is an explicit per-agent opt-in, off by default: the dashboard A2A tab answers422 A2A_DISCOVERY_DISABLED until the agent’s discovery mode flips to public (or tenant), and an inbound task answered with a fixed 401 means the envelope’s HMAC X-A2A-Signature header was missing, malformed, stale, or signed with the wrong secret. A 502 A2A_PEER_ERROR on an outbound delegation means the peer’s agent errored the task — read the peer-error envelope in the task detail before retrying. The decision checklist for all of these lives in Troubleshooting: A2A federation discovery and peer tasks; the discovery modes and signing scheme are documented on the A2A federation model concept page.
Why did my agent version promotion return 422 PROMOTION_GATE_FAILED or RED_TEAM_GATE_FAILED?
Both are deliberate change-control gates on the promote call, not a general failure: the version was refused and the live config never flipped. PROMOTION_GATE_FAILED means the pinned eval suite (your golden sets) regressed against the candidate, a set could not run, or the aggregate pass rate fell below the gate’s min_pass_rate. RED_TEAM_GATE_FAILED means the candidate’s overall safety score fell under the gate’s min_safety_score floor, or it newly compromised a probe the pinned baseline had resisted. The 422 response body carries the full gate report — reasons, which golden sets or red-team categories failed, and the thresholds the decision was made against — and the same explanation appears inline in the dashboard under Agents → [agent] → Versions → Promotion gate. Read the report, fix the prompt or adjust the gate thresholds (GET|PUT /agents/:id/promotion-gate/settings and GET|PUT /agents/:id/red-team/gate-settings), then promote again. The full decode-and-recover runbook is Troubleshooting: promotion gate blocks a version.
What are guardrails?
Guardrails are rules that validate AI agent outputs before they reach end users. They can enforce:- No PII exposure
- No profanity or harmful content
- Brand voice consistency
- Factual accuracy checks
Why did my agent’s completion return 422 GUARDRAIL_VIOLATION?
That code name is a reserved guardrail-refusal code, listed as such in the Error Code Reference — reserved means the platform may return it from any guardrail surface rather than one single endpoint. The surface that currently fires such a refusal is the agent’s output validation: a policy violation on the generated reply (PII leakage beyond the allowed class, a missing required citation on a knowledge-grounded reply, a harmful-content finding, or a factual-inconsistency verdict when block_ungrounded_speech is enabled) blocks the reply before it is spoken or texted to the end user, and the assistant’s fallback message is used instead. Tune your guardrails and policies — thresholds, enabled classes — rather than retrying the same prompt. The per-agent and policy mechanics are on the guardrails pages (for example Guardrail effectiveness and Sensitive words guardrail).
What is Orby?
Orby is the operator assistant embedded in the Orbit dashboard — distinct from an AI agent, which faces your customers. It answers questions about your own workspace, orients operators (“where do I set quiet hours?”), and proposes data-changing actions (send a message, reassign a conversation, pause a campaign) behind an explicit approval card, so nothing executes until an operator confirms it. Orby accepts a dashboard session only — API-key calls return403 ORBY_SESSION_REQUIRED — and it inherits exactly the signed-in operator’s role, so it can reach nothing the operator couldn’t reach through the dashboard itself. See the Orby architecture concept, the in-dashboard workflow guide, and the Orby API reference.
Why does an Orby action sit at pending instead of executing?
A pending action is Orby’s approval gate, not a stuck call: a tool whose confirmation policy isalways — sends, reassignment, campaign state — writes a durable proposal with the exact arguments and stops there until an operator approves it, and a threshold policy pauses only above a dollar cut-off. Reads and drafts fire inline under the never policy; the proposal expires untouched after its TTL. The action-approvals model holds the transition rules and the durable event trail, and Orby tool approval triage covers the PENDING_ACTION_* refuses.
Integrations
Why is my Salesforce / HubSpot sync failing?
One of three codes lands on the failure —CRM_CONNECTION_MISSING (no active connection to the provider on your organization), CRM_AUTH_EXPIRED (the OAuth token the stored connection holds has expired or been revoked on the CRM side), or CRM_DISPATCH_FAILED (a queued CRM activity-log or CDP segment-sync item couldn’t dispatch — the provider endpoint rejected or timed out the outbound call). Decode which one the failed envelope carries before you touch anything: connection-class codes need one OAuth re-install from Settings → Integrations (Owner or Admin role required), and dispatch-class codes need the provider’s underlying 4xx/5xx read first — re-queueing blind just re-stamps the same failure. The full decode table, the per-leg locate step (agent CRM tool vs inbound contact resolution vs CDP POST /api/v1/cdp/crm-sync/run), and the do-not-retry rules are in Troubleshooting: CRM integration sync error codes; the saved-connection status endpoint (GET /api/v1/integrations/{id}/status) answers connected: true|false when you want the current membership, not the last failure.
Why does my CDP event get rejected with 422 TRACKING_PLAN_VIOLATION?
A tenant-authored tracking plan declares what _track, _page, and _screen events may carry, and one row of that plan is enforced strict for the event name you sent — so the ingest gate compared your payload against the declared properties_schema and refused the write. The refusal is deterministic: replaying the same payload returns the same 422 until either the payload or the plan changes, and the error’s details.violations list names each mismatched property (missing_required, type_mismatch, enum_mismatch, extra_property) with the expected and actual shape so you can fix the sender, not guess at it.
Three fix paths, and they do not compete — pick per send:
- Fix the payload to match the plan. If the property is genuinely wrong (a string where the plan declares a number, a required field absent), correct the sender and re-send the event; the schema already encoded the correct shape, so matching it is the intended outcome.
- Update the plan row’s schema to accept the new shape. When the producer has deliberately started sending a new or different property, PATCH the plan row to reflect it — the plan follows the producer, not the other way around.
- Relax enforcement for that event. If you want the mismatch recorded but not refused, change the plan row’s
enforcementfromstrictback tosoft(or add asoftplan row) — violations still land in the violations feed withaccepted: true, so nothing is silently dropped, but ingest stops refusing.
unknown_event) never 422s, even in strict mode — the platform permanently accepts catalogless events rather than dropping a send whose event name simply wasn’t declared yet. The full declare-validate-triage loop, the soft-vs-strict decision, and the version-control pattern are on CDP tracking plan; the violation feed is GET /api/v1/cdp/tracking-plan/violations, and the code is in the Error Code Reference under CDP.
Billing
What pricing model does Orbit use?
Orbit is pay-as-you-go with no monthly subscription tiers. You pre-load credits; outbound usage (messages, voice minutes, AI agent invocations) deducts at per-channel rates. There are no Starter, Growth, or Business subscription plans — only PAYG and Enterprise (negotiated rate card).What is the Enterprise plan?
Enterprise customers get negotiated per-channel rate cards, volume discounts, dedicated support, SLA guarantees, and a custom contract. Contact sales@devotel.io for details.How do prepaid credits work?
Credits are purchased in advance via the dashboard or Stripe Checkout and consumed for messages, voice minutes, and agent invocations. Credits never expire. Enable auto-top-up in Settings → Billing → Auto top-up to prevent service interruption.Is there a free tier?
There is no free tier and no automatic trial credit. Every account is pay-as-you-go: after KYC approval, you fund your balance via a paid top-up before sending live traffic. Use the sandbox environment to test the platform for free before you go live — see Go Live. See the Pricing page for current rates.My sends stopped with a spend-cap refusal. Which cap fired?
Spend-cap refusals (SMS_DAILY_SPEND_CAP, CHANNEL_DAILY_SPEND_CAP, VOICE_DAILY_SPEND_CAP, CAMPAIGN_VOICE_SPEND_CAP_REACHED) are tenant-owned daily or per-campaign ceilings that fire before the send is dispatched. Match the returned code to the surface — a daily-spend alert rule on Billing → Alerts, or the voice_spend_cap_cents field on a campaign — and raise or clear the ceiling. A wallet 402 (INSUFFICIENT_BALANCE) means the account is out of funds; a warming 429 on a new 10DLC number means the carrier-side ramp is throttling. The spend-cap troubleshooting page walks the decision tree, and Billing documents the re-arm surfaces.
Why did my auto-top-up fail and my sends stop with INSUFFICIENT_BALANCE?
Auto-top-up is on, but the refill failed, so the wallet ran out and sends now refuse with402 INSUFFICIENT_BALANCE. The rule records the failing reason on the config itself — GET /api/v1/billing/auto-topup returns a last_error field after every attempt (Owner or Admin role required), so check there first. Two failure modes account for nearly every stalled refill:
AUTO_TOPUP_PAYMENT_FAILED— the off-session Stripe PaymentIntent failed. The most common causes are a declined card, no default payment method on file (codespayment_intent_requires_payment_methodandno_default_payment_method), and a card that needs authentication before it charges off-session (payment_intent_requires_action).AUTO_TOPUP_MONTHLY_CAP_REACHED— themax_monthly_minorcap on your rule has already been hit this calendar month, so the recharge is refused even with a valid card. The cap resets on the first of the month.
- Update the saved card or complete one interactive top-up under Settings → Billing — one interactive checkout also clears a card’s authentication requirement for future off-session refills.
- Raise the monthly cap on your auto-top-up rule if it was the blocker (
PUT /api/v1/billing/auto-topup) — the en-route validator requires it to be at least one full refill in size. - Top up manually now to unblock sends — the rule retries automatically on the next scheduler tick, but it only refills on the threshold; it never catches you up after a dry run.
GET /api/v1/billing/balance — the send retries cleanly only once the top-up credit posts. The full recovery matrix (all four last_error causes) is in Configure wallet auto-top-up, and both codes are listed in the Error Code Reference.
Webhooks
How do I verify webhook signatures?
Every webhook includes anX-Orbit-Signature header (the canonical HMAC-SHA256 signature). For backwards compatibility, a legacy X-Devotel-Signature header is also included. Verify either signature by computing the HMAC of the raw request body using your webhook secret. See the Webhook Security guide for verification examples and code samples in multiple languages.
Signature verification fails on my webhook endpoint — why?
Your endpoint returns 200 on unverified deliveries but rejects the recomputed HMAC, or a local verifier can’t match Orbit’s signed payload. The signed string is<t>.<raw_body> — HMAC-SHA256 (see the glossary term) of the Unix timestamp and the exact raw request bytes, keyed by the endpoint’s whsec_ secret. Work the five failure classes:
- Parsed body, not raw — a framework body-parser deserializes JSON before verification, and re-serialization changes key order and whitespace. Verify against the raw bytes (
req.rawBody, or disable the parser on the webhook route). - Wrong header — prefer the canonical
X-Orbit-Signature; a rotation grace window addsX-Orbit-Signature-Next(previous secret), and the legacyX-Devotel-Signaturecan carry twov1=candidates — accept if any candidate matches. - Clock skew — a
t=timestamp outside the 5-minute replay window is rejected before the HMAC check (STALE_WEBHOOK); sync with NTP and compute age in UTC seconds. - Wrong secret — a sandbox
whsec_on live traffic (or vice versa), or the previous value after a rotation — use the endpoint’s signing secret, never your tenant API key, and never the masked preview fromGET /api/v1/webhooks/{id}. - Replaying an old delivery — a captured payload re-POSTed after its timestamp window closes fails the same check; let Orbit retry instead.
crypto.timingSafeEqual:
What happens if my webhook endpoint is down?
Orbit retries failed webhook deliveries on an exponential backoff schedule: up to 9 retries with a 30-second base delay (doubling with each attempt), spanning roughly 4.3 hours end-to-end (plus up to 20% jitter on each delay). After all attempts are exhausted, the event is sent to a dead letter queue. You can review failed deliveries in the dashboard. See Webhook Events for the full retry semantics.Why does my webhook endpoint show ‘auto-disabled’ even though it only returned a few 4xx/5xx responses?
Auto-disable is a protective halt, not a fault, and it engages on one of two paths: 50 consecutive delivery failures, or a proven-dead permanent401/403/404/410 response that counts as dead on the first occurrence without burning through the threshold. The ~4.3-hour retry window of ~10 attempts (exponential backoff with up to ~20% jitter) still runs for whatever failures happened before the disable, so a healthy integration trips no protection and an endpoint that is already auto-disabled stops accepting new deliveries outright — Orbit skips the in-flight retry pipeline and sends every new event straight to the dead-letter queue. To recover, list the dead-lettered events with GET /api/v1/webhooks/dlq, then replay each one with POST /api/v1/webhooks/dlq/{delivery_id}/replay once the receiver is fixed. Your endpoint returns to live serving as soon as delivery succeeds again. See the DLQ glossary entry, the Webhook Security guide, and the failed-deliveries troubleshooting runbook for the disable/replay/recovery walkthrough.
Can webhook events arrive out of order, and how do I stop a later event from being overwritten?
Yes — Orbit’s delivery contract is at-least-once with no ordering guarantee, so a retry or a fast first attempt can delivermessage.delivered before message.sent. Arrival order is meaningless; the envelope created_at timestamp is the only sorting key. Supersede per entity — apply each event with an update guarded by created_at so an older event lands as a no-op against newer state, and serialize consumers per entity key when concurrency would otherwise race two applies. Grouped with different failure machinery from duplicates — dedup on body.id first (duplicate events troubleshooting), then ordering takes over. The full symptom table, remedies, and a reordered-pair fixture are on Webhook ordering and fan-out; the delivery model itself is Webhook delivery semantics.
Can I subscribe to specific event types?
Yes. When creating a webhook, specify the events you want to receive:["*"] to subscribe to all events.
Why is my WhatsApp migration or template change silent even though I subscribed to webhooks?
Two layers to check. First, subscription scope: template lifecycle events (whatsapp.template.submitted / approved / rejected / auto_paused, quality_update) are distinct event types — a subscription to message.* events never notifies on a template review or post-migration quality change. Add them explicitly on the webhook, or subscribe to ["*"]. Second, delivery inspection: when an event does fire but your handler stays quiet, read GET /api/v1/webhooks/{id}/deliveries — duplicate deliveries of one event_id are the at-least-once model, so dedupe on body.id. See Duplicate webhook events and consumer-side dedup and failed-deliveries troubleshooting; event names in the Webhook Events reference.
Can I only have 10 webhook endpoints per tenant?
Yes — ten webhook endpoints is the hard per-tenant cap.POST /api/v1/webhooks refuses the eleventh with 422 WEBHOOK_ENDPOINT_CAP_REACHED, and the error details report limit and current so you know where you stand. The fix is almost never “more endpoints”: consolidate by subscribing one endpoint to many event types (or ["*"]), or delete an endpoint you no longer need (DELETE /api/v1/webhooks/{id}) and re-register. The full error-code table and the delete-and-consolidate walkthrough are on Troubleshooting: webhook endpoint creation errors.
Are sandbox endpoints counted separately from live ones?
No — the cap is per tenant, not per environment. Sandbox and live endpoints draw from the same ten-endpoint pool, so keep test registrations lean for the same reason: prefer one endpoint subscribed to the event types you exercise.GET /api/v1/webhooks lists what your tenant currently holds across both environments.
How do I raise the webhook endpoint cap?
Open a support ticket (support@devotel.io) and we can raise the limit for your tenant out-of-band — there is no self-serve setting, and deliberately so: a subscription-wide endpoint is almost always the better answer than a second destination. Most tenants asking for more endpoints actually want to consolidate the ones they have.SDKs
Which SDKs are available?
The Node, Web, and Python SDKs are live on npm/PyPI today. Go, Java, PHP, Ruby, and .NET are still on the roadmap — until each ships, call the REST API directly (
curl, fetch, httpx, or any HTTP client works). Install coordinates live on the SDK index.
Do I need an SDK to use Orbit?
No. The Orbit API is a standard REST API. The Node and Web SDKs will be convenience wrappers once published; the same endpoints are reachable from any HTTP client today.Compliance
What does the emergency stop do on my whole organization?
The emergency stop is an org-wide, owner/admin-only kill switch that halts every outbound dispatch path — SMS, MMS, voice origination, dialer campaign activation — with a fail-closed403 ORG_COMPLIANCE_EMERGENCY_STOP before provider dispatch, so no balance is debited on blocked sends. Transactional Verify/OTP and email are deliberately ungated because they run separate delivery paths; lift the switch with a second call when the incident resolves. The Emergency Stop spec holds the flag semantics and recovery path, and Run an emergency org halt is the operational guide.
How do I run a fraud alert end to end?
Open the triage list (GET /api/v1/compliance/fraud-reviews, sorted critical-first with an actionable flag from your thresholds), read the single alert, then acknowledge it (ack) as a resolution hint or dismiss it (dismiss) — writes are owner/admin. Re-raise thresholds when actionable misfires, or export the chargeback-evidence CSV; the list → open → acknowledge/dismiss loop page walks it end to end on top of the Fraud Shield payload reference.
Why can’t I edit my compliance profile?
Once a compliance profile leaves the draft stage, its fields are frozen for accuracy of the carrier-side review — onlydraft, rejected, and partially_rejected profiles stay editable. Editing a profile that is in pending_review, approved, or expired returns 409 COMPLIANCE_PROFILE_LOCKED. To change it, clone the profile into a fresh, editable draft with POST /api/v1/compliance/compliance-profiles/:id/clone (it copies your fields and document attachments into a new profile named … (copy)), edit that draft, and re-submit it. Attach-and-buy flows that depend on re-verified numbers then re-attach the fresh profile with POST /api/v1/numbers/:id/attach-compliance-profile.
Why did my profile submit return COMPLIANCE_PROFILE_INCOMPLETE?
422 COMPLIANCE_PROFILE_INCOMPLETE is a pre-submit validation gate: the profile is missing required data fields or document roles for its use-case and country (for example the business address, contact details, or regulatory documents the carrier mandates). The 422 response body from POST /api/v1/compliance/compliance-profiles/:id/submit lists what is missing in the details.missing array — fill those fields or attach the marked documents, then re-submit. The call is safe to retry once the profile is complete because incomplete submissions are refused before any carrier fan-out. Until the profile passes this gate, every purchase at a number regulated to it stays refused with 422 COMPLIANCE_PROFILE_NOT_APPROVED on the purchase endpoint.
Why is my number stuck at pending_compliance after the profile submitted?
Submitting the profile is step one; the number only unblocks once a carrier approves the profile and you attach it. Attach an approved profile with POST /api/v1/numbers/:id/attach-compliance-profile; if the grace window lapses first, the auto-release sweep refunds the captured monthly cost. The post-attach pending state (per status and per surface, including a Sender ID) is walked to its fix on Troubleshoot a pending number or Sender ID.
How does Orbit’s compliance posture work — enabled toggles that don’t block, fail-open versus fail-closed controls, approval lead times?
Compliance questions sit on their own page: Compliance Posture FAQ answers who owns the gates (tenant-owned, default open), which controls fail open versus closed, why an enabled toggle may not be blocking yet, and which approvals carry external lead time. The full map is on Your Tenant Compliance Posture: The Toggle Map.Why did my SMS send refuse with 403 MESSAGING_TCPA_KNOWN_LITIGATOR?
The recipient’s phone number matched the TCPA known-litigator screen — a check that flags numbers associated with TCPA-litigation risk — and your send to a matched number is refused pre-send until you hold verifiable consent for it. The screen runs on SMS/MMS only and deliberately fails open: a screening outage never blocks your sends. It is also an opt-in tenant control — it only applies once you enable tcpa_check_enabled in your organization’s settings (it ships off by default because it is a US-TCPA construct; traffic that never touches +1 recipients gets nothing from the check — see Compliance posture FAQ).
Resolve a blocked recipient one of two ways, both on your side:
- Record the consent proof — if the recipient genuinely opted in, file the record through
POST /api/v1/compliance/consent(or Contacts → Consent in the dashboard); the gate then allows the send and writes an audit entry for post-hoc review, so the refusal clears. - Remove the recipient from the campaign — a matched number with no consent record on file stays blocked, and no retry changes that outcome. Legitimate, recurring traffic (clinics, service reminders) usually resolves this by collecting written consent at intake; a bulk import pre-flags matched recipients once per import so the group refuses consistently rather than one-by-one.
details.to is truncated) and reports which source matched (numeracle, seed, cache, manual_override, or the pre-flagged contact_flag) plus a 0–100 risk score where one exists. The code is listed in the Error Code Reference.
Why did my GDPR erasure status check return 404 ERASURE_REQUEST_NOT_FOUND?
Every erasure request is filed through POST /api/v1/contacts/:id/gdpr/erasure-request (or the org-level POST /api/v1/contacts/gdpr/erasure-request), which returns the request’s tenant-scoped id (for example num_8fb2d1b7c00c4ec9a1d3f5e7b9c1d3). Any read you do afterwards — GET /api/v1/cdp/erasure/{erasureId}/propagate, the certificate fetch, or a propagation POST — must pass exactly that id. 404 ERASURE_REQUEST_NOT_FOUND means the id does not resolve to a row in your organization’s erasure ledger, and the two causes are distinct:
- A malformed or mistyped id. If you shortened or copied only part of the id, or the id is a
contact_idrather than the request id the original POST returned, the lookup misses. Re-list the requests withGET /api/v1/compliance/dsar/erasure-requestsand copy theidfield verbatim — the list endpoint shows the exact id per row alongside its lifecyclestatus. - A cross-tenant lookup. A request filed under a different organization never resolves here — each organization sees only its own erasure ledger. If you hold a valid id from another workspace, the same id returns 404 (not 403) on the wrong tenant, because an unresolvable id is indistinguishable from a foreign id. Verify you are calling with the API key for the organization that filed the request.
409 ERASURE_COOLING_OFF_ACTIVE (see the ERASURE_COOLING_OFF_ACTIVE entry under “Data residency & privacy” above) whenever a prior request exists. The erasure lifecycle, propagation trail, and certificate endpoints are documented on the compliance endpoints reference and the DSAR runbook.
Why does my consent propagation endpoint return 404 CONSENT_CONTACT_NOT_FOUND?
POST /api/v1/cdp/consent/{contactId}/{channel}/propagate-revocation pushes a contact’s opt-out OUT to your connected destinations, and it needs the contact’s identifiers (email, phone, external id) to do that mapping. 404 CONSENT_CONTACT_NOT_FOUND means the contactId in the path did not resolve to a row in your organization’s contacts table — so there is nothing to resolve identifiers from. The two mismatches behind it:
- Wrong scope. The contact lives in a different organization than the API key you are calling with. Consent records are per-organization, so a foreign (but real) contact id returns 404 rather than 403 — verify the key before anything else.
- Wrong id shape. A
contactIdhere is the contacts-table id (returned byGET /api/v1/contactsandGET /api/v1/contacts/{id}), not an email, phone number, orexternal_id. If you are holding an email or phone, resolve it to a contact id first withGET /api/v1/contacts?email=...(or the equivalent lookup), then call the propagate route with that id.
GET /api/v1/contacts/{id} before any consent-propagation POST, and only propagate when that read returns the row. When the contact is found and the propagation route instead returns 409 CONSENT_NOT_REVOKED, the gate is telling you the contact has no active opt-out on record for that channel — record the revocation first, then propagate. The endpoint contract is on the CDP endpoints reference, and the consent ledger model behind it is on Consent and suppression.
CX Analytics
What is CSAT and where does it surface?
CSAT (Customer Satisfaction score) is a 1–5 rating a caller gives about a single interaction. Orbit captures it two ways: as a post-call IVR survey after a voice call ends, and as an API-distributed survey template you send on SMS, WhatsApp, or email. Per-call aggregates come back onGET /api/v1/voice/csat/surveys/{id}/analytics and the per-survey rollup on GET /api/v1/surveys/{id}/results; a submission below your detractor threshold fires the conversation.csat_detractor webhook (post-call IVR surveys fire survey.csat.response_recorded) so a workflow can reopen the conversation for recovery (see Webhook Events). The full mechanics are in Post-Call Surveys (CSAT & NPS) and the Voice-of-Customer guide.
What is NPS and which endpoints expose it?
NPS (Net Promoter Score) is the long-running-loyalty metric, scored 0–10 and computed as promoters (9–10) minus detractors (0–6) on a −100 to +100 scale. Orbit exposes it exactly like CSAT: as a post-call IVR survey and as an API-distributed survey. Read the per-survey NPS analytics onGET /api/v1/voice/nps/surveys/{id}/analytics (promoters/passives/detractors plus a daily trend) — on an API-distributed nps-type survey template, GET /api/v1/surveys/{id}/results returns the same nps_score, and GET/PATCH /api/v1/voice/nps/surveys/{id}/config manages the attached survey configuration. Individually scored detractor responses fire the survey.nps.response_recorded webhook (see Webhook Events). The end-to-end template → send → results → benchmark loop is in Surveys end to end, the customer survey guide covers setup, and the Surveys API reference covers the response contract.
What counts as abandonment in queue SLA?
An abandonment is a caller who hung up before reaching an agent. Orbit splits the counter so you don’t confuse signal with noise:short_abandons (callers who dropped within the queue’s short-abandon window, treated as mis-dials and excluded from the service-level denominator) and abandoned_after_threshold (callers who waited past the queue’s target_service_level_seconds and then gave up — the SL-window misses supervisors actually act on). Live counts come back on GET /api/v1/voice/queues/{id}/stats (abandoned_24h), and the full abandonment breakdown per interval on GET /api/v1/voice/queues/{id}/analytics. The queue-analytics model and per-metric semantics are on Inbound queue SLA forecast + virtual callback gate and the voice queues guide; the alert/escalation rules on top of the same metrics are on Queue SLA escalation policies.
What does a queue SLA forecast callback actually measure?
It projects the wait a new inbound caller would accrue —waiting callers × recent average handle time ÷ available agents — against the queue’s own target_service_level_seconds (5–300, default 20), configured on PUT /api/v1/voice/queues/{id} alongside the queue’s slaCallbackPolicy. When the forecast (or a head-of-line check saying the oldest waiter already crossed the target) says the queue breaches, the policy decides the outcome: suggest flags the supervisor surface while the caller joins normally, and block diverts the caller straight into the press-1 virtual-callback consent flow with their position saved, while the queue-entry call answers 503 with slaCallbackBlocked: true. The forecast uses live queue inputs, so the wallboard verdict and the entry gate cannot drift. Full mechanics: Inbound queue SLA forecast + virtual callback gate; the sibling advisory forecast for callers already waiting is on Forecasted pre-suggestion + callback.
When does a voicemail box trigger?
A voicemail route is attached to a DID (or a queue’s fallback chain) and fires when the route resolves to it — typically because the queue or ring group nobody answered, or because an inbound route is configured asvoicemail / dispatch_to_voicemail_box directly rather than when a caller simply hangs up. The voicemail type captures one caller’s message with optional notification to up to five recipients; dispatch_to_voicemail_box drops the caller into a shared department mailbox (boxId) the whole team owns and lists via GET /api/v1/voice/voicemail-boxes, retrievable with full-read fencing and MWI. You create the box and attach a greeting by upload or text-to-speech, and a queue’s fallback chain (queue → ring group → voicemail) treats the mailbox as the terminal hop. Per-type configs, notification recipients, and the routing fallback order live in the voicemail boxes guide and Inbound number routing.
Which built-in CDP predictive models does Orbit ship?
Four server-owned model keys, listed byGET /api/v1/cdp/predictive-models with each model’s kind and output unit:
Scores become segments through the activation flow — the full walkthrough (train → score → activate) is in the CDP predictive models guide. The same guide covers the per-model feature set (six first-party behavioral signals like
recency_days and events_30d) on its catalog section. For score→segment specifics per model, see the CDP API reference.
WFM, Quality & Team Chat
What is WFM, and do I need a separate workforce-management product?
No — workforce management is a shipped domain inside Orbit, not a third-party integration. The whole surface is mounted under/api/v1/wfm and answers the four contact-center questions: how much work is coming in (forecasts), when each agent should work (schedules and assignments), did the day go as planned (adherence), and how to rebalance when reality drifts (intraday staffing). The planning side runs from GET /api/v1/wfm/forecasts — per-channel, per-interval required-agent rows, refreshed by POST /api/v1/wfm/forecasts/recompute and measured for trust with GET /api/v1/wfm/forecasts/accuracy — through POST /api/v1/wfm/schedules/generate (draft) → POST /api/v1/wfm/schedules/generate/commit (publish). The during-the-day side reads required vs. scheduled headcount on GET /api/v1/wfm/intraday/staffing and fixes the drift with POST /api/v1/wfm/rebalancing/recommend → POST /api/v1/wfm/rebalancing/apply. The model-map for all six domains is The WFM model; dash-following on the workflow side is in WFM intraday adherence and requests.
How do I run QA autoscore against recent queue calls?
Call quality autoscore is opt-in per tenant, and the readiness gates are readable before you enable it. The auto-QA readiness guide pre-flights the whole pipeline: the Generation switch gate checks Settings → AI Auto-QA is on, the Active scorecard form gate checks at least one evaluation form is active, and the Coverage bar shows the share of eligible calls (agent-attributed, transcribed) actually scored over the trailing three days. The same snapshot is queryable over the API onGET /api/v1/quality/auto-qa-readiness, and the coverage/flag-share rollup feeds GET /api/v1/quality/autoscore-coverage. Setup itself is one toggle plus one threshold, documented in AI Auto-QA configuration: enable qa_autoscore under Settings → AI Auto-QA, and the once-a-minute sweep scores every completed agent-handled call’s transcript against your most recently updated active evaluation form, writing a qa_evaluations row with auto_scored: true and flagged_for_review set when the score falls below your flag threshold (default 70). Reviewers pick up flagged rows through the same GET /api/v1/quality/evaluations ledger as manual scores; the scorecard authoring endpoint is POST /api/v1/quality/evaluation-forms (Quality evaluations).
When do I use Team Chat versus an inbox thread?
Use Team Chat for intra-team coordination that should never touch a customer; use Inbox for customer-facing threads. Team Chat (mounted at/api/v1/team-chat) is internal messaging for the operators running your workspace — channels, DMs, reactions, presence, and huddles (quick talk-it-through rooms started from a channel or DM). Nothing posted there reaches a customer, and no outbound customer message flows through it — that separation is the point. Inbox, by contrast, is the customer-facing ticket/thread surface where a reply goes out to the requester. The workflow rubric: on-call handoffs, deploy heads-ups, and quick huddles while triaging an incident belong in a team-chat channel (e.g. on-call-platform); anything the customer should read belongs in Inbox. The operator walkthrough is Team Chat, the concept model is Team chat model, and the endpoint contract is Team Chat API reference.
Which WFM levers require QA setup first?
Only the coaching/evaluation-flavored ones — not the WFM core. WFM planning and adherence (forecasts, schedules, assignments, staffing, rebalancing) stand on their own. Where QA setup becomes a precondition is wherever the WFM surface funnels into evaluation- or coaching-shaped output: the QA autoscore pipeline needs an active evaluation form (authored in Quality → Evaluation forms orPOST /api/v1/quality/evaluation-forms) before the sweep can score, and the auto-QA readiness card names that gate explicitly (QA autoscore readiness). Persona/evaluation leads and coaching cards read from those auto-scored rows, so enable AI Auto-QA and activate a form before expecting coaching-driven WFM follow-ups to fire. Plain WFM drift repair — rebalancing recommendations, intraday adherence, open-shift bidding — does not consult QA at all.
Can I silence a forecast spike with one rebalance?
One rebalance call answers today’s drift; it does not rewrite the forecast itself. The intraday loop works like this: the 30-minute reforecast keepswfm_forecasts.required_agents live, POST /api/v1/wfm/rebalancing/recommend compares that live requirement per (channel, interval) against the scheduled headcount you pass (or the latest forecast row when you omit it), and returns three levers in cost order — MOVE (re-route a cross-trained agent from an over-staffed channel to an under-staffed one, zero headcount), ADD (pull-in/overtime/open-shift for a residual deficit), RELEASE (VTO/early release for a residual surplus). POST /api/v1/wfm/rebalancing/apply then extends or trims assignments to enact them. If the spike is a genuine traffic shift rather than a one-off blip, the durable fix is POST /api/v1/wfm/forecasts/recompute (rebuild the forecast from current history) followed by a schedule regenerate — rebalance buys you the intraday relief while you do that. The supervisor-side workflow lives in WFM intraday adherence and requests; forecast accuracy (GET /api/v1/wfm/forecasts/accuracy) tells you how much to trust the next recompute.
Troubleshooting and runbooks
Why can’t I find my error on the troubleshooting hub?
The hub indexes runbooks by the surface they work on — messaging, voice, video, webhooks, Verify, billing, channels — and a newly shipped runbook is routed into the matching accordion even when the page itself went live first. If the hub still doesn’t list the runbook you need, three fallback paths reach it:- The Error Code Reference. Read the
codeon the error envelope your API call returned and match it there first — the reference is complete, and most codes link their dedicated runbook directly. - The sidebar. Every runbook is wired into the sidebar navigation the moment it ships, so expand the Troubleshooting group and scan the page titles — the page is reachable there whether or not the hub already routed it.
- The “From an error code to a runbook” table on the hub. At the bottom of the hub page, an accordion maps error-code classes to runbooks and falls back to the Error Code Reference for anything without a dedicated page — use it when you know the envelope’s
codebut not the runbook’s name.
When should I use the troubleshooting hub versus the FAQ?
Three waypoints sit between a symptom and a fix — the hub page, the FAQ, and the individual runbooks — and each does a different job:- The Troubleshooting hub is the entry point. Every runbook Orbit ships lives in one of its sections, grouped by the surface it works on — messaging, voice, video, webhooks, Verify, billing, channels, and the rest. Start at the hub when you know the symptom (“a message stuck in
queued”, “no delivery receipt”, “an inbound route that never arrives”) but not which runbook answers it. - The FAQ answers one specific question at a time — a code meaning, a limit value, a behavioural quirk — and most answers hand off to a runbook or a reference page. Use it when you already know exactly what you are asking.
- A runbook is the destination. Once triage has picked a page, that page follows the same shape every time: a cause table that maps symptoms to fixes, a full-error sample you can paste in a ticket, a decision checklist, the fixes you should NOT try, and when to escalate.
queued— the message has not been handed to a provider yet; the hold is pre-delivery. See Troubleshooting: message stuck in queued.failedorundelivered— a terminal non-delivered outcome. See Troubleshooting: message undelivered or failed.sentbut no receipt — the provider accepted the message, but adelivered/undeliveredoutcome never lands. See Troubleshooting: message sent but no delivery receipt and, when the receipt itself needs recovering, Recover a failed or late DLR.- Channel-specific gates — sender identity, templates, compliance, or inbound routing — branch to the channel section on the hub only after the status above is known.
code — match the code against the Error Code Reference and skip the whole entry-point question.
Support
How do I contact support?
- Email: support@devotel.io
- Dashboard: In-app chat via the help widget
- Enterprise: Dedicated Slack channel and account manager
Support history FAQ
Six answers about the dashboard’s Support → Timeline page — the reader over your organization’s own support history. The full walkthrough is the Support history guide. Who can see the Support history? Any authenticated member of your organization whose role includes the surface. The page re-reads the same org-scoped inbox ticket endpoints the operator queue uses, so every request your organization has opened appears to every member who can reach it. What does a restricted role see? An access-restricted notice instead of the list. Dashboard section restriction is a tenant-owned control — an owner or admin adjusts the member’s role to include the surface. How do I reply on a past request? The Continue this conversation button on the expanded thread deep-links to the ticket-comments composer for that request, where your reply is composed and sent under your own name. A member whose role is read-only can read the thread but cannot answer from either surface. Can I download attachments? Yes — files shared during the conversation appear under Attachments on the expanded thread as read-only downloads. Internal agent notes and their files are excluded; the page shows the public thread only. How do I export the history?GET /api/v1/inbox/tickets/export downloads the organization’s request list as CSV or JSON (with include_comments=true to inline the public threads), and the list endpoint’s filters (status, priority, opened_after, …) narrow the slice. Overflow past the server cap is flagged with truncated: true so you can narrow and pull the remainder.
How far back does the history reach?
Nothing is removed — a resolved request stays readable. The page loads the 100 most recent requests first; older history is preserved, reachable through support or the export endpoint.
Where can I check service status?
Visit status.orbit.devotel.io for real-time platform status, incident history, and maintenance schedules.How do I report a security vulnerability?
Email security@devotel.io. We respond within 24 hours and follow responsible disclosure practices.Insights & Analytics
Why does my SMS click-through show as 0 after a sent campaign?
The click-through counter on SMS click-through only moves for sends that carried a tracked short link — a plain-text URL in the body cannot emit a click event, so an untracked campaign correctly showsTracked: 0 and a — or 0 CTR. Tracked-link minting happens three ways, in order: an explicit metadata.shorten_urls on the send payload, the tenant’s SMS auto-shorten channel setting (default ON, rewrites URLs over 30 characters when the minted link is strictly shorter), and the WhatsApp auto-track counterpart. If minting failed silently (fire-and-forget — the original URL ships untouched), the send lowers Tracked rather than failing the message. The one-line diagnosis of where the counter lives: if unattributed is your biggest bucket, stamp campaign_id at send time or fix queue resolution on your messaging service — the fix is upstream in the send payload, not on the panel. A compact summary embedded at Messages → SMS messages shows a fixed 7-day window; open Insights → SMS click-throughs for the 24h/7d/30d control. The endpoint behind it all is GET /api/v1/analytics/sms-click-through.
When does an abandoned call count toward my queue SLA forecast?
A hang-up only enters the forecast’s denominator once the caller has waited past the queue’starget_service_level_seconds (the short-abandon window filter drops sub-threshold misdials from the service-level math). The forecasted wait is waiting callers × recent average handle time ÷ available agents, live inputs — so the wallboard verdict and the entry gate cannot drift. When the forecast breaches the target, the queue’s slaCallbackPolicy decides: suggest flags the supervisor surface, block diverts the caller to the press-1 virtual-callback consent flow and the queue-entry call answers 503 with slaCallbackBlocked: true. The per-interval counter lives on GET /api/v1/voice/queues/{id}/stats (abandoned_24h); the full abandonment breakdown on GET /api/v1/voice/queues/{id}/analytics. Mechanism: Inbound queue SLA forecast + virtual callback gate.
How do I read my squad’s routing numbers?
The glossary’s Squad (Agent Squad) entry defines the construct: a classifier front door, 2–6 specialist members, optional fallback and daily cost cap. Its analytics question — “is the squad routing traffic correctly, and which specialist is stuck?” — is answered byGET /api/v1/agents/squads/{id}/analytics, which reports the four observability rollups: handoff success rate (resolved-terminal shares per target), drop-off (handoffs that bailed to a human or errored), specialist utilization (which member carries the conversation load), and containment lift (the squad’s end-to-end containment minus the classifier-alone baseline). The percentages are a window rollup, not a live-dedup: the route is tenant-scoped and degrades to empty/zero rather than 500 when a provisioning gap hits. The hub that carries every member-accessible analytics surface (and cross-links the wallboard) is the Insights overview — build/test routing specifics in Agent squads.