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 ten communication channels — six core channels plus four APAC chat apps: Core- SMS — Global coverage in 190+ countries
- WhatsApp — Business API with template and session messaging
- RCS — Rich messaging on Android devices
- Viber — Popular in Eastern Europe and Southeast Asia
- Email — Transactional and marketing email
- Voice — Outbound/inbound calls, SIP trunking, IVR, and AI voice agents
- 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.
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.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.
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
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. Full decision table and escalation criteria: Troubleshooting: number warming caps; 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. 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: a future send_at (the row is scheduled, not broken), a quiet-hours or compliance gate, 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). Full cause table and status decoder: Troubleshooting: message stuck in queued.
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.
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.
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 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.
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 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. See the sender pools guide and sender resolution concept.
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.)
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).
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.
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
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 SENDER_POOL_EMPTY or SENDER_NOT_OWNED?
SENDER_POOL_EMPTY (422) is covered above — the routed pool’s sender_dids array has no members. SENDER_NOT_OWNED (422) is an always-on (not strict-mode gated) numeric-sender ownership check: a from like +12125550198 that’s owned by a different organization is rejected because the Devotel softswitch trusts the originator field and recipients would attribute the message to that number’s true owner. Fix it by sending one of your own numbers, the platform default (Devotel), or an approved alphanumeric sender ID — the check deliberately fails closed with 503 SENDER_OWNERSHIP_CHECK_UNAVAILABLE when the registry read is transiently unavailable. See the sender pools guide and the sender resolution concept.
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.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.
Number Lifecycle
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.- Unresolved sender — a
409 NUMBER_ALREADY_TAKENmeans an inventory race between your search and checkout; re-run the search and retry. For bulk purchases, failed rows come back in thefailed[]array with a per-row error code, are never charged, and are safe to retry —debited_centsonly countssucceededrows. - 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 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.
What happens if my number is stuck at pending_compliance?
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 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 fix workflow is on 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.
Number identity & caller ID
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.
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.Why did my port-out release reject with 401 PORT_OUT_PIN_MISMATCH?
The number has Port-Out Protection enabled — a tenant-owned PIN that gates every port-out submission for that number. When protection is on, the release call must carry the matching port_out_pin in the request body; a missing or wrong value is rejected with PORT_OUT_PIN_MISMATCH (401) 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-protection only ever exposes { enabled, set_at }. Set or rotate it with POST, and disable it with DELETE on 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. A correct release looks like:
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 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 mu-law), and PCMA (G.711 A-law). OPUS is recommended for the best quality at lower bandwidth.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.
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.
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 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.
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.
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 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 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 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
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.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
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.
Why do my webhooks return 401 signature-invalid errors even though the secret matches?
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
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.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.
Troubleshooting and runbooks
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