> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Frequently Asked Questions

> Answers to common questions about the Orbit platform — messaging, voice, video, Verify, billing, webhooks, SDKs, CX analytics, routing, numbers, and support.

## Sprachhinweis

Wenn keine Übersetzung verfügbar ist, wird der englische Inhalt als Fallback angezeigt. Fehlercodes, API-Pfade und Codeblöcke bleiben unverändert.

# 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](/concepts/identity-federation-saml-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](/verify/overview)). The per-member enrollment mechanics are in the [Authentication](#authentication) section below — the answers there still apply unchanged under SSO.

### What channels does Orbit support?

Orbit supports nineteen communication channels — fifteen core channels plus four APAC chat apps:

**Core**

* **SMS** — Global coverage in 190+ countries ([SMS](/channels/sms))
* **WhatsApp** — Business API with template and session messaging ([WhatsApp](/channels/whatsapp))
* **RCS** — Rich messaging on Android devices ([RCS](/channels/rcs))
* **Viber** — Popular in Eastern Europe and Southeast Asia ([Viber](/channels/viber))
* **Email** — Transactional and marketing email ([Email](/channels/email))
* **Voice** — Outbound/inbound calls, SIP trunking, IVR, and AI voice agents ([Voice](/channels/voice))
* **Fax** — Send and receive faxes as PDF or TIFF documents, with delivery receipts ([Fax](/channels/fax))
* **Video** — In-browser video rooms from the API or dashboard, with recording and live broadcast ([Video](/channels/video))
* **USSD** — Menu sessions over dial codes for feature phones and 2G ([USSD](/channels/ussd))
* **Push** — Mobile and web push notifications over APNs and FCM ([Push](/channels/push))
* **Telegram** — Bot messaging with text, media, and inline keyboards ([Telegram](/channels/telegram))
* **Instagram** — Direct messages with text, media, quick replies, and story mentions ([Instagram](/channels/instagram))
* **Messenger** — Facebook Page messaging with quick replies, personas, and message tags ([Messenger](/channels/messenger))
* **Slack** — Workspace messaging via OAuth install, with slash commands and events ([Slack](/channels/slack))
* **Wallet passes** — Issue Apple and Google Wallet loyalty cards, coupons, and event tickets ([Wallet passes](/channels/wallet-passes))

USSD, Video, and Wallet passes are specialized channels — reach for them when core messaging and voice don't fit the use case (feature-phone menus, in-browser calls, loyalty cards).

**APAC** (see [APAC channels onboarding](/guides/asia-channels-onboarding))

* **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 the `europe-west1` region (Belgium). Data is stored in Cloud SQL PostgreSQL 16 with at-rest encryption.

### Can I migrate from Twilio (or another CPaaS) to Orbit?

Yes — plan it as five steps, and treat the API surfaces as different, not drop-in compatible. Orbit is not a Twilio-shaped API behind a new URL: endpoints, request fields, and webhook payload shapes all differ, so every integration point needs a mapping, not a redirect.

1. **Port your numbers in.** Bring your existing phone numbers over with a standard port request — the checklist and per-country lead times are in [Number porting](/numbers/porting). Port-out protection is a tenant-owned switch, so once you control it the [Port-out blocked](/troubleshooting/port-out-blocked) runbook covers a refused port in either direction.
2. **Map MessagingServiceSid to a messaging service.** Twilio's Messaging Service (the `MessagingServiceSid` you send against) maps to an Orbit messaging service — routing, sender pool, and delivery configuration in one named object. Create it and assign the sender pool in the [messaging services console](/guides/messaging-services-console), then send against the service instead of a raw `From` number.
3. **Map your sender pool.** Recreate the senders behind each Twilio service — long codes, short codes, toll-free, alphanumeric sender IDs — as the sender pool on the matching Orbit messaging service. Regulatory requirements travel with the sender (US 10DLC campaign registration, alphanumeric sender-ID pre-registration in the markets that require it), so register the senders on Orbit before cutover, not after.
4. **Update your webhook handling.** Orbit signs every delivery with HMAC-SHA256 in the `X-Orbit-Signature` header instead of Twilio's `X-Twilio-Signature`, and the event payload shape differs from Twilio's status callbacks. Verify with [Webhook security](/webhooks/security) and re-point your handlers at the Orbit event names in [Webhook events](/webhooks/events).
5. **Validate in sandbox before cutover.** Run the whole integration — sends, inbound replies, webhook signatures, sender-pool routing — against [sandbox and test mode](/guides/sandbox-test-mode) first, then cut traffic over number by number.

For the feature-by-feature comparison before you start, see [Best Twilio Alternatives 2026](https://orbit.devotel.io/en/compare/best/best-twilio-alternatives-2026).

### How is each error-code family indexed across the platform?

An error `code` moves through four pages that each have one job:

* **[Glossary](/reference/glossary)** — tells you what the term itself means (DLR, sender ID, quiet hours) once the terms in a runbook are unfamiliar.
* **[Error codes reference](/reference/error-codes)** — gives the full definition for a specific `code`. Its [From a code to a runbook table](/reference/error-codes#from-a-code-to-a-runbook) sorts every code into one of three retry classes:
  * **Deterministic rejects** (422 validation, tenant-owned pre-send gates) — never retry until the payload or tenant config changes.
  * **Transient faults** (429 / 5xx) — retry with `details.retry_after`, or exponential backoff when that field is absent.
  * **Conditional retries** — retry only once the named condition is met (stale SSH session, cold wallet balance, over-cap 10DLC campaign, etc.).
* **[Troubleshooting hub](/reference/troubleshooting-hub)** — indexes every runbook page by surface: messaging, numbers, voice, video, compliance/deliverability, webhooks, Verify, email, billing, account, platform, channels, and more.
* **[FAQ](/reference/faq)** — answers the "why did my X happen" questions the runbooks don't phrase directly.

So a `PENDING_ACTION_*`, `COMPLIANCE_PROFILE_*`, `VIDEO_*`, or `WHATSAPP_CALLING_*` code goes to the glossary for the vocabulary, the error-codes table for the retry class, the hub's Video or Compliance accordion for the matching runbook, and falls to the FAQ for the common-question angle.

### Why does my branded console fall back to the plain theme with `DOMAIN_NOT_FOUND`?

The pre-render lookups that resolve your custom domain to its organization (`/public/resolve-domain` by host, `/public/resolve-org-domain` by org, `/public/branding` for the login-screen logo) all return `404 DOMAIN_NOT_FOUND` until the custom-domain record reaches `active`: never claimed, still `pending` on DNS, `verifying` on the managed certificate, or `failed` on issuance. While the lookup misses, the dashboard degrades to the neutral default theme instead of erroring out — fix the claim (re-verify from **Settings → Branding → Custom domain**) and resolution recovers on its own within the short edge-cache window. The full runbook is [Troubleshooting: DOMAIN\_NOT\_FOUND on branded console and public resolve](/troubleshooting/domain-not-found-branded-resolve); the status lifecycle is in [Custom domains and managed SSL](/concepts/custom-domains-and-ssl).

***

## Data residency & privacy

### Where is my data stored, and is a DPA available?

All tenant data runs on Google Cloud Platform in the `europe-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)](/compliance/data-processing-agreement) is available to execute in-platform, and the [Trust Center](https://orbit.devotel.io/en/trust) carries the current data-residency and security posture. The full region and storage breakdown is on [Data residency overview](/compliance/data-residency-overview).

### How do I exercise GDPR rights on my own customers' data?

The [DSAR guide](/compliance/dsar) 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](/compliance/gdpr-posture-guide) maps which controls you own on the platform side.

### What certifications back the residency claim?

The [Evidence binder](/compliance/evidence-binder) holds the SOC 2, ISO 27001, GDPR, and HIPAA compliance packs, including the attestations behind the EU-residency claim.

### My erasure POST returned `ERASURE_COOLING_OFF_ACTIVE` — can I cancel or wait?

That 409 is a duplicate-filing refusal, not a failure: a pending or executing Article-17 erasure request already exists for the contact, and only one can be active at a time. You have three moves, all tenant-owned:

1. **Wait for the active request to execute.** Each request carries a cooling-off window (7 days by default, per-tenant overridable) — read `cooling_off_ends_at` on `POST /api/v1/contacts/:id/gdpr/erasure-request` or list them via `GET /api/v1/contacts/:id/gdpr/erasure-requests`. When the window lapses, the scheduler hard-deletes the contact and writes the per-resource audit chain, and filing a fresh request for another contact is unaffected.
2. **Cancel the active request.** While it is still `pending` (inside the cooling window), `POST /api/v1/contacts/:id/gdpr/erasure-request/:requestId/cancel` (or `POST /api/v1/compliance/dsar/erasure-requests/:id/cancel` on the org-level surface) withdraws it — after which a new POST for that contact is accepted again. An optional `reason` records why the request was withdrawn.
3. **Accept the terminal 409.** `DSAR_NOT_CANCELLABLE` is the terminal gate on the cancel path: the request no longer exists, or it already completed, failed, expired, or was cancelled — a cancel at that point is a no-op you resolve on your side, not a retryable error. Poll the request status instead of re-cancelling.

Cancel semantics and the SLA surface that keeps you ahead of the statutory deadline are on the [DSAR guide](/compliance/dsar); the erasure-request endpoint contract is in the [contacts API reference](/api-reference/endpoints/contacts); the full cancel-or-wait walkthrough — including the sibling `422 CONTACT_ERASURE_PENDING` gate on sends — is on the [pending-compliance and erasure gates troubleshooting page](/troubleshooting/pending-compliance-and-erasure-gates).

### Why did my residency change return `409 RESIDENCY_LOCKED`?

The residency pin (`PUT /api/v1/compliance/data-residency`) is locked, and a locked pin cannot be silently re-homed to another region — moving the pinned region would strand data already written under the old boundary, which is exactly the silent cross-border transfer GDPR Art. 44 forbids. Two readiness refusals sit around the same surface, and the details field tells you which one fired:

* **`RESIDENCY_LOCKED`** (409) — the region-change call hit a configuration whose `locked: true` flag protects the existing pin. The deliberate path is the unlock call: an admin explicitly POSTs `/api/v1/compliance/data-residency/unlock`, runs the governed region migration, then re-locks. Skipping the migration (unlock, change, stay unlocked) defeats the control — treat unlock as an audited, out-of-band step, not a routine part of a region change.
* **`RESIDENCY_NOT_ENFORCED`** (409) — you POSTed `/lock` before anything is enforceable. Lock requires an already-pinned, already-enforced region; the bootstrap is `PUT /api/v1/compliance/data-residency` with `enforced: true` on a live region first, then lock it.
* **`RESIDENCY_REGION_UNAVAILABLE`** (409) — you asked to enforce a region whose POP and region-scoped storage are not provisioned yet. Advisory pins to a preview region are allowed (they register intent with `enforced: false`); what you cannot do is promise enforcement where the platform has no region-scoped storage. Either enforce the currently-live region, wait for the region to graduate, or pin advisorily and enforce later.

Read the current pin (and its `locked` / `enforced` flags) before retrying: `GET /api/v1/compliance/data-residency` returns the singleton config per organization, and a `404 RESIDENCY_NOT_FOUND` is the signal nothing is pinned yet. The full region catalog, availability states, and unlock-escalation path are on [Data residency overview](/compliance/data-residency-overview); the per-plane residency matrix the pin controls is on the same page.

***

## Platform Concepts

### What's the difference between UCaaS and CPaaS?

**CPaaS** (Communications Platform as a Service) is a set of APIs — messaging, voice, video — that a developer integrates into their own application to add communication features. **UCaaS** (Unified Communications as a Service) is a finished internal-communications product — browser softphone, SIP trunking, PBX replacement — configured through a dashboard rather than code. Orbit ships both on the same tenant: build with the [CPaaS APIs](/api-reference/overview), or run your team's own phone system on the [UCaaS pillar](https://orbit.devotel.io/features/ucaas).

### 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 is `POST /api/v1/messages/sms` (see [SMS](/channels/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](/guides/saml-sso-enrollment).
* **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).

The full conceptual model — the provisioning lifecycle from create to deprovision, attribute and group mapping, and the first-login sequence — is on [Identity federation: SAML and SCIM](/concepts/identity-federation-saml-scim).

***

## Authentication

### How do I get an API key?

1. Sign up at [orbit.devotel.io](https://orbit.devotel.io/signup)
2. Navigate to **Settings > API Keys**
3. Click **Create Key**
4. Copy and securely store the key — it's only shown once

### What's the difference between live and test keys?

| Key Type | Prefix | Environment | Real Messages |
| - | - | - | - |
| Live Secret | `dv_live_sk_` | Production | Yes |
| Test Secret | `dv_test_sk_` | Sandbox | No (simulated) |
| Public | `dv_live_pk_` | Client-side | Read-only |

### 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 are `enabled` (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](/concepts/authentication-model); the SAML stages are in the [enrollment guide](/guides/saml-sso-enrollment).

### Why can't I revoke my current session from Settings → Sessions?

Revoking the session you are signed in with would end your own login while you are still using it — the device goes silent for you, and the session you actually meant to close stays open. To stop that, the revoke endpoint refuses self-revocation: `DELETE /api/v1/settings/sessions/{sessionId}` compares the session id that your current request is authenticated with against the target session id, and a match returns `400 CANNOT_REVOKE_CURRENT_SESSION` with the message "You can't revoke the session you're currently signed in with. Use sign-out instead." The **Active Sessions** card under **Settings → Security** only offers Revoke on your other devices, so you normally see this error only when an API client (or a stale dashboard tab) targets your own live session.

The correct move is the one the message names: use the normal sign-out flow to end the session you are on. Sign-out and revoke are two different paths — signing out ends your current session through the identity provider's logout, while revoke targets a *different* session from a device that stays signed in.

One adjacent note for support tickets: if the platform cannot resolve your current session when the guard runs, the guard degrades to no self-match and a warning with the request id is logged — include the `meta.request_id` from the response envelope when you report unexpected revoke behavior so the log line can be found:

```json theme={null}
{
  "error": {
    "code": "CANNOT_REVOKE_CURRENT_SESSION",
    "message": "You can't revoke the session you're currently signed in with. Use sign-out instead.",
    "status": 400
  },
  "meta": {
    "request_id": "req_9f2c1a7b4d",
    "timestamp": "2026-09-11T12:00:00.000Z"
  }
}
```

To close out your own remaining sessions safely: sign out first, then sign back in and revoke any other stale sessions from **Settings → Security → Active Sessions** (or `DELETE /api/v1/settings/sessions/{sessionId}`, documented under [Revoke one of your active sessions](/api-reference/endpoints/settings#revoke-one-of-your-active-sessions)). The code and its behavior are in the [Error Code Reference](/reference/error-codes); the session model is on [Authentication and session model](/concepts/authentication-model).

***

## Messaging

### What phone number format does Orbit use?

All phone numbers must be in E.164 format: `+[country code][number]`. Examples:

* US: `+14155552671`
* UK: `+447911123456`
* Turkey: `+905551234567`

### How are SMS messages billed?

SMS messages are billed per segment:

* **GSM-7 encoding** (standard characters): 160 chars per segment, 153 for multi-part
* **Unicode** (emoji, non-Latin scripts): 70 chars per segment, 67 for multi-part

Each segment counts as one message against your plan or credits.

### How is my number warm-up ceiling computed?

The per-number warm-up ceiling follows a geometric curve that starts small and compounds daily, capped at 3,000 messages/day per DID:

| Day | Daily ceiling |
| - | - |
| 0 | 50 |
| 3 | 123 |
| 7 | 305 |
| 10 | 756 |
| 12 | 1,386 |
| 14 | 3,000 (curve tops out; the number becomes eligible to graduate to `warmed`) |
| 15+ | 3,000 — the trust-tier phase cap is now the binding constraint |

The curve is the same default shape for every new DID: a day-0 base of 50 with roughly 35% daily compounding, hard-capped at 3,000. The ramp is tracked through four phases — `initial → ramp → steady → verified` — and the engine gates every outbound SMS send: a send past today's ceiling is refused with a 429 (`DAILY_CAP_EXCEEDED` when the day's count is spent, `WARMING_QUOTA_EXCEEDED` when the growth curve is the binding limit, or `NUMBER_MPS_EXCEEDED` when you are bursting faster than the per-second rate).

Two properties can pull the ceiling *down* from the base curve, but nothing can lift it above:

* **DLR-driven reputation**: delivery receipts over a rolling 30-day window drive a 0–100 health score. An `excellent`, `good`, `fair`, or `unknown` tier lets the ramp proceed at 100%; a `poor` tier holds it at 50% of the curve; a `critical` tier throttles it to 25%. Complaint-coded carrier rejections count heavily against the score.
* **Trust score**: the 10DLC registration ecosystem assigns your brand a trust tier. A higher trust score moves the phase boundaries further out, but the per-day numbers inside a phase follow the same geometric ramp.

What does *not* change the curve: warm-up applies to long codes and toll-free numbers; short codes are pre-vetted from day one and excluded from the warming ramp.

Read the live truth on `GET /api/v1/numbers/:id/warming` — it returns the current `warming_phase`, today's `daily_cap` versus the live `current_day_count`, the `trust_score`, and the `warming_started_at` anchor the curve compounds from. To plan a launch, check `GET /api/v1/numbers/:id/warming/progression`, which returns the `warming_state` and a 15-day forward forecast of the daily ceiling — you can map out your send volume against the ramp without polling day-by-day.

For the comparative chart across assets (email IP, sending domain, phone number, and alphanumeric sender ID), see [How is warm-up different across an email IP, a phone number, and a sender ID?](#how-is-warm-up-different-across-an-email-ip-a-phone-number-and-a-sender-id). The full ramp walkthrough, pacing guide, and quota-error handling are in the [number warm-up guide](/guides/number-warming); the carrier-reputation model and the closed-loop reputation feedback mechanics are in [Sender warming and reputation](/concepts/sender-warming-and-reputation).

### What happens if a message fails?

Failed messages can be retried using `POST /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](/guides/10dlc-registration) for step-by-step instructions.

### Why was my SMS rejected a few days after my number was registered?

That is the number warming ramp, not a registration failure. A newly acquired 10DLC number starts with a small per-day send ceiling that grows as the ramp advances (`initial → ramp → steady → verified`), and sends past today's ceiling are refused with a 429 before any carrier is attempted: `DAILY_CAP_EXCEEDED` (the number's daily cap is spent, resets at midnight UTC), `WARMING_QUOTA_EXCEEDED` (today's growth-curve ceiling on a still-warming number), or `NUMBER_MPS_EXCEEDED` (you are bursting faster than the per-second rate — back off one second). Watch the ramp on `GET /api/v1/numbers/:id/warming` (live `daily_cap` vs `current_day_count`, `warming_phase`, `trust_score`) and `GET /api/v1/numbers/:id/warming/progression` (15-day ceiling forecast). Do not retry-loop inside the same day — the `retry_after` on a warming 429 points at the next UTC window, so re-route the overflow to an already-warmed sender instead. The carrier-reputation model behind the ramp is on [Sender warming and reputation](/concepts/sender-warming-and-reputation); the full decision table and escalation criteria are on [Troubleshooting: number warming caps](/troubleshooting/number-warming-caps), and the warmup terms are defined in the [glossary](/reference/glossary).

### Why is my message stuck in `queued` and how do I unstick it?

`queued` means the message has not been handed to a sender or provider yet — the hold is on your side of the handoff, not carrier-side. Work [Troubleshooting: message stuck in queued](/reference/troubleshooting) — that page owns the `queued` lane: the send ladder `pending → queued → sending → sent → delivered`, the full cause table, and the escalation path. List the stuck rows on the dashboard Delivery Log with `direction=outbound&status=queued`, or via `GET /api/v1/messages?status=queued`. The usual causes: an exhausted sender pool, an email warm-up cap, or provider connectivity trouble. Unstick it by clearing that hold — never by double-sending a copy (you can duplicate when the hold clears). The decoder above covers the whole ladder; a row parked `scheduled`, `pending`, or by your quiet-hours window belongs to the sister page — the routing note on the hub names each stage's owner.

### Why did my scheduled-message edit or cancel return `409 MESSAGE_NOT_EDITABLE` / `MESSAGE_NOT_CANCELLABLE`?

Both verbs — `PATCH /api/v1/messages/:id` (edit) and `DELETE /api/v1/messages/:id` or `POST /api/v1/messages/cancel-scheduled` (cancel) — are gated on the row still being in `scheduled` at the instant you touch it. Once the send\_at drain promotes the row (it moved on to `queued`/`sending`/`sent`, or someone else already cancelled it), the gate closes and the call returns 409 instead of silently rewriting a message that already left. Two worked refusals:

```
PATCH /api/v1/messages/msg_8h3k → 409 MESSAGE_NOT_EDITABLE    // row already reached `sent`
DELETE /api/v1/messages/msg_8h3k → 409 MESSAGE_NOT_CANCELLABLE // drain was already promoting it
```

The fix is to read the row's current status (`GET /api/v1/messages/:id`) and reconcile against it — never blind-retry the same edit or cancel, because a 409 here is a deterministic state verdict, not a transient fault. If the message is already past scheduled and you still need that send to go out (or not), cancel what you can and re-create the send with corrected content or a new `send_at`. The full gate model is on [Scheduled sends (`send_at`)](/concepts/send-at-scheduled-sends), the edit/cancel API walkthrough is in the [Message scheduling guide](/guides/message-scheduling), and the drain and promotion mechanics are on [Scheduled send never fired](/troubleshooting/scheduled-send-never-fired). Both codes are enumerated in the [Error Code Reference](/reference/error-codes).

### 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 is `available`, 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](/concepts/agent-presence-lifecycle) and dispatch gating on [The ACD queue model](/concepts/acd-queue-model).

### What happens when my APNs token returns Unregistered — does the send retry?

No. A per-device provider rejection (APNs `Unregistered` / an expired FCM token) is a deterministic refusal, so the platform does not retry it and you should not either. The send still returns `201`: the affected device lands in `notifications[]` as `status: "failed"` with an `error` string naming the provider's verdict. Tokens APNs/FCM report as permanently gone are pruned from your registry automatically, so the next send skips them rather than failing them again. Re-check `GET /api/v1/push/notifications` for the `error` per device and re-register the device token from the SDK — do not replay the same body. The common-error and per-device decoder table is on the [Push channel page](/channels/push#common-errors); the sandbox-vs-production note (a `BadDeviceToken` rejection driven by `NODE_ENV`, not by the token being invalid) is under [Apple Push setup](/channels/push#apple-push-setup-apns).

### Can I preflight a push send against device capability flags before blasting the audience?

Yes, but you do it against the **registered-device list**, not a capability linter. There is no pre-send gate that validates capability flags (sound, badge, mutable-content) — those are resolved against the APNs category at send time. The preflight move is: call `GET /api/v1/push/device-tokens` for the target user (or resolve the device ids you hold) and drop any token whose `platform` you do not want (for example a `web` subscription you meant to exclude from a mobile-only blast), then send against the filtered `device_token_ids`. STOP / opt-out and disabled tokens are filtered server-side on every send regardless, so a `user_ids` broadcast reaches only deliverable devices; the capability-flag resolution itself is the notification-category expansion described on the [Push fields section](/channels/push#fields). If you want a break-glass confirmation that the audience is deliverable before you blast, check the user's registered tokens explicitly — never rely on the send to surface a removed device.

### How are per-device results on `POST /api/v1/push/send` aggregated?

The response is the aggregation: an immediate send returns `201` with `notifications[]` — one entry per matched device, each with its own `id` (`push_…`), `deviceTokenId`, `status`, and (on failure) the provider `error`. The `data` object also carries `total`, `sent`, and `devices_targeted` counters, so a broadcast reports its full fan-out in one response. `GET /api/v1/push/notifications` then returns the same per-device delivery log (newest-first, pageable with `cursor`/`limit`), and the `push.delivered` / `push.opened` webhooks carry the receipts per device id — so you can reconcile and audit per device rather than reconcile a single opaque verdict. The per-platform error strings (`APNs credentials not configured …`, `Huawei Push Kit not configured`, Web Push not configured) scope a missing credential to those devices only — they do not fail the whole send.

### How does a VAPID rotation affect existing Web Push registrations?

A VAPID rotation invalidates every existing browser subscription — the public key the page subscribed against no longer matches, so every Web Push target starts failing until each browser calls `pushManager.subscribe` against the new public key and re-registers the resulting subscription as a device token. Native APNs/FCM/HMS targets in the same send are unaffected. Rotate only when the private key is compromised; when you do rotate, treat every existing Web registration as gone and re-subscribe, because no in-place re-subscribe happens automatically. The per-device `error` on `GET /api/v1/push/notifications` flags the failing web target, and the keys are provisioned as `DEVOTEL_VAPID_PUBLIC_KEY` / `DEVOTEL_VAPID_PRIVATE_KEY` / `DEVOTEL_VAPID_SUBJECT` on the [Web Push setup section](/channels/push#web-push-setup-vapid).

### Why is my wildcard push broadcast refused before anything dispatches?

Two deterministic pre-send gates abort a whole-org broadcast (`user_ids: ["*"]`) before APNs/FCM/HMS/Web Push are ever touched. `BROADCAST_TOO_LARGE` (422) means your **deliverable** audience — enabled device tokens minus push/all suppression opt-outs — exceeds the 100,000-device fan-out ceiling: split into explicit `user_ids` (the platform never truncates silently). `CHANNEL_RATE_LIMITED` (429) means the tenant's per-channel velocity gate tripped on a sliding one-minute window: raises are owner-only via `PUT /api/v1/settings/compliance/channel-rate-overrides` (whole-map replace) for the dedicated `push` envelope, or `PUT /api/v1/settings/compliance/fraud-caps` for the fraud floor. Read `error.details.channel` (`push`, not `sms`) and `request_id` before either escalation. The full recovery runbook is on [Push broadcast pre-send gates](/troubleshooting/push-broadcast-caps).

### Why is my send refusing with `CHANNEL_REQUIRED`, `CONVERSATION_MISMATCH`, or an inbox-only label?

The deterministic channel- and conversation-naming pre-send refusals fire before the message reaches any provider. `CHANNEL_REQUIRED` (422) means the body had no `channel`, an empty string, or the literal `"auto"` — pass a valid enum such as `sms`, `whatsapp`, `rcs`, `email`, or `voice` explicitly (the unified send path no longer guesses, because guessing once billed callers for PSTN voice on an SMS send). `CHANNEL_COMING_SOON` (422) means the channel name is valid but points at a regional/beta channel not yet enabled for your tenant — open a support ticket to gate it in. `CHANNEL_NOT_REPLYABLE` (422) means the reply flow named the inbox-only triage labels `agent` or `video` — pick a real outbound channel from the composer. `CONVERSATION_NOT_FOUND` (404) means a supplied optional `conversation_id` resolves to nothing on your tenant; `CONVERSATION_MISMATCH` (422) means the id resolves, but it does not address this recipient and channel — drop the field to auto-resolve, or supply the conversation id whose recipient and channel match. The [channel and conversation pre-send refusals runbook](/troubleshooting/channel-and-conversation-refusals) walks all five with worked envelope samples.

## Sandbox & Testing

### What's the difference between a sandbox send and a live send, and how do I switch?

The key decides the environment — create a test key (**Settings → API Keys → Create Key**, prefix `dv_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](/reference/troubleshooting) classifies `test_sent` as expected in sandbox, and the mechanics are defined in the [glossary](/reference/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](#billing) section mentions; it refers to the sandbox environment, not to a pricing concession.

### Can I clear a sandbox attempt from history?

Yes. A `test_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](/concepts/delivery-lifecycle) concept and the [submitted-no-receipt troubleshooting page](/troubleshooting/submitted-no-receipt).

### 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 move `delivered → 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](/troubleshooting/message-undelivered-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](/troubleshooting/message-undelivered-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](/concepts/message-cascade-groups), the waterfall semantics on [Notify cascade cost model](/concepts/notify-cascade-cost-model), and the event family in [Webhook Events](/reference/webhook-events).

### Why did my send return `422 TEMPLATE_NOT_APPROVED` when the look-up said the template was approved?

The gate fires on the **exact rendered variant**, not the template headline you remember approving. Three causes cover nearly every unexpected 422 here:

* **A sibling variant is still in review.** You approved the WhatsApp variant, but the send is rolling to the RCS variant (or vice versa) whose provider-side status is still `pending`. The gate blocks whenever the resolved variant's status is anything but approved.
* **The approval never landed back in Orbit.** The provider approved it today and your cached template record still carries the old `status` — the gate keys on the stored status, so the send refuses until a fresh read re-syncs it.
* **A campaign targets a per-channel variant that is approved on one channel but not the rendered one.** The per-channel variant renderer picks the variant; if that variant is still in review, the whole send 422s before dispatch.

Fix it by reading the variant, not re-firing:

1. `GET /api/v1/messaging/templates/:id` (per channel when the template is multi-channel) and walk each variant you might render — confirm the variant that will actually render carries `approval.status: "approved"`.
2. Wait for the approval to land (provider side), then retry the send. Identical replays fail identically while the status is `pending` / `rejected` / `disabled`.
3. Do **not** resubmit the template early to hurry it — each re-submit re-enters provider review from scratch, so you end up in line again instead of one approval ahead.

`INVALID_TEMPLATE` is a separate gate: it fires when the send's rendered shape can't resolve (variable placeholders that can't be substituted) — the fix is on the variable-resolution side, not approval. And `NO_VARIANT_FOR_CHANNEL` is the sibling variant-shape gate (the template exists but carries no variant for the rendered channel): the fix is authoring the missing variant / extending `fallback_chain`, covered on [Troubleshooting: template variant missing](/troubleshooting/template-variant-missing).

### How do `queued`, `undelivered`, and `failed` fit into one lifecycle ladder?

Read them as one funnel, not three unrelated statuses — and the failure-queue fallback as the legitimate end of the failing leg. `queued` is a platform-side hold with full dashboard and API visibility (`direction=outbound&status=queued` on the Delivery Log, `GET /api/v1/messages?status=queued`); diagnose the hold with the [enqueued-empty-states checklist](/troubleshooting/enqueued-empty-states). `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](/concepts/message-cascade-groups)). `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](/troubleshooting/message-undelivered-failed), and the full ladder mapping is in the [Message status transition rules](/concepts/message-status-dag) concept. If the row is closing late with no visible failure at all, check [DLR retry and late-receipt recovery](/troubleshooting/failed-dlr-recovery).

### Why do my push notifications fail per-device with `Unregistered` / `410` / `UNREGISTERED` on some targets — and how do I retire those tokens?

A push send returns `201` and grades every device individually, so an expired token shows up as one `status: "failed"` entry in the `notifications[]` list, not as a top-level error. The provider-code string in that row (`Unregistered`, `BadDeviceToken`, `UNREGISTERED`, `subscription is gone`, `410`/`404`/`403`) tells you whether the failure is permanent. When it is, the platform deletes the dead device-token row inline, so the next send skips it; the client must re-register a fresh token through the SDK before it can be targeted again. If a capability flag (`sound`, `mutable_content`, `interruption_level`, `badge`) ships but the device stays quiet or cropped, it is a device-side settings or entitlement mismatch, not a provider fault — preflight the flag. A runbook for all five failure classes (APNs 410, FCM `UNREGISTERED`, capability-flag drift, `user_ids` ownership collisions, and VAPID rotation) is on [Push token expiry troubleshooting](/troubleshooting/push-token-expiry), and the per-device response contract is on the [Push channel page](/channels/push).

### 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](/compliance/whatsapp-content-policy). The full fix workflow per status is on the [WhatsApp template troubleshooting page](/troubleshooting/whatsapp-template).

### What does `WHATSAPP_SESSION_EXPIRED` mean on a failed WhatsApp send?

Meta's 24-hour customer-service window for that recipient has closed, and WhatsApp only accepts template messages outside it — your free-form send was refused. Waiting does not re-open the window; only a recipient reply does, so do not retry the same free-form body. Gate the composer on `GET /api/v1/messages/whatsapp/window-status`, re-send the content as an approved template, and treat the recipient's reply as the signal to resume free-form. The cause table, the pre-flight endpoint, and the what-not-to-do list are on the [WhatsApp session expired runbook](/troubleshooting/whatsapp-session-expired), and the window model narrated end to end in the [24-hour window guide](/guides/whatsapp/24h-window).

### 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](/concepts/rate-limit-and-cooldown-taxonomy) (Family 2), and [Troubleshooting: rate limits](/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 return `WHATSAPP_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](/troubleshooting/whatsapp-template).

### 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](/compliance/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](/troubleshooting/whatsapp-connection) (Account lock section).

### Why is my 10DLC campaign stuck in review?

Campaign review typically takes 1–5 business days, and the top-level `status` 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](/guides/10dlc-registration).

### 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](/guides/10dlc-registration).

### 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](/concepts/brand-identity-trust-score); the endpoint contract is on the [Brand Identity API reference](/api-reference/brand-identity).

### 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 as `verified / 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](/api-reference/brand-identity), the model on the [Brand identity & trust score concept page](/concepts/brand-identity-trust-score), and the throughput upside in the [10DLC registration guide](/guides/10dlc-registration).

### 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 a `message.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](/channels/messenger).

### 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 through `GET`/`POST /api/v1/settings/channels/messenger/personas` (see the [settings endpoints](/api-reference/endpoints/settings)). The connect and onboarding steps are on the [Messenger channel page](/channels/messenger).

### 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](/channels/email-smtp-relay).

### 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-agnostic `message.delivered` / `message.failed` webhook events with `channel: "fax"`. See the [Fax channel page](/channels/fax).

### 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](mailto:support@devotel.io)) so the data can be re-encrypted onto the current key, and avoid immediately retrying the same encrypted value. The full runbook — including the BYOK-enforced `409 BYOK_KEY_UNAVAILABLE` sibling and its cause table — is on [BYOK key unavailable / decryption failed](/troubleshooting/byok-key-unavailable), and the key lifecycle it reads is on [Customer-Managed Keys (BYOK)](/compliance/byok-customer-managed-keys). The write-side failure (`ENCRYPTION_FAILED` — IV or authTag generation failed) and this read-side code are both listed in the [Error Code Reference](/reference/error-codes).

### 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](/reference/error-codes), and sandbox mechanics are covered in the [Sandbox & Testing](#sandbox--testing) section of this page.

### Why did my send return `422 SENDER_POOL_EMPTY`?

The sender pool resolved fine but its `sender_dids` array has no members — you routed traffic through a pool with no senders in it. Add at least one DID, short code, or alphanumeric sender ID to the pool before sending. The full fix walkthrough is on [Fix an empty sender pool](/troubleshooting/sender-pool-empty); the [sender pools guide](/guides/sender-pools) and [sender resolution concept](/concepts/sender-resolution) cover pool mechanics.

### Why does a US recipient see a phone number instead of my alphanumeric sender ID?

US and Canada carriers reject the shared alphanumeric sender, so the implicit default for a `+1` recipient swaps to a platform phone number. Pass an explicit `from` or a `sender_pool_id` to pin the sender identity. The fallback chain and per-corridor rules are on [sender resolution](/concepts/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](/api-reference/endpoints/campaigns) with `channel: "kakao"`. The cause-and-fix table is on the [KakaoTalk channel page](/channels/kakao#common-errors).

### 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](/channels/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** to `https://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](/channels/wechat#configuration) and [Zalo](/channels/zalo#configuration). (KakaoTalk gates the same way when its three keys are missing — see [KakaoTalk configuration](/channels/kakao#configuration).)

### Why did my email send get rejected with a 403 "domain not verified", and which DNS record do I fix?

The provider refused because DNS verification behind your sending domain regressed — verification is not a one-time check, and the platform re-validates the SPF, DKIM, and DMARC records on a daily health check. When a record drifts (a TTL expires into a changed answer, someone edits the zone at your registrar, or a key rotates), the domain slides back to unverified: the DNS status widget on the Email dashboard flips one or more per-record traffic-light chips (SPF, DKIM, DMARC) yellow or red, a bell notification flags the regression, and sends from that domain start failing at the provider with a 403 "domain is not verified" rejection while the Delivery Log row stays on your side. Read the chip that went yellow or red to learn which record drifted, fix that record at your DNS host to match the expected value, then re-verify on demand — the interactive re-check re-reads the record and re-registers the domain with the provider, which is what clears the 403, so expect sends to resume only after the re-verify passes. The full recovery loop — per-record diagnosis, checking the history before you edit, the on-demand re-verify, warm-up posture while the domain is down, and resuming sends — is on [Troubleshooting: email DNS verification drift and recovery](/troubleshooting/email-dns-drift). If the rejects are recipient-side (bounces or spam complaints after the provider accepted the send), work the [email bounces and spam complaints](/troubleshooting/email-bounces-complaints) page instead.

### Can I route inbound LINE/Kakao/WeChat/Zalo into the same inbox?

Yes. All four APAC channels arrive on the standard `message.received` webhook with their own `channel` value (`line` / `kakao` / `wechat` / `zalo`) on the [normalized inbound envelope](/webhooks/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](/guides/inbox-setup) — and the [APAC channels onboarding guide](/guides/asia-channels-onboarding) closes with the inbox path its fold-in notes imply. The sender-resolution model behind "which sender answered" is on [Sender resolution](/concepts/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](/channels/line#inbound-envelope-samples)).

### Why does my Lookup response say `coming_soon` on SIM swap, Identity Match, or Live Activity?

`coming_soon` is a per-field status on the Lookup response, not a lookup failure — it means the field is in the public catalog but the upstream provider feed behind it isn't contracted yet, and the dip was skipped for just that field. Read the `dataPackages` map entry by entry: each one carries a discriminated `status` (`available`, `coming_soon`, `not_implemented`, or `error`) with a `reason` string and an intended `provider`. The carrier-network fields — `sim_swap` and `identity_match` (CAMARA SIM Swap and KYC Match over GSMA Open Gateway) — return `coming_soon` until a CAMARA operator is wired for your deployment; `live_activity` and the CNAM/caller-name fields return it when their upstream returned no usable signal or isn't configured. Replay won't change the verdict until the vendor work lands, and the verdict is deliberately fail-closed — a missing operator answer never falls through as a false match. The per-field status matrix is on the [Number Lookup page](/numbers/lookup), and the runbook for the whole-request failure case is on [Troubleshoot number lookup failures](/troubleshooting/number-lookup-and-messaging-service-failures).

### What's the difference between `LOOKUP_FAILED` and a per-field `coming_soon`?

They're two different layers of the same API. `LOOKUP_FAILED` is a request-level `502` — the whole lookup could not answer because both the primary HLR provider and the fallback were unreachable (or no provider is configured), so the route fails before any per-field verdicts are produced; retry with backoff and follow the [number-lookup runbook](/troubleshooting/number-lookup-and-messaging-service-failures). A `200` with one or more `coming_soon` entries in `dataPackages` is the opposite case — the base dip succeeded, and per-field isolation judges each requested field independently, so a field whose upstream isn't contracted reports `coming_soon` or `error` explicitly while every other field still answers. Build your integration on the per-entry `status`; never treat the presence of the map as a guarantee that every field answered.

### Why does `identity_match` return `not_available` verdicts even when I passed the attributes?

`not_available` is a per-attribute result on the `identity_match` field, not an error — it means the mobile operator holds no value for that one attribute in its KYC records, so it can't assert a match either way. The field matches the identity attributes you supply (`identity_name`, `identity_given_name`, `identity_family_name`, `identity_birthdate`, `identity_email`, `identity_id_document`) against the operator's own records and returns a `data.matches` map with a `true` / `false` / `not_available` verdict per attribute plus an aggregate `data.overall_match` (`null` when any verdict is indeterminate) — the operator's underlying PII is never returned. Treat `not_available` as "unknown", not as a failed check; a gate that hard-fails on it would block real users with thin operator records. Selecting the field with no attributes returns a per-field `error` (`identity_attributes_required`) instead, and a `coming_soon` entry until a CAMARA operator is wired. The attribute list and verdict semantics are on the [Number Lookup page](/numbers/lookup).

## Inbox & Operator

### What's the difference between a conversation, a ticket, and a thread?

A **conversation** is the live message record the inbox keeps per customer across channels — SMS, WhatsApp, email, web chat, voice calls — with inbound and outbound messages ordered into one **thread**. A **ticket** is the unit of tracked work an operator creates (from the dashboard or over the API): it survives a thread's idle close, carries a subject, priority, assignee, and an `open → pending → resolved / wont_fix` lifecycle with an audit trail. Use a conversation while the unit of work is a live exchange; move to a ticket when the unit is a case your team owns until resolution. The everyday-operator model is in [Conversation operators](/inbox/conversation-operators); the full thread/ticket split — including when a ticket is the right move — is in [Operate the Inbox Tickets queue](/guides/inbox-tickets-workflow). If by "thread" you mean a customer asking the same question: a thread is just a conversation's message history, not a separate object.

### How do keyword rules dispatch an inbound message?

A keyword rule is a per-tenant row evaluated against every inbound (MO) message body on one channel (`sms`, `whatsapp`, `rcs`, `viber`, `email`), and at most one rule fires per message — rules evaluate in creation order with `active = true` as the gate. The match types (`exact`, `contains`, `starts_with`, `regex`) choose the comparator, and the action (`reply`, `opt-out`, `opt-in`, `forward_agent`, `trigger_flow`) decides what a match does: send an auto-reply, write a consent change, hand the turn to a deployed AI agent, or start a flow. The full matching model — legacy alias normalization, precedence, and the consent writes — is in the [keyword-rules model](/concepts/keyword-rules-model), and the form-first setup plus API walkthroughs are in [Auto-reply rules](/guides/keyword-auto-reply-rules) with [Keyword rules worked examples](/guides/keyword-rules-recipes).

### How do I share a canned or quick reply across my whole team?

Canned responses are one **org-wide library**: an owner or admin creates each entry (a shortcut, a title, a body, an optional channel scope) under **Settings → Canned responses**, and every agent then inserts it in the inbox composer by typing the shortcut. For sequences that are more than a single reply — reply plus assign, tag, status change, follow-up — use **Macros** instead; a macro can carry either personal or team-shared scope. The library model and the scope split are in [Macros and canned responses](/guides/macros-canned-responses).

### A macro runs — so what exactly is a macro versus a canned response?

A macro is the canned-response body plus an ordered chain of side-effect steps — assign, tag, status or priority change, snooze, follow-up task, or an AI-agent escalation — run as one confirmed action, with variable placeholders that resolve against the live conversation at run time. The [canned-responses/ macros model](/concepts/canned-responses-macros-model) has the full split, and the [Macros and canned responses](/guides/macros-canned-responses) guide is the authoring walkthrough.

### When does a conversation auto-close, and can I change that?

Auto-close is a tenant-owned, opt-in control — it ships **off**. When an owner or admin enables it (**Inbox → Settings → Auto-close stale conversations**), a background sweep runs every 10 minutes and closes conversations with no new customer message for the configured idle window: a whole number from **1 to 90 days** (default **7**), applied to the statuses you allow (open, pending, or both). Every auto-closed row is marked `closed_reason: "auto_stale"`, fires the same `conversation.closed` webhook a manual close fires, and writes the audit event `conversation.auto_closed_stale` — so reporting splits platform-closed volume from agent closes. Configuration and the sweep's exact semantics are on [Auto-close stale conversations](/inbox/auto-close). Tickets are unaffected by the conversation auto-close sweep; each closed ticket carries its own resolved-reason lifecycle.

### When does double opt-in drain my pending-consent pool — and why aren't my first-party opt-ins being marked verified?

A confirm-by-reply handshake holds a contact at **pending** until the recipient replies YES; first-party opt-ins that were begun but never confirmed never become `opted_in` — that is the "why aren't they verified" answer, and the same mechanic drains your un-approved pool. The flow has three states you can read: `pending` (a confirm-request was recorded and the reply is still awaited), `opted_in` (the affirmative reply converted the pair into a confirmed grant), and the pool of contacts whose handshake was never even begun. Only `sms` and `email` can auto-deliver the confirm-request (an SMS reply or the **double-opt-in confirmation email** — the email-side variant — where the recipient confirms from the link); every other channel stays record-only, so on those channels the pending pool only drains when the recipient replies through your stack.

Work the drain in two reads, and do not confuse them: the contact's **consent timeline** (per-contact, per-channel, the two timestamps handshake evidence is built on — prompt then reply) tells you how far each contact got; the **suppression table** is a separate list and is not a substitute for re-consent — an opted-out contact needs an explicit re-consent, the suppression layer does not feed the pending pool. Start or confirm handshakes on `/compliance/double-opt-in`, use [Opt-in disclosure templates](/compliance/opt-in-disclosure-templates) for the copy the confirm-request uses, and read pool state on the contact's consent timeline rather than the suppression list. This is a tenant-owned control — nothing on Orbit starts a handshake on its own.

### What does "Reconnect Microsoft 365" on Settings → Presence mean?

A presence tile rides an org-level OAuth grant, and **Reconnect Microsoft 365** (or **Reconnect required** on any provider tile) means that grant no longer works — Microsoft revoked or expired the refresh token (a password change, admin consent removal, or conditional-access policy change all revoke it), or the admin consent step was never completed. While the badge is up, presence sync from Teams and Microsoft 365 Calendar has stopped: calls ring as if the source were off, using whatever status you set yourself in the dashboard header. The fix is a tenant-owned org-admin action: open **Settings → Integrations**, reconnect the **Microsoft 365** integration, and complete the Microsoft consent screen — the tile clears within about a minute for every member at once, no per-user re-toggle needed. The full badge-to-cause-to-fix map is on [Presence federation degraded states](/troubleshooting/presence-federation-states); setup is in the [presence federation guide](/guides/presence-federation-settings).

### How do I assign a conversation to a specific agent or squad?

Three layers, from manual to fully automatic:

1. **Manual** — any teammate can open the conversation and pick an assignee from the assign-control roster (eligible teammates only).
2. **Routing rules** — owner/admin-authored `if conditions → then assign` rules evaluated in priority order on every inbound conversation, assignable to a user, a team, a round-robin pool, a skill-matched agent, or an AI agent. Configure and dry-run them under **Inbox → Settings → Routing**; see [Routing rules](/inbox/routing-rules).
3. **Digital queues** — when what you want is "the longest-idle eligible agent with the right skills," not a fixed teammate: a queue maps a channel to a skill set and SLA target, and the inbox assigns accordingly. The model is on [Digital queues — skill-based ACD](/inbox/digital-queues).

The simplest working setup for most teams is: routing rules for channel/keyword/language facets, queues for the actual agent placement. Tickets use the same assignee model but additionally link to a queue or a direct assignee.

### What is a supervisor takeover, and which codes come back when I seize control?

A **takeover** is the supervisor intervention that moves reply control of a conversation from the assigned agent (or AI agent) to a supervisor — the caller stops waiting on one side of the desk and picks the conversation up themselves. Voice and digital (inbox) takeovers are separate surfaces: `POST /api/v1/voice/supervisor/takeovers` for a live call, and the `POST /api/v1/conversations/:id/takeover/{start,send-reply,end}` set for a digital conversation. The refusal codes you meet fall in two groups — state you can resolve on your side, and platform failures you retry once:

* **`TAKEOVER_ALREADY_ACTIVE`** (409) — a takeover is already open on the conversation; `details` names the `takeover_id` and `supervisor_id` holding it. Read the open takeover, advance or cancel it, then start again.
* **`TAKEOVER_FORBIDDEN`** (403) — the caller neither owns the active takeover nor holds an owner/admin role to recover it. The right response is the takeover owner ending their session, or an owner/admin releasing it — not a credential retry.
* **`NO_ACTIVE_TAKEOVER`** (409) — a send-reply or release went to a conversation with no live takeover; whatever took control before is gone, so the action has no target.
* **`TAKEOVER_NOT_ALLOWED`** (422) — the conversation is in a terminal state (`closed`, `archived`, `snoozed`); there is nothing live left to intercept.
* **`DIGITAL_TAKEOVER_START_FAILED`**, **`DIGITAL_TAKEOVER_REPLY_FAILED`**, **`DIGITAL_TAKEOVER_END_FAILED`**, **`DIGITAL_TAKEOVER_GET_FAILED`** (500) — the digital takeover start, reply, release, or read failed at the platform; retry once with the request id, and escalate if the same conversation keeps refusing.
* **Voice-side `SUPERVISOR_TAKEOVER_*` 500s** — the same pattern on the voice bridge; retry once, then report with the call id.

The full cause-and-resolution matrix per code, including the already-active race and what never to retry, is on the [supervisor takeover, whisper, and barge failures runbook](/troubleshooting/supervisor-takeover-failures).

### My ticket attachment upload returned `503 ATTACHMENT_STORAGE_UNAVAILABLE` — is the file too big or the wrong type?

Neither — that code fires **after** size and content intake checks pass, so the file is exonerated. It means the storage backend the attachment bytes land in timed out or returned a 5xx during the write. Re-upload the same file with a short backoff; a transient incident clears within the first couple of retries. If the same file keeps failing at backoff, escalate with the `meta.request_id` from the failed response and the `sha256` checksum of the bytes — support pins the exact attempt in the storage log instead of guessing from the filename. Do not strip the message off the thread or rename/re-encode the file: the fault is scoped to storage, and a renamed variant breaks the checksum you will send. The full cause split and retry matrix are on the [attachment storage unavailable runbook](/troubleshooting/attachment-storage-unavailable).

## 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](/channels/ussd) has the endpoint map, and the [USSD session model](/concepts/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](/channels/ussd#menu-shape).

### 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 is `POST /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:

```
dial → text=""          → CON Welcome to Acme — 1: Check balance, 2: Buy airtime
press 1 → text="1"      → CON Your balance is 1,250 NGN.
press 2 again → text="1" → END Invalid selection. Session ended.
```

The replay model and navigation rules are on the [USSD session model](/concepts/ussd-session-model) page; if Orbit can't resolve a step it answers `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](/channels/ussd#simulate-a-session-step).

### Do USSD sessions hit my webhooks?

No — there is no `ussd.*` event type in the catalog ([Webhook Events](/reference/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](/concepts/ussd-session-model) page.

## Wallet passes

### Why does my wallet pass update return 409 CONFLICT, and why does a duplicate issue return 200?

Two different edges of the same lifecycle. A voided pass is terminal — any update or re-void against it returns `409 CONFLICT` with a `reason` like `already_voided`, and the only recovery is to issue a replacement pass. A duplicate issue is the opposite move: `POST /api/v1/wallet-passes/issue` is idempotent, so a retried request with the same `idempotency_key` (or `Idempotency-Key` header) replays the first issued pass with `replayed: true` and `200` instead of minting a second pass. The full state machine (`active` accepts, `voided` rejects, `expired` renders expired in the wallet while the ledger stays `active`) and the recovery table are on the [wallet pass lifecycle troubleshooting page](/troubleshooting/wallet-pass-lifecycle); the channel guide's [common errors](/channels/wallet-passes#common-errors) table lists the same codes.

### Why does the holder's phone show stale content after my wallet pass update succeeded?

`generation` is a pass-changed signal, not a version to diff — each accepted update increments it by exactly one, and reads fold the pass's full event history at request time, so a fresh `GET /api/v1/wallet-passes/:id` always returns the exact current count while a cached read does not. When two clients issue or update the same pass, compare `generation` across polls on the same pass (never across issuers) and treat the highest observed value as canonical. To force a stale phone to refresh, re-issue once under the same `idempotency_key` — the replay folds to the pass's current state — and re-derive the holder's save link from the fresh `GET`; the platform rebuilds the Google `save_url` and Apple payload on every read, so the new link always points at the latest content. The [wallet pass lifecycle troubleshooting page](/troubleshooting/wallet-pass-lifecycle) walks the drift and the cache-busting recovery.

## Flows

### Why does my flow execution stay at `waiting`?

`waiting` is not stuck — it means the run parked on a long delay node and resumes automatically at its scheduled time; the node shows as a `running` step in the trace while it waits. To un-park it early, call `POST /api/v1/flows/executions/:id/resume` with the delay node's id and the run continues from the node wired after it. The per-node decoder (`steps[].status` — `completed` / `running` / `failed` / `skipped` / `pending`) is on [Flow Executions → status values](/flows/executions#status-values), and the full read of a problem run is on [Troubleshooting: a flow execution that failed](/troubleshooting/flow-executions-failed).

### Why did one node inside the run fail while the others completed?

That is the `completed_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](/troubleshooting/flow-executions-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](/troubleshooting/flow-executions-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](/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](/concepts/provisioning-and-test-mode).

### 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](/flows/executions), and the decode table for both messages is on [Troubleshooting: a flow execution that failed](/troubleshooting/flow-executions-failed).

## Sender identity & routing

### Which sender type should I register — 10DLC, toll-free, short code, or alphanumeric sender ID?

Pick the sender type by destination country and use case; the four sender forms differ on registration gate, throughput, cost, and two-way support. Orbit supplies the registration surfaces for every type — which posture you file, and which sender you send on, is a tenant-owned decision.

| Sender type | Registration gate | Registration time | Throughput | Two-way | Best for |
| - | - | - | - | - | - |
| **10DLC** (US 10-digit long code) | Brand + campaign filed with The Campaign Registry (TCR) | Days to \~2 weeks (depends on review) | Per-number MPS tier tied to your vetting score | Yes | US A2P: OTP, notifications, marketing |
| **Toll-free** (US/CA `8xx` number) | Toll-Free Verification (TFV) filing | \~2–4 days once the lint passes | Moderate; higher than unverified toll-free | Yes | US/CA A2P when you don't want a geographic long code |
| **Short code** (5–6 digit) | Carrier-leased; per-carrier registration filing | Weeks to months | Highest — heavy sustained MPS | Yes (US short codes reply) | High-volume marketing blasts, two-factor bursts |
| **Alphanumeric sender ID** | Pre-registered in markets that require it | Hours to days per market | Destination-dependent; one-way only | **No — one-way only** | Brand-name sender in markets that allow alpha (not US/CA) |

Selection rules by region:

* **US (and effectively Canada):** A2P SMS over a `+1` destination requires either a registered 10DLC campaign or a verified toll-free sender — carriers reject alphanumeric sender IDs outright. The [10DLC registration guide](/guides/10dlc-registration) walks brand + campaign filing, and the per-carrier throughput tiers are set by your [Brand Vetting / Vetting Score](/reference/glossary#brand-vetting--vetting-score). A rejected campaign returns per the [10DLC campaign rejection runbook](/troubleshooting/10dlc-campaign-rejection). Toll-free alternative: file the TFV verification flagged by [TFV](/reference/glossary#tfv-toll-free-verification) — a blocked filing surfaces as `TFV_REQUIRED`, covered on the [toll-free TFV runbook](/troubleshooting/toll-free-tfv-required).
* **Sender-ID pre-registration markets (India TRAI DLT, Turkey BTK, Saudi CITC, UAE TRA, others):** file the alphanumeric sender before sending; the country matrix is checked by the opt-in strict sender-ID gate — see [Strict Sender / Strict Sender-ID](/reference/glossary#strict-sender--strict-sender-id-destination-rule-gate) and the [compliance sender-ID FAQ](/compliance/faq). In those markets an alpha sender is the identifier customers recognize; in markets that don't accept alpha, the platform falls back to a numeric sender.
* **High-volume marketing in the US:** a [Short Code](/reference/glossary#short-code) is the carrier-leased 5–6 digit sender with the highest sustained throughput — pick it when per-day volume outgrows the 10DLC throughput tier your vetting score qualifies for.
* **One-way brand-name sends outside the US/CA:** an alphanumeric [Sender ID](/reference/glossary#sender-id-alphanumeric-sender-id) names your brand in the sender field — pick it for OTP and alert traffic where a reply path isn't needed.

Comparison axes behind the table: **registration time** (TCR review days vs TFV days vs carrier-lease weeks), **throughput ceiling (MPS)** (10DLC tier scales with vetting; short code is the ceiling), **cost** (10DLC long codes cheapest per-number; short codes carry lease fees; alpha IDs carry per-market registration), **two-way support** (alpha is one-way only), and **use case** (OTP alerts → 10DLC/toll-free; marketing blasts → short code; brand-name one-way → alpha). Fill the per-channel detail from the [SMS channel page](/channels/sms). For branded number presentation on the voice side see [CNAM](/numbers/cnam); for the wider per-type deep dives see the questions below.

### 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 `ORBIT123` returns `wrong_length_exact` for 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 like `BANKNOW` to a Turkish destination returns `restricted_prefix` in 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 `+1` destination under strict mode fails the request — send to US/CA recipients from a phone number or short code instead.

To revert: disable the mode the same way you enabled it — permissive behavior resumes immediately with no migration. To keep the protection: fix the sender value on the failing send path, or route through a [sender pool](/guides/sender-pools) whose members conform per destination. The advisory codes (`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](/compliance/posture-faq)). The response includes the violating country, sender, rule code, and the violating prefix as structured details. Example: a send from `"MYBRAND NAME"` to the US is accepted permissively with a critical advisory; a send from `"BANKNOW"` to Turkey returns `422 SENDER_INVALID_FOR_DESTINATION` with `matchedPrefix: "BANK"`.

### Why did my send fail with `422 SENDER_NOT_OWNED`, and what's the fix flow?

`SENDER_NOT_OWNED` is an always-on (not strict-mode gated) numeric-sender ownership check: a `from` like `+12125550198` that resolves to a different organization's number is refused pre-send, because the Devotel softswitch trusts the originator field and recipients would attribute the message to that number's true owner. The fix flow, whichever leg of resolution you came from:

1. **Send from a number your org owns** — an active DID you purchased or ported in, or a number you verified through the caller-ID register → confirm journey (see [Number identity & caller ID](#number-identity--caller-id)), passes the gate with no extra step.
2. **Or use a non-numeric identity your tenant owns** — the platform default `(Devotel)` sender, or an approved alphanumeric sender ID, skips the numeric ownership lookup entirely (subject to destination-country alpha rules; US/CA reject alpha outright, above).
3. **If the code reads `503 SENDER_OWNERSHIP_CHECK_UNAVAILABLE`, don't change anything.** That sibling means the ownership registry read was transiently unavailable and the gate failed closed — retry the same send after a short backoff instead of reworking your sender.

`SENDER_POOL_EMPTY` (422) is the sibling covered [above](#why-did-my-send-return-422-sender-pool-empty) — the routed pool's `sender_dids` array has no members; it is unrelated to ownership. The resolution chain and fallback order are on [sender resolution](/concepts/sender-resolution) and the [sender pools guide](/guides/sender-pools). For the full ownership-gate decode — the cause table, the verify flow, why the 503 sibling intentionally fails closed, and a ticket-ready error envelope — see the runbook [Sender-ownership blocked](/troubleshooting/sender-ownership-blocked).

### What does "strict" versus "permissive" mean on this page, and throughout the docs?

Throughout the compliance and sender-identity docs, "strict" names a tenant-owned, opt-in gate — a control you enable because you prefer a pre-send reject over a carrier-side failure. "Permissive" names the platform default: accept the send, surface a warning advisory when a destination rule would have flagged. That phrasing pattern holds for sender-ID rules and for suppression/opt-out gating — see [Compliance posture FAQ](/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](/reference/error-codes) and the retry-safe fix path is on the send endpoint you used.

### When does a send go over an aggregator versus a direct route — and can I mix the two on purpose?

An **aggregator route** is a wholesale trunk with carrier-approved, high-volume connectivity to hundreds of destination operators — the path a send takes when Orbit's Devotel wholesale softswitch terminates it. A **direct route** is a carrier you licensed yourself and connected as a BYO (bring-your-own) SMPP carrier. The choice is made per send, per destination, by the least-cost-routing (LCR) policy — never per message body or sender:

1. **Sender resolution** picks the identity (sender pool, explicit `from`, or the fallback chain).
2. **LCR upstream selection** ranks the Devotel wholesale route against your connected BYO carriers for that destination — by the composite score of cost, delivery quality, bind health, and a sticky last-successful-route bonus.
3. **Delivery** proceeds over the top-ranked route; route health and circuit breakers then meter it continuously.

You don't "mix" routes inside one send — one send always exits over exactly one upstream. What you *do* control is the policy that ranks the candidates: `trafficType: "cost"` orders BYO carriers strictly by cost, the default `quality` mode ranks by the composite score, `preferByo: true` puts every active BYO carrier ahead of the Devotel wholesale entry, and `candidateOrder` pins an explicit list. Preview any policy before saving it with the dry-run route quote (**Developer → SMPP → Dry-run route quote**, or `POST /api/v1/messaging/smpp/carriers/route-quote`) — it names the winning route per destination E.164 without moving traffic. A send you expected to take your BYO carrier but that instead exits on the Devotel route is almost always a missing country scope on the carrier profile, or an unhealthy bind demoting it below the incumbent. The full scoring model, policy fields, and dashboard panels are on [Least-cost routing (LCR) policy](/concepts/least-cost-routing); the aggregator reach is on the [SMS channel page](/channels/sms).

### 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 the `contacts:read` scope. See the [Interaction Search guide](/guides/interaction-search).

### What do `MMS_NANP_ONLY`, `NOT_SMS_CAPABLE`, `TFV_LINT_BLOCKED`, and `LOA_NOT_SIGNED` mean?

Four sender/asset preflight gates, all refusing before anything queues or submits. `MMS_NANP_ONLY` (422) means the MMS payload named a recipient outside the North American Numbering Plan — drop the media and cascade to SMS for that recipient, and keep MMS on `+1` destinations only. `NOT_SMS_CAPABLE` means the sender you picked has no `sms` capability for the destination country — it is in the platform's permanent retry-suppression set, so fix the sender (pre-check with `GET /api/v1/numbers/country-capabilities`), never the retry. `TFV_LINT_BLOCKED` (422) means the pre-submit content lint refused your Toll-Free Verification filing — fix the sample messages named in `error.details.lint`, test them against `POST /api/v1/numbers/tfv-lint`, then re-submit. `LOA_NOT_SIGNED` (409) means a hosted-messaging order cannot submit to the carrier until you sign the LOA with `POST /api/v1/numbers/hosted-messaging/:id/loa/sign`. The per-code cause tables, a decision tree, and ticket-ready envelope samples are on the [sender/asset preflight gates runbook](/troubleshooting/sender-asset-preflight-gates).

## Number Lifecycle

### Why is a number's capability set (voice, SMS, MMS, two-way) different per country — and where do I check before buying?

Number capabilities are per-country because the *carriers* that provision the country expose different line types and feature support: the UK may be rich in local voice-capable numbers but have zero SMS-capable mobiles; US SMS capability only exists on 10DLC-registered long codes; toll-free buckets exist in many countries with no two-way SMS at all. Orbit probes every country's inventory separately per line type (mobile, local, toll-free) × capability (voice, SMS, two-way SMS) across every upstream carrier that serves it — so the capability matrix is a property of the destination country's carrier stock, not a global flag you toggle.

Check it before you search or buy: the dashboard's **Numbers → Buy a number** picker pre-dims empty capability buckets per country off `GET /api/v1/numbers/country-capabilities?country=<ISO>` — call it yourself for the same reality check (`mobile_sms`, `local_voice`, `toll_free`, …, clamped at 100 per bucket; `two_way_sms` counts only numbers confirmed to send **and** receive). For regulated countries the same pre-check surfaces `requires_registration`, so a purchase isn't refused at checkout for a compliance reason you could have known. The full count semantics and response shape are on [Country capabilities](/numbers/country-capabilities); the regulatory leg is on [Regulatory Preview](/numbers/regulatory-preview).

### Why didn't my number purchase go through?

A failed purchase has four distinct shapes — work through them in order:

1. **`402 INSUFFICIENT_BALANCE`** — the wallet pre-flight rejected the call before any carrier order. The response names both amounts (`required_cents` / `available_cents` in the error `details`), 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.
2. **Inventory race** — a `409 NUMBER_ALREADY_TAKEN` means another claim reached checkout between your search and purchase. The dedicated question below has the full retry semantics.
3. **Compliance-pool eligibility** — a regulated country (`requires_registration: true` on `GET /numbers/available`) rejects the purchase with `422 COMPLIANCE_PROFILE_REQUIRED` or `422 COMPLIANCE_PROFILE_NOT_APPROVED` unless you pass an approved profile id as `compliance_profile_id`. Check eligibility up front with [Regulatory Preview](/numbers/regulatory-preview).
4. **`pending_compliance` hold** — the purchase succeeded but the number sits parked until an approved compliance profile is attached. Attach one with `POST /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).

On webhooks: subscribe to `number.purchased` for the success signal and `number.released_compliance_timeout` for a compliance-deadline release (the full event catalog is in [Webhook Events](/reference/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](/guides/buy-numbers); pending gates are covered in [Troubleshoot a pending number or Sender ID](/compliance/troubleshooting-pending-gated-surfaces).

### Why did my number purchase return `409 NUMBER_ALREADY_TAKEN`?

That 409 is an inventory race, and it is deterministic — not a billing error, not a carrier failure, and not something you wait out. Two claims for the same DID raced: a different tenant, or two of your own concurrent purchase calls, both passed `GET /api/v1/numbers/available`, and checkout serialized so only the first claim won. The loser got this response **before a carrier order was placed**, so no charge was taken and the DID claim was never registered — which is what makes the retry below safe. (The same race can also surface as `409 NUMBER_NO_LONGER_AVAILABLE`; either way, the number was claimed between your search and your buy.)

Retry by re-running the search, then re-buying:

1. Call `GET /api/v1/numbers/available` again — inventory moves by the minute, and a row you saw is not a reservation.
2. Pick a still-listed number from the fresh result.
3. Call `POST /api/v1/numbers/buy` with that number. The response returns `debited_cents` only for the rows that succeeded; any row that lost the race comes back in the `failed[]` array with the error code.

Bulk purchases carry the same semantics per row: a `POST /api/v1/numbers/buy-bulk` call returns `200` and reports each number in `succeeded` or `failed[]` — gate on `succeeded.length`, not the status code. Failed rows are never charged (`debited_cents` counts only `succeeded` rows), so re-submitting exactly the failed set in a new call is billing-safe. Do not replay the whole batch — re-send only the failed E.164s.

To stop racing at all, rehearse the two calls back-to-back at checkout time: fire `GET /api/v1/numbers/available` immediately before the buy call and accept one of the nearest results instead of a number you cached minutes ago. If a `buy-bulk` batch matters to you, re-run the pre-flight inside the same request window.

The destination runbook for the whole checkout-failure lane — inventory races, the quarantine nuance on your own recently-released numbers, and the post-checkout `502 NUMBER_PROVISIONING_FAILED` rollback — is [Troubleshoot number purchase failures](/troubleshooting/numbers-provisioning-failed); the endpoint contract itself is on [Numbers overview](/numbers/overview).

### Why did my bulk purchase come back with `CARRIER_RATE_LIMITED` rows?

A `POST /api/v1/numbers/buy-bulk` batch processes row by row. When one row hits a carrier-side rate limit (HTTP 429), Orbit stops pulling new rows — every remaining row comes back in `failed[]` marked with `error.code = "CARRIER_RATE_LIMITED"` without another carrier call, so a partially-completed batch is a normal, recoverable shape. The response is still `200 OK`, so gate on `succeeded.length`, never the status code. Retry **only** the `failed[]` slice (exactly those E.164s) in a new call a few seconds later — failed rows are never charged, and re-running the whole batch re-attempts rows that already succeeded. If the same pattern recurs after a handful of spaced retries, escalate to support with the exact `meta.request_id`, the batch `order_id`, and the carrier from `error.details.carrier` when present. The full runbook is [Troubleshoot CARRIER\_RATE\_LIMITED on bulk number purchase](/troubleshooting/bulk-purchase-carrier-rate-limit).

### Why did my number suddenly disappear and my wallet get refunded?

That is the compliance-deadline auto-release sweep: a regulated-country purchase sits at `pending_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](/reference/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](/compliance/troubleshooting-pending-gated-surfaces) and the [Number status map](/concepts/number-lifecycle).

### My number is `pending_compliance` — how do I unblock it?

A number at `pending_compliance` is blocked from sending or receiving until an approved compliance profile is attached to it — attach one with `POST /api/v1/numbers/:id/attach-compliance-profile` and the carrier webhook flips it to `active`. If the deadline passes first, Orbit auto-releases the number (see the auto-release question above). Two write-time gates keep the profile from ever reaching approvable state: `409 COMPLIANCE_PROFILE_LOCKED` when you edit or re-submit a profile that is already in review, and `422 COMPLIANCE_PROFILE_INCOMPLETE` when required fields or documents are missing pre-submit — both are answered in the Compliance section below. The entrance point for the whole pending-gated surface — status vocabulary, recovery paths, and the auto-release sweep — is [Troubleshoot a pending number or Sender ID](/compliance/troubleshooting-pending-gated-surfaces).

### 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](/numbers/lifecycle) carries the full recovery toolkit: re-submitting a rejected or expiring compliance profile (the re-verify round), reclaiming a parked number inside its grace window with `POST /api/v1/numbers/:id/reclaim` (returns `409` when the window has lapsed or the number is not parked), re-trying a failed carrier release with `POST /api/v1/numbers/:id/retry-release`, and re-attaching a partial messaging profile or voice connection with `POST /api/v1/numbers/:id/repair-provisioning`. Attach the approved profile — or complete whichever of those re-verify paths fits — before the grace deadline lapses; once the sweep runs, the only recovery is a fresh purchase, and the `number.released_compliance_timeout` webhook is the signal to route into that workflow. Both the attach and the reclaim path are tenant-owned controls you trigger on your own schedule.

### I tried to send to a contact and got `422 CONTACT_ERASURE_PENDING` — what lifecycle owns it?

The contact is inside an in-flight Article-17 erasure: a pending or executing erasure request exists for them, and the platform refuses every outbound send to that contact until the window resolves — a campaign recipient is marked `skipped` with `reason: "erasure_pending"` (no wallet deduction held on it), and a direct API send gets the structured `422` back with `details.contact_id` so you can deep-link to the contact. The gate is tenant-owned: it is your erasure request blocking your own send, not a platform compliance verdict.

Two resolutions, both operator actions from your side: cancel the erasure from the contact's profile — `POST /api/v1/contacts/:id/gdpr/erasure-request/:requestId/cancel` — and sends to that contact unblock immediately, or let the cooling-off window (7 days by default) run out; the scheduler then hard-deletes the contact, after which any send to the phone number recreates nothing and behaves like a send to an unregistered recipient. Do not retry-loop the `422` — the refusal is deterministic for the life of the request. The cancel and wait options, plus the terminal `DSAR_NOT_CANCELLABLE` gate, are in the [erasure question above](#my-erasure-post-returned-erasure_cooling_off_active--can-i-cancel-or-wait), and the send-side codes are in the [Error Code Reference](/reference/error-codes).

## Number identity & caller ID

### Why did my API call reject a phone number with an E.164 validation error?

A non-E.164 number is refused before anything is dispatched — a deterministic `422` (or `400` on some surfaces) with a `VALIDATION_ERROR`-class code and a message naming the failing field, for example `"The 'to' field must be a valid E.164 phone number"`. The gates all enforce the same shape — `+` followed by 2–15 digits with a non-zero country code — and they run pre-send (nothing is billed, nothing is dispatched): the send endpoints (`POST /api/v1/messages/sms` and the per-channel variants), the caller-ID register (`POST /api/v1/voice/caller-ids/verify`), voice dial endpoints, flow send nodes (`to must be a valid E.164 phone number`), and the LCR route-quote. Because the gate is deterministic, retrying the same body fails identically — fix the value first.

Normalization is a client-side step: accept the national format a user typed, normalize to E.164 (a libphonenumber port in your stack) at collection or import time, and pass only the normalized form on your API surface. When a stored value can't be trusted, resolve it with the lookup surface — `POST /api/v1/numbers/lookup` returns `valid: false` for anything unparseable and the national format of anything that parses (see [Number Lookup](/numbers/lookup); the data-model split between a validated number and an enriched number is on [Data model](/concepts/data-model)). On a refused send, the Delivery Log row carries the number as-sent plus the validation code, so the audit trail names exactly which value your integration passed. The code family is in the [Error Code Reference](/reference/error-codes), and the shared E.164 shape is answered above in [messaging](#what-phone-number-format-does-orbit-use). The whole decode-pre-flight-fix walkthrough — including JS/Python E.164 formatter snippets and the `from_number` source-resolution post-check — is on the [validation gates runbook](/troubleshooting/validation-gates).

### Why was my send rejected with a validation gate code?

Four codes sit in this deterministic family, all refusing **pre-send** with nothing billed and nothing dispatched — and all named in the [Error Code Reference's Validation section](/reference/error-codes#validation): `INVALID_PHONE_NUMBER` (a phone field isn't E.164), `INVALID_FROM_NUMBER` (the `from_number` source id you passed is missing, disabled, or malformed), `MISSING_REQUIRED_FIELD` (a required field is absent from the body), and `INVALID_EMAIL` (the email recipient address is malformed). The error envelope always tells you two things: `code` says which gate tripped, and `details.field` / `details.value` echo the exact value your integration sent. Any mention of these codes — the FAQ and the Error Code Reference's *From a code to a runbook* routing table both do — resolves to one destination: the [validation gates runbook](/troubleshooting/validation-gates), which decodes each envelope, gives the `GET /api/v1/numbers/lookup/{phoneNumber}` pre-flight and the E.164 formatter snippets, walks the per-code rejected-and-corrected request body, and ends with the `from_number` source-id resolution post-check. Fix the named field and re-send exactly once — a retry loop on an unchanged payload is the most common way this family gets worse, not better.

### Where does the logo or brand name a recipient sees come from?

It depends on the channel — there is no single "brand asset" that renders everywhere:

1. **Voice — CNAM.** A 15-character uppercase displayed name the handset pulls from the carrier LIDB lookup per call. Register it per number with `PUT /api/v1/numbers/:id/cnam`; a sibling subaccount cannot read or mutate another tenant's registration. Full contract: [CNAM & Caller ID](/numbers/cnam).
2. **Voice — RCD branded calling.** A verified brand name (up to 40 characters), a square logo, and a reason-for-call line rendered on the incoming-call screen, carried cryptographically inside the signed STIR/SHAKEN PASSporT. It requires A-attestation on the from-number (the number must be an active platform-owned number), so an external caller ID never carries it; when RCD drops, the recipient still sees the attestation result and any CNAM name on file. See [Branded Calling (Rich Call Data)](/compliance/branded-calling).
3. **RCS — the agent's brand card.** The business name, logo, and color a recipient sees in the rich-messaging thread come from the **RCS agent registration** you submitted (`brand_logo_url` on the agent registration, a public HTTPS-hosted square image) — verified or launched agents only. See the [RCS channel page](/channels/rcs).
4. **SMS sender IDs.** No logo exists on SMS; presentation is the alphanumeric Sender ID itself, which several countries require you to register per destination before traffic delivers — see [Sender-ID Registration](/compliance/sender-id-registration).

The roll-up posture across all of these (plus 10DLC, toll-free, and WhatsApp Business) is the trust score on `GET /api/v1/brand-identity/status` — the model is on [Brand identity & trust score](/concepts/brand-identity-trust-score).

### Why did my outbound voice/SMS refuse with `UNVERIFIED_CALLER_ID`?

The pre-send gate rejected the `from` because the number is neither an active platform-owned DID on your org nor a row you have verified as your own. The refusal happens before any carrier dispatch, so no wallet deduction is taken. You have three fixes:

1. **Send from a number your org owns** — an active DID you purchased or ported into Orbit passes the gate with no extra step.
2. **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).
3. **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](/reference/error-codes) under Voice; the send-side contract is in the [Voice API reference](/api-reference/endpoints/voice).

### 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:

1. **Register** — `POST /api/v1/voice/caller-ids/verify` with the E.164 `phone_number`, an optional `friendly_name`, and `channel: "sms" | "voice"`. Orbit sends a one-time code to that handset (TTS readback on voice, a text message on SMS).
2. **Confirm** — `POST /api/v1/voice/caller-ids/confirm` with the `verification_id` and the code. A correct code flips the row to `verified` and opens the number as an outbound `from`.
3. **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 with `POST /api/v1/voice/caller-ids/:id/resend`. If the challenge dispatch itself fails, the endpoints raise `VOICE_CHALLENGE_FAILED` with the infrastructure detail redacted — re-run the challenge (register or resend) rather than debugging the redaction.

The same flow runs from the dashboard: **Voice → Caller IDs** registers the number, carries the confirm step inline, and shows each row's lifecycle (`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](/guides/voice-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](/numbers/cnam) sets *what name* carriers display next to it.

### What is the trust registry, and what does its lookup answer?

The **trust registry** is the public, per-organization opt-in lookup endpoint that answers "is this caller or agent really this brand, and what is it delegated to do?" from outside your tenant. One unauthenticated GET (`/api/v1/public/.well-known/ans/registry/lookup?org=<slug>&agentId=<id>` or `&ans=<uri>` or `&phone=<e164>`) composes the verification signals Orbit already computes for your organization into one resolve: the brand-identity rollup and trust score, the STIR/SHAKEN attestation tier (resolved with the same logic the dial path stamps on the egress INVITE, so the registry reports the egress-time decision rather than a guess), and the delegated capability scopes of your active API keys (read through the default-deny scope ledger — key ids, hashes, and key material never cross the boundary).

Publication is a **tenant-owned** control per organization: enable the public trust-registry flag, and the lookup answers; leave it off, and unknown organizations, opted-out organizations, and malformed lookups all collapse to the same `404 AGENT_TRUST_NOT_FOUND` envelope, so a probe can neither enumerate tenants nor learn who publishes. Responses are capability and verification metadata only, edge-cached for 60 seconds — short enough that a newly revoked agent or key drops out within a minute.

The platform-side anchor this registry complements is the **ANS (Agent Name Service) Trust Card** — the signed, domain-anchored identity document served at `/.well-known/ans/trust-card.json` that declares Orbit's own ANS URI, endpoint surfaces, and Ed25519 signing-key set, so a resolver can verify an outbound request from Orbit's agents against published keys. The full model — why caller identity needs a verifiable anchor, the four-step card verification flow, and how to raise your organization's attestation posture — is on [ANS Trust Card and trust registry](/concepts/ans-trust-card-registry).

### Why did my caller-ID rewrite get rejected on my own SIP trunk with `VOICE_CALLER_ID_REJECTED`?

Your trunk's outbound caller-ID rule set rejected the value — it is neither on the trunk's `allowedCallerIds` list nor an org-owned DID. The guard exists so a rewrite rule cannot spoof an arbitrary number as your FROM. Two fixes, both tenant-owned:

* **Add the number to the trunk's allow-list** — set `allowedCallerIds` on the SIP-trunk create/update call (the [SIP trunks endpoints](/api-reference/voice#create-sip-trunk)) 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.

The same guard runs when you save the trunk, so a rejected value is caught at write time as well as at dispatch — the rejection surfaces are described in the [Error Code Reference](/reference/error-codes) under Voice.

### 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 `from` gets the `UNVERIFIED_CALLER_ID` refusal 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 treat `revoked` as 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 with `resend`, same as an `expired` row.

The store behind this is a per-tenant table with one row per (org, number) — the same number verified by another org is independent of yours, and your revoke affects only your tenant's row.

## Number Porting

### My port seems stuck — where do I look?

Call `GET /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](/numbers/porting) for endpoint detail and the [Port a number end-to-end guide](/guides/port-numbers) 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](/numbers/porting) is the authoritative estimate — an expectation, not a guarantee.

### The number I ported out got rejected — what do the codes mean?

The port-out rejection family splits by who refused and when:

* **`401 PORT_OUT_PIN_MISMATCH`** — your own PIN gate refused it. Port-Out Protection is a tenant-owned per-number PIN, and when it is enabled the release call must carry the matching `port_out_pin`; a missing or wrong value is rejected **before** anything is dispatched to the carrier, so a bad PIN never burns a carrier attempt. The PIN is 4–32 characters, hashed at rest, and never returned by any endpoint — `GET /api/v1/numbers/:id/port-out-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.
* **`409 CONFLICT`** — the number is not in a portable state (`port_out_pending`, `released`, `suspended`, `parked`), or another port-out for the same number is already in flight. The in-flight claim is atomic, so a `409` almost always means "someone already submitted this" — check the list endpoint before retrying.
* **`422 PORT_OUT_NOT_SUPPORTED`** — this number's carrier has no self-serve port-out adapter. Nothing was dispatched; run the port in the carrier's own portal instead.
* **`502 CARRIER_PORT_OUT_FAILED`** — the carrier rejected or errored the order. The number's local status rolls back to its prior state, so nothing is stranded in a phantom pending — the number stays yours and a corrected submission is safe to re-run.

A correct release carries the losing-carrier account, the authorized signer, the target OCN, and the PIN when protection is on:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/num_abc123/port-out \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "losing_carrier_account_number": "ACC-99182",
    "authorized_person": "John Doe",
    "target_carrier_ocn": "1234",
    "port_out_pin": "4821"
  }'
```

The full runbook — pre-flight, PIN rotation, batch flow, FOC tracking, and the cancel window — is on [Port out safely](/guides/port-out-numbers); endpoint detail is on [Numbers overview](/numbers/overview) and the ownership model on [Port-out lifecycle and ownership model](/concepts/number-port-out-model).

### 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](/reference/glossary) concept lays out. Freeze first, validate second, release last:

1. **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 with `PORT_OUT_PIN_MISMATCH` unless 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.
2. **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.
3. **Release.** Present the PIN with the port-out payload only once the authorization checks out; until then the mismatch gate holds.

While you vet, watch for the three port-out rejections in the [Error Code Reference](/reference/error-codes): `PORT_OUT_PIN_MISMATCH` (401) is the PIN gate above; `CARRIER_PORT_OUT_FAILED` means the carrier rejected or failed the order — the claim rolls back and the number stays yours; and `PORT_OUT_NOT_SUPPORTED` (422) marks a number on a provider with no self-serve port-out adapter, which moves through support instead. Theft-by-port (slamming) is exactly the fraud class the PIN gate exists to stop — the step-by-step runbook for all three rejections is [Troubleshoot port-out rejections](/troubleshooting/port-out-blocked), and the full ownership model is [Port-out lifecycle and ownership model](/concepts/number-port-out-model).

### What is the maximum message size?

| Channel | Limit |
| - | - |
| SMS | 1,600 characters (multi-segment) |
| WhatsApp | 4,096 characters |
| RCS | 4,096 characters |
| Email | 25 MB (including attachments) |

***

## 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 with `RECIPIENT_OPTED_OUT` (see the [Error Code Reference](/reference/error-codes)). 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](/api-reference/endpoints/opt-out-lists) or the [Opt-Outs API](/api-reference/optouts).

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](/compliance/consent-management), their own START reply, or a [Preference Center](/compliance/send-gates#preference-center) opt-in (see [Removing a suppression (re-opt-in)](/compliance/opt-out-suppression#removing-a-suppression-re-opt-in)).

See [Opt-Out & Suppression Lists](/compliance/opt-out-suppression) and [Send Gates](/compliance/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](/compliance/opt-out-suppression#bulk-csv-import).
* **Batched opt-outs API** — if your export is contacts keyed by channel, `POST /api/v1/contacts/optouts/bulk` accepts up to 500 rows per call and is idempotent: rows already opted out come back as `skipped`, not errors. See the [Opt-Outs API](/api-reference/optouts).

Both paths are tenant-owned records — you control which addresses enter your list; Orbit enforces it at send time. See [Opt-Out & Suppression Lists](/compliance/opt-out-suppression) and [Send Gates](/compliance/send-gates).

### 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 scope `all` — 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](/compliance/opt-out-suppression#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](/api-reference/optouts)).

See [Opt-Out & Suppression Lists](/compliance/opt-out-suppression) and [Send Gates](/compliance/send-gates).

***

## Voice

### What codecs does Orbit support?

Orbit supports OPUS, PCMU (G.711 µ-law), and PCMA (G.711 A-law) — negotiated OPUS → PCMU → PCMA in that preference order per leg, with OPUS recommended for the best quality at lower bandwidth. The trade-offs are defined in the [glossary](/reference/glossary) entry **Codec (Voice Codec)**.

### How do DNIS routes override inbound routing?

They don't override it — they sit *under* it. Resolution for an inbound call runs in a fixed order, and the first hit wins: a **per-number route** on the dialed DID always beats every DNIS pattern rule, and a DNIS rule only fires when no per-number row exists. Among your DNIS rules the resolver ranks them deterministically — lowest `priority` first, then the longest (most specific) pattern, then the oldest rule — and fall-through beyond that is your org default, then the safe default. So "overriding" a DNIS pattern on one line means adding a per-number route for that DID; the pattern keeps covering the rest of the pool untouched. Manage both on **Voice → DNIS pattern routing** and the inbound-routing surfaces; the full precedence chain is on [Inbound Voice Routing](/concepts/inbound-voice-routing) and the [DNIS pattern routing guide](/guides/dnis-pattern-routing).

### 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](/api-reference/voice#create-sip-trunk), 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.

Diagnose a failing trunk on the [SIP-trunk troubleshooting page](/channels/voice/sip-trunks-troubleshooting).

One prerequisite: ensure your carrier supports OPUS, PCMU (G.711 µ-law), and PCMA (G.711 A-law) — see [the codec question above](#what-codecs-does-orbit-support).

### 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 OK` carries no audio guarantee. (See **Codec (Voice Codec)** in the [glossary](/reference/glossary) for the OPUS / PCMU / PCMA trade-offs.)

Work the full decision path per symptom on the [voice call-quality troubleshooting page](/troubleshooting/voice-call-quality).

### 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](/troubleshooting/sip-trunk#capacity-gates).

### Why did outbound calls stop using my SIP trunk?

When the trunk's last registration probe reads anything other than `registered`, the dial path stops offering it new calls: it walks your configured failover chain for a registered spare, and with no usable candidate it falls back to the Devotel wholesale softswitch so calls keep terminating. Re-test the trunk (`POST /api/v1/voice/sip-trunks/:id/test` or **Test trunk** in the dashboard), read `lastRegistrationError` for the named cause (DNS, `401` on rotated credentials), and confirm the failover chain points at a registered trunk. Older client integrations may show this as a `503 SIP_TRUNK_UNREGISTERED` — the current gate falls back rather than refusing. Full runbook: [Troubleshooting: SIP trunk — unregistered at dial time](/troubleshooting/sip-trunk#unregistered-trunks-at-dial-time).

### Why did my outbound call reject with `422 VOICE_DNO_BLOCKED`?

Orbit's origination-time **Do-Not-Originate (DNO)** gate rejected the resolved `from` because it matches an entry on your organization's DNO list (or the platform env baseline baked into the deployment) — the pattern a DNO entry encodes is an invalid, unallocated, inbound-only, or spoof-bait caller ID (spoofed government/bank lines, inbound-only toll-free, unassigned ranges) that must never be presented as a calling-party number. The gate runs before any billing hold or dispatch, so the refuse takes nothing from your wallet. Two numbers to keep straight: this is a refusal on *your own list*, and it is a tenant-owned control — the per-organization list **ships empty**, so a 422 here means someone in your organization configured the override (or your deployment's operator curated the env baseline).

The list reconciles a platform env baseline with a per-organization override in one of three modes, all living at `organizations.settings.voice.dno_override` (owner/admin write, `PUT /api/v1/settings/general` — or the organization's voice settings in the dashboard): **`extend`** (default — your entries add to the baseline), **`replace`** (your entries replace the baseline entirely), **`subtract`** (your entries are removed from the baseline). Fix paths, in order:

1. **Correct the `from`** — read `details.matched_prefix` on the error envelope to see which entry fired, and re-dial from a number you legitimately own.
2. **Adjust the list if the block is wrong** — when a baseline entry wrongly matches a number you legitimately own, carve it back with the `subtract` mode; when your own entry is too broad (short prefix hitting a whole range), tighten or remove it.

Keep the look-alikes separate: DNO is the **source-identity** spoofing gate on the caller ID you present — different from **DNC** (Do-Not-Call), which is the recipient-side opt-out registry, and from `UNVERIFIED_CALLER_ID`, which is the ownership check on the `from` (see the [caller-ID question above](#why-did-my-outbound-voicesms-refuse-with-unverified_caller_id)). The config guide is [Do-Not-Originate (DNO) caller-id blocking](/compliance/do-not-originate), triage of the code and its sibling voice gates is on [Troubleshooting: voice destination and emergency blocks](/troubleshooting/voice-destination-blocks), and the code family is in the [Error Code Reference](/reference/error-codes).

### How do AI voice agents work?

Orbit's voice agents combine:

1. **STT** (Deepgram Nova-3) — converts caller speech to text
2. **LLM** (Anthropic Claude) — generates a response
3. **TTS** (Cartesia Sonic 3.5) — speaks the response back

The entire round-trip typically completes in under 500ms.

### Why does registering a voice-inference provider key return `422 VOICE_CREDENTIAL_PROVIDER_REJECTED`?

`PUT /api/v1/voice/voice-provider-credential/{kind}` and `POST /api/v1/voice/voice-provider-credential/{kind}/rotate` both probe the credential live with the provider before persisting it. If Cartesia (TTS) or Deepgram (STT) refuses the key — typically with a 401 or 403 — Orbit returns `VOICE_CREDENTIAL_PROVIDER_REJECTED` and does **not** save the key, so it can never flip to `active` or get enforced. The fix is always on the provider side: re-issue or re-scope the key in the provider's own dashboard, re-copy it without whitespace, and retry. A key that works in the provider console but fails here usually lacks the permission the voice pipeline needs (transcription scope for Deepgram, synthesis scope for Cartesia). The full cause table, envelope sample, and ticket checklist are on [Troubleshooting: voice-model credential rejected](/troubleshooting/voice-provider-credential-rejected).

### Why did my voice-clone creation return `422 VOICE_CLONE_CONSENT_REQUIRED`?

A voice clone trains a custom TTS voice from a real person's recorded audio, so the gate refuses the creation until you prove two consent classes: **recording consent on the source call** (an active `consent_state='granted'` receipt for the recorded call the sample comes from — the audio itself never existed lawfully without it) and **a voice-owner attestation** (the cloned person's signed voice release or written consent, described in the free-text attestation plus the `biometric_consent_acknowledged` flag). Uploading a raw sample (`POST /api/v1/voice/clones/upload`) skips the recording-consent receipt but carries the same voice-owner attestation bar; a clone **designed from a text prompt** never trains on a real voice, so neither consent class applies. Once the clone exists, dialing with it is a separate gate — outbound calls that speak a cloned or synthesized voice need the recipient's own written consent (see [FCC AI voice written consent](#why-did-my-voice-call-reject-with-fcc_ai_voice_written_consent_required-even-though-the-recipient-consented-verbally)). The full enrollment flow — attestation fields, one- and two-sided consent classes, watermarking, and cloning-credit pricing — is on [Enroll voice clones from calls](/guides/voice-clones).

### 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](/compliance/consent-management) 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](/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](/reference/error-codes) 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](/voice/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:

| Value | Behavior |
| - | - |
| `skip` | Allow the call. **Default** for non-voice channels and presumed-non-US recipients. |
| `deny` | Block the call (fail-closed). **Default** for outbound voice to a US (`+1`) recipient whose timezone can't be resolved. |
| `enforce_utc` | Evaluate the call against the UTC clock — 8 AM–9 PM UTC. |

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](/troubleshooting/tcpa-window-blocked-calls)) 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](/troubleshooting/hot-desking-sign-in-rejects); all five codes are in the [Error Code Reference](/reference/error-codes) 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](/api-reference/voice#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 with `409 HOTELING_DOUBLE_BOOKED`, not at sign-in. The reservation model and the no-show sweep are in the [hot-desking guide](/guides/voice-hot-desking).

### How do DNIS routes override inbound routing?

They don't — a DNIS pattern is the fallback layer under your per-number configuration, and the precedence order is fixed. Create DNIS routes under **Voice → DNIS routes** (`/voice/dnis-routes`), and precedence there is shown right on the row. At call setup, resolution runs in order:

1. A **per-number route** on the exact dialed DID always wins, no matter how low a pattern's priority sits.
2. Among matching DNIS patterns, **priority ascending**, then the most specific pattern, then the oldest entry, breaks the tie.
3. With no per-number route and no pattern match, the **org default destination** answers, so a call never ends unrouted.

One E.164-prefix or regex rule can steer a whole DID block to one queue or IVR. DNIS routing is inbound-only — it never changes how outbound calls terminate. The rule and curl-shape details are on [DNIS pattern routing](/guides/dnis-pattern-routing), the resolution order on [Inbound voice routing](/concepts/inbound-voice-routing), and the endpoint contracts on [Voice API → DNIS routes](/api-reference/endpoints/voice).

### How do paging groups reach desk phones?

A paging group holds one-tap broadcast members of two kinds: a **SIP extension** (a registered desk phone the page rings directly) or a teammate on the **in-browser softphone** (delivered as an in-app page). Create the group under **Voice → Paging groups** (`/voice/paging-groups`) or with `POST /api/v1/voice/paging-groups`, then trigger the announcement with `POST /api/v1/voice/paging-groups/{id}/page`.

The desk-phone reachability rule: the page only reaches a SIP extension whose credentials are live-registered (the page delivery report counts drops when a device is offline or unregistered). Device registration is the prerequisite — provision them under **Voice → Extensions** with QR-code or config-file provisioning and track live registration there; the full flow is [Voice extensions (SIP credentials)](/voice/extensions), and the group mechanics, member limits, and paging-versus-ring-group trade-offs are in [Broadcast paging with paging groups](/guides/paging-groups).

### What does the STT playground measure?

Before you commit a speech-to-text vendor to a voice agent, the **STT Playground** (`/voice/stt-playground`) transcribes one short clip — recorded from your microphone or uploaded — against every STT vendor Orbit recognizes and reports each vendor's **transcript**, **confidence score**, and **processing latency** side by side. A vendor that is recognized but not yet provisioned is shown as *Not yet available* rather than a fabricated result. Nothing you record or upload is persisted server-side — the clip is ephemeral, so the playground is safe for a quick comparison. The full walkthrough is in [STT Playground: compare speech-to-text vendors](/guides/stt-playground).

### What is VAQI and how is it computed?

**VAQI** (Voice Agent Quality Index, **Voice → Voice Agent Quality** at `/voice/vaqi`) aggregates every **completed** voice-agent session in a selectable window (24 h, 7 d, or 30 d) into per-session-mechanics rollups, auto-refreshing every 30 seconds:

* **Latency** — time to first byte (TTFB) and turn gap, each reported as a mean plus p95 across per-session averages, with component cards that split the time into average STT, LLM, and TTS latency so a regression names its stage.
* **Turn-taking** — average caller↔agent turns per session.
* **Barge-in** — the share of turns where the caller interrupted the agent, and the **clean-yield success rate** of those interruptions (the agent stopped and yielded instead of talking over the caller).

The dashboard warns at p95 TTFB over 1200 ms and p95 turn gap over 1500 ms — treat those as starting points, not your SLO: baseline on a 30-day window, then alert against your own thresholds. The formula details, threshold table, and regression-triage walkthrough are in [Read the Voice Agent Quality Index (VAQI)](/guides/voice-agent-quality-vaqi); the per-session rollup is also readable per call on `GET /api/v1/voice/vaqi`.

### What happens when an agent presses the panic or distress button during a live call?

The softphone sends `POST /api/v1/voice/agents/me/distress` from the authenticated agent session. Orbit first persists a durable, per-tenant incident row, then forces recording on for that call regardless of queue policy, publishes `agent.distress.raised` to the omnichannel supervisor stream, and appends an audit row with the severity, reason, and recording outcome. The event pages supervisors on the [Supervisor live monitoring](/guides/supervisor-live-monitoring-voice) surfaces; dismissing the alert does not remove the incident or its audit evidence; both rows follow the tenant's standard retention windows, and pressing the button does not consume credits. The `/me/` scope prevents an agent from raising an alert for another agent. See the [Agent distress alert model](/concepts/agent-distress-alert-model) for the sequence and retention details, and the [Voice API reference](/api-reference/endpoints/voice) for the endpoint contract.

### Why is my agent stuck in wrap-up even after the call ended?

A disposition gate blocks the agent's `busy → available` flip until a wrap-up row exists for the call — the agent posted (or their softphone auto-posted) none, so the queue refuses to release them back into dispatch. The three refusal codes decode different layers of that gate:

* **`DISPOSITION_REQUIRED`** (422) — `POST /api/v1/voice/agents/:id/status` with `state: "available"` was refused because the queue has `require_disposition: true` and no disposition row exists for the agent's most recent terminal call. The fix is the missing write: an agent or supervisor POSTs `/api/v1/voice/queues/:queueId/calls/:callId/disposition` (idempotent on `(queue_id, call_id)` — safe to retry), and the status flip is then accepted. A background sweep will auto-flip a still-stuck agent after the queue's wrap-up ceiling — but writing the disposition is the resolution, not waiting out the sweep.
* **`DISPOSITION_CODE_NOT_FOUND`** (404) — your disposition POST named a `dispositionId` / `dispositionCode` that is not an **active** row in that queue's wrap-up reason catalog. Per-queue catalogs are tenant-owned: an owner/admin re-adds the code with `POST /api/v1/voice/queues/:queueId/dispositions`, and agents pick it again from `GET /api/v1/voice/queues/:queueId/dispositions`.
* **`DISPOSITION_TAG_NOT_FOUND`** (404) — your POST named a `tagSlug` / `tagId` that does not resolve to an active row in the **tenant-wide** tag catalog. Tags are a separate catalog from the per-queue wrap-up codes — the 404's `details.missingSlugs` / `details.missingIds` names exactly which ones missed, so re-add the tag (or fix the slug you sent) on the tag-catalog surface, not the queue catalog.

The gate mechanics, the two catalogs (per-queue codes vs. tenant-wide tags), and the auto-release sweep are on [Disposition and ACW model](/concepts/disposition-and-acw-model); the tag taxonomy surface is on [Voice → call disposition tags](/voice/call-disposition-tags), and the code family is in the [Error Code Reference](/reference/error-codes) under Voice.

### Why did flagging a call for malicious-call trace return `400 MCID_INVALID_DIRECTION` or `409 MCID_CALL_TOO_OLD`?

Both codes are eligibility gates on the same operation — `POST /api/v1/voice/mcid/:callId` — so decode them by which gate refused you. **`400 MCID_INVALID_DIRECTION` means the call you named was not inbound.** Malicious Call Identification (the `*57` feature code on NANP, a regulated UCaaS service) exists to let a *callee* report a threatening or nuisance *caller* and preserve the originating identity for law-enforcement or an abuse team — flagging is therefore inbound-only, and an outbound call id is refused before any record is written; the flag path itself never originates or signals a leg of its own, so all outbound traffic continues to exit only through the Devotel softswitch. Re-check the call's `direction` on the call detail page before flagging. **`409 MCID_CALL_TOO_OLD` means the call fell outside the recent-eligibility window** — flagging is bounded to the trailing seven days, because the carrier-side CDR/SIP trace trail that makes a trap-trace actionable for an abuse team degrades with age, and a stale historical call can't be retroactively "traced" once that trail has aged out. Flag nuisance calls promptly: from the call/recording detail page in the dashboard, or by dialing `*57` from the receiving softphone — both write the same tamper-evident audit entry. A flag that landed already returns the existing record on a re-POST (idempotent), not an error. The endpoint surface — flag the call, read its state, and pull the owner/admin regulated export — is documented in [Voice API → MCID](/api-reference/voice#create-mcid).

### Why did my payment-capture step return `403 KBA_VERIFICATION_REQUIRED`?

The sensitive action you called — payment capture, PII disclosure, or an account change on a live call — gates on a verified knowledge-based-authentication session for that call, and none was verified at request time. Three states decode to the same 403, so check the session with `GET /api/v1/voice/kba/:callId` before changing anything: **pending** — the challenge was started (`POST /api/v1/voice/kba/:callId/start`) but the caller's answers were never submitted or a factor mismatched with attempts still left; submit the correct answers on `/verify` and retry your action. **failed** — the bounded attempt budget (three by default) burned out and start now answers `409 KBA_LOCKED`; the only way to re-open the gate is a deliberate `POST .../start` body of `{ "restart": true }`, which starts a brand-new challenge — no stray retry routes around the lockout. **expired** — the verified session outlived its TTL (four hours on the per-call ledger), and status computes at request time so a stored `verified` flag past expiry reports as `expired`, never as verified; run the challenge again while the caller is still on the line. On the stateless token pair the same gate reads via `POST /api/v1/voice/kba/session/check` on your `session_token` — those tokens carry a 15-minute signed expiry and must be minted per call. Session semantics, factor selection, and the attempt budget are on [KBA caller verification](/concepts/caller-verification-kba); the end-to-end runbook with curl for both endpoint families is [Verify a caller with KBA on a live call](/guides/kba-caller-verification).

***

## Video

### Why did my video poll fail to launch — or why did a vote come back with an error?

Polls in a video room ride the room's realtime data channel, so launch and vote failures share one cause class: the room's realtime connection could not carry the message. Distinguish the two codes by which action refused:

* **`VIDEO_POLL_LAUNCH_FAILED`** (500) — `POST /api/v1/video/rooms-scheduled/:id/polls` failed to broadcast the launch to the room. Launch is a host moderation action, so before retrying confirm the caller holds a host-capable role: owner/admin (or a participant minted with the `host` tier) — the write guard refuses other keys before the broadcast is even attempted. The refusal happens before any room state changes, so no phantom poll exists: fix the caller's key, then retry once after a short backoff.
* **`VIDEO_POLL_VOTE_FAILED`** (500) — `POST /api/v1/video/rooms-scheduled/:id/polls/vote` failed to carry one participant's vote. Voting is open to any participant, and the platform stamps the voter identity from the authenticated caller, so a refusal is a transport or room-state problem, not a permissions one. Have the participant re-fetch the room (the poll may have closed in the meantime), then re-submit the vote once.

A launch or vote that survives a few retries is platform-side — open a ticket with the room id and the `request_id` from the response `meta`. Poll syntax and the close flow are in the [Video API reference](/api-reference/endpoints/video); all Video codes are in the [Error Code Reference](/reference/error-codes).

### Why can't I ban or unban a video participant?

Three codes cover the ban surface, in order of where they trip:

* **Check the caller's role first.** Ban, unban, and the ban roster are host moderation actions — owner/admin only. A participant key or viewer-tier session is refused by the write guard before any ban state is touched; run the same call from an owner/admin key (or a `host`-tier token) instead of retrying with the same credentials.
* **`VIDEO_PARTICIPANT_BAN_FAILED`** (500) — the ban (`POST /api/v1/video/rooms-scheduled/:id/participants/:identity/ban`) was refused after the role check, persisting nothing. The identity may already be on the ban list — read it first with `GET /api/v1/video/rooms-scheduled/:id/bans`, then ban only an identity that isn't already barred. The sibling `VIDEO_PARTICIPANT_UNBAN_FAILED` (500) on the `DELETE` path is intentionally idempotent: unban returns success even when no ban existed, so treat a double-unban as harmless and a 500 there as transient — retry once, then escalate with the room id.
* **`VIDEO_BAN_LIST_FAILED`** (500) — the ban roster read itself failed. The read is retried automatically against short-lived connectivity blips before this code surfaces, so a 500 here means the read kept failing; retry once, then open a ticket with the `request_id` from the response `meta`.

One behaviour worth knowing before you build on bans: unlike a kick (ephemeral — the same identity can mint a fresh token and rejoin), a ban **persists** on the room and is enforced at the token-mint gate, so the barred identity stays out until you lift it. Back the moderation UI with the roster endpoint rather than local state. The endpoint contracts are in the [Video API reference](/api-reference/endpoints/video).

### Why did my RTMP egress fail to start?

`VIDEO_RTMP_EGRESS_START_FAILED` (500) on `POST /api/v1/video/rooms-scheduled/:id/egress/rtmp` means the start did not complete — but check the destination before you retry:

1. **Verify the destination URL is reachable and streaming is enabled.** Confirm the `rtmp_url` you sent answers from the streaming platform (YouTube, Twitch, or your CDN), that the stream/key pair is active and the broadcast is enabled on that platform's control surfaces, and that the URL and the stream key are not swapped — the URL carries the `rtmp://` scheme, the secret-bearing key goes in its own field.
2. **Retry the start.** The failure is atomic: either the egress was never accepted, or the platform stopped the upstream egress before returning the error, so no orphaned stream keeps rendering your room — a retry is safe and starts fresh. A refusal that survives ten minutes of retries is platform-side; escalate with the `request_id` from the response `meta`.

When the start succeeds, remember the symmetric step: stop the stream with `DELETE /api/v1/video/rooms-scheduled/:id/egress/rtmp/:egressId` when the broadcast ends — an egress left running keeps rendering an idle room to a live destination. The full refusal table (`SERVICE_UNAVAILABLE` when the streaming control plane is unconfigured on your deployment, destination refuse shapes) is on [Troubleshooting: video room lifecycle failures](/troubleshooting/video-room-lifecycle), and the request contract is in the [Video API reference](/api-reference/endpoints/video).

### Why does my in-meeting live transcript show unavailable?

`VIDEO_LIVE_TRANSCRIPT_UNAVAILABLE` (422) on `POST /api/v1/video/rooms-scheduled/:id/ask` — the in-meeting AI assistant ("ask AI during the meeting", catch-me-up) — means the assistant has no usable live transcript to ground an answer in yet. The answer is grounded only in the meeting's captured speech so far, so two causes cover nearly every 422:

* **Too early in the meeting.** No finalized caption segments exist for the room yet, or the captured speech is under the minimum length the assistant requires before it will spend an answer. Speak — or let the meeting run — and try the question again once the first exchanges have been transcribed.
* **Captions are off or not flowing.** Live captions power both the room's transcript surface and this assistant. Turn captions on for the room and verify the live transcript renders for participants (the voice gateway's realtime caption edge has to be delivering); if the transcript surface itself stays empty for a speaking room, treat that as the incident, not the assistant's 422 — escalate with the room id.

The sibling `VIDEO_LIVE_ASSIST_LLM_MALFORMED` (502) is different: the transcript loaded fine but the answer came back unparseable — retry the question once. The contract for `/ask`, and the summary counterpart (`VIDEO_TRANSCRIPT_UNAVAILABLE` on `POST /api/v1/video/sessions/:id/summary`), are in the [Video API reference](/api-reference/endpoints/video); all Video codes are in the [Error Code Reference](/reference/error-codes).

***

## 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](/verify/overview).

### Why did my Verify send get refused with 422 VERIFY\_LINE\_TYPE\_BLOCKED?

The Verify send runs one tenant-owned line-type guard **before** the wallet deducts and before any carrier is attempted, when your org's line-type policy (`landline_sms`, `landline_voice`, `voip_sms`, `voip_voice`, each `block` / `warn` / `off`) says the recipient's classified line type is not eligible for the channel you named. The guard reads the Number Lookup verdict for the destination: a `landline` (fixed-line) verdict blocks an SMS-factor send when your `landline_sms` slot is `block`, and a `voip` verdict blocks either channel when the matching slot is `block`. If the lookup's line-type field itself answered per-field `error` or `coming_soon` while the send named voice, the guard refuses rather than gamble on an unknown line — and because it refuses pre-send, no wallet deduction happens and no OTP is dispatched. The response details name the verdict: `line_type`, `channel`, and `policy_slot`.

Fix it by changing the payload or moving factors — never replay the identical body:

1. **Pre-flight the line type** with `POST /api/v1/numbers/lookup` requesting the `line-type` field; send to a destination whose verdict is voice-capable / SMS-capable for the channel you want.
2. **If the destination is fixed**, switch factors: use `sms` or `whatsapp` instead of `voice` when the line can't take a call, or raise the policy slot to `warn` (send proceeds, warning webhook fires) when a hard block is more than you need.
3. **Loosen the tenant policy only deliberately** — an owner can change a policy slot from `block` to `off` / `warn` under **Settings → Security** when a whole class of recipients should be allowed through; the default of `landline_sms: "warn"` and everything else `off` is the starting point.

Same-payload retries return the same 422 deterministically. The gate mechanics are on the [verify OTP runbook](/troubleshooting/verify-otp); the lookup field itself is on the [Number Lookup page](/numbers/lookup).

### 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](/api-reference/verify#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 reached `active` — `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](/reference/error-codes#audit-agt-007-custom-tool-dispatch-body-cap). Read engagement errors from the customer side's `activity` feed and the [conversations activity endpoint](/api-reference/endpoints/conversations#conversation-activity-feed) 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](/api-reference/cobrowse) 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 the `video.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`](/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](/api-reference/dialer) page.

### Why did my agent end-to-end latency jump after morning content while the API answer is OK?

A `200 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](/agents/continuous-production-evals) page for the full workflow.

### Why did my AI agent fail to join the conference, or refuse on a 409?

Three classes cover the AI-agent-conference join path: the room already carries an agent — `POST /api/v1/voice/conferences/:id/ai-agent` is refused with `409 CONF_AI_AGENT_ALREADY_ACTIVE` and `DELETE` must clear the `joining`/`active` row before a re-add; the call addresses an agent that isn't there — whisper or remove on a room with no `joining`/`active` row returns `404 CONF_AI_AGENT_NOT_FOUND`; or the join succeeded but the leg never bridged — the attachment row on `GET /api/v1/voice/conferences/:id/ai-agents` sticks in `joining` or flips to `error`. That last class has no PSTN leg involved, so trunk health is not the story. Read the row's `metadata.error`, remove it, re-add once, and escalate with the conference id if it recurs. The full cause table — and what not to try (re-join loops re-fire the 409) — is in [Troubleshooting: conference lifecycle failures](/troubleshooting/conference-failures#ai-agent-as-conference-participant).

### Why was my video room recording deemed degraded?

The `video.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](/api-reference/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](/verify/overview); all error codes are in the [Error Code Reference](/reference/error-codes).

### 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](/verify/overview) and the cause table on [Troubleshooting: verify OTP](/troubleshooting/verify-otp). All Verify error codes are listed in the [Error Code Reference](/reference/error-codes).

### What do `BINDING_REQUIRED` / `BINDING_MISMATCH` mean on Verify?

A binding is the device/token association a factor run must carry across enroll → send → check. `BINDING_REQUIRED` fires when a binding-bearing route is called without its binding — typically `POST /api/v1/verify/check` on a verification that was minted with a PSD2 SCA `sca_binding` (amount, payee, transaction\_id) that the check did not replay, or an SNA /device-bound send that omitted its device token. `BINDING_MISMATCH` fires when the check replays a token that does not match the one the send stored — a regenerated device identifier, a reformatted `sca_binding`, or a passkey ceremony on a different origin / `rpId` than the enrollment pinned. In both cases the gate refuses deterministically: retrying the same body returns the same code, so mint a fresh verification with a stable token, or replay the exact object the send captured. The runbook is [Troubleshooting: Verify binding gates](/troubleshooting/verify-binding-gates); the factor enrol and verify mechanics are in the [Verify factor suite](/troubleshooting/verify-factor-suite) page.

### What does a 429 on Verify mean?

A `429` 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](/guides/verify-no-sdk) and the [Rate Limits guide](/guides/rate-limits).

### 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 — set `expirySeconds` 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](/verify/overview) and in the [verify profiles guide](/guides/verify-fallback-chains).

***

## 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](/api-reference/endpoints/email).

### 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](/api-reference/endpoints/email) 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](/reference/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](/api-reference/endpoints/email).
* **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](/troubleshooting/email-dns-drift).
* **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` (live `daily_cap` vs `current_day_count`, `warming_phase`, `trust_score`) and `GET /api/v1/numbers/:id/warming/progression` (15-day ceiling forecast). Over-cap sends are refused pre-send with `429 WARMING_QUOTA_EXCEEDED`. The full flow is in the [number warm-up guide](/guides/number-warming).
* **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.

Whichever asset you are warming, the breach posture is the same: respect today's cap, re-route overflow to an already-warmed sender, and let the ramp finish. The definitions live in the [glossary](/reference/glossary) — IP Warmup and Domain Warmup for email, Warmup for sender registration.

### 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](/reference/error-codes).

### 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](/reference/error-codes).

### Why did my SMS bill for 2+ segments instead of 1?

SMS bills per segment, and two multipliers stack on a body you thought of as one message. First, single-part capacity: GSM-7 (the standard alphabet) fits 160 characters, but once a message goes multi-part every segment also carries a 7-byte user data header so handsets can reassemble it — per-segment capacity drops to 153, so a 161-character ASCII body bills as 2 segments, priced as two sends. Second, the encoding flip: one character outside the GSM-7 alphabet reclassifies the *entire* body as UCS-2 (Unicode), whose single-part limit is 70 characters and whose per-segment concatenated capacity is 67. The two effects combine: a 150-character ASCII blast is 1 GSM-7 segment; append one emoji and the whole body flips to UCS-2, the emoji alone counts as two UTF-16 units, and the 152-unit body bills as `ceil(152 / 67)` = 3 segments — one glyph tripled the line item.

The characters that flip encoding are the ones GSM-7 has no slot for: emoji, non-Latin scripts (Cyrillic, CJK, Arabic, Greek letters outside the small GSM alphabet subset), plus smart punctuation and invisible zero-width characters pasted from a document. Orbit normalizes the accidental offenders — curly quotes to straight quotes, em dashes to hyphens, zero-width spaces stripped, decomposed accents recomposed — *before* counting, so paste-from-a-doc noise does not change your bill. A deliberate emoji has no GSM-7 substitute, so it still flips the whole body; and nothing bills past 10 segments (1,530 GSM-7 characters or 670 UCS-2 code units) — longer bodies arrive trimmed with a truncation flag.

To read the count billing actually used, check the same fields per-segment pricing multiplied against: the send response on `POST /api/v1/messages/sms` carries `segments` and `encoding`, and `GET /api/v1/messages/{id}` returns them on the stored record. To read the count *before* anything is billed, preflight with `POST /api/v1/messages/estimate` — pass `channel: "sms"`, `to`, and `body`, and it returns the same `encoding` and `segments` plus a tenant-priced `estimated_cost`, while sending nothing. The full arithmetic — the capacity table, the normalization pipeline, and worked examples — is on [SMS segments and encoding](/concepts/sms-segments-and-encoding); the per-segment rate the count multiplies against is in the [pricing and throughput reference](/guides/voice-messaging-pricing-throughput) and on the [pricing page](https://orbit.devotel.io/pricing).

### 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](/reference/error-codes).

***

## AI Agents

### How does the agent marketplace review, install, and payout flow work?

A builder (any organization) publishes a template — an agent, flow, or tool — and it moves through a review gate before anyone outside the builder's organization can install it. The author flips the listing from `draft` to `pending_review`; a platform reviewer then approves it (→ `published`, which lists it in the public catalog) or rejects it back to `draft` with a written reason that lands on the author's listing row. Authors can only move between `draft` and `pending_review` — the two verdict states belong to reviewers, and self-publishing returns a 403.

Installing is a one-time copy, not a live link: the clone lands inside your own tenant and becomes your resource, so the builder's later edits never mutate your agent and there is no auto-upgrade path. Paid listings settle at install — the price is deducted from your wallet before the copy is created, and the builder's organization is credited its net share (minus the platform fee, computed with the same rev-share math the preview endpoint reports). The charge and the payout either both complete or the install fails, so a failed install never leaves a charge behind; the builder payout is skipped for self-installs and for free or built-in templates.

A moderation takedown pulls a live listing out of the catalog for a recorded reason, but anything already installed keeps working — the copy in your tenant is independent of the listing. The full lifecycle is on the [marketplace concept page](/concepts/marketplace-listing-lifecycle), the operational walkthrough in [Browse, install, and publish](/guides/marketplace-browse-install-publish), and the pre-built agent templates in the [Agent Marketplace](/agents/marketplace) reference; the endpoint contract lives in the [Marketplace API reference](/api-reference/marketplace).

### 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's `text-embedding-3-large`. Tenants don't connect their own model-provider keys. Supported models:

* `claude-opus-4-7` — top-tier reasoning for complex agents
* `claude-fable-5` — vision-enabled (image and PDF input); the intended platform default, but currently export-suspended and unavailable for completions
* `claude-sonnet-4-6` — balanced quality / cost; the model a new agent is assigned at creation time
* `claude-haiku-4-5-20251001` — low-latency classification + cheap tasks

New agents are created on `claude-sonnet-4-6` unless you select another model — that is the per-agent create-time default, which is distinct from the platform runtime default. `claude-fable-5` is the intended platform runtime default once it returns from export suspension; until then the platform runtime default falls back to `claude-opus-4-7`. You can change an agent's model at any time.

### Can agents use external tools?

Yes. Define custom tools with JSON Schema parameters. The agent will invoke tools during conversations to look up data, take actions, or fetch information from your systems.

### Why did my MCP server registration return 422 `INVALID_MCP_SERVER_URL`?

The write-time SSRF guard rejected the URL — it must be public HTTPS end to end (no `http://`, no loopback/private/CGN addresses, no link-local cloud-metadata hosts, no internal-only hostname suffixes, and no DNS record that resolves to any of those). The 422 response carries a `failure_code` naming which class tripped; the OAuth variant `INVALID_MCP_OAUTH_TOKEN_URL` applies the same guard to the grant's `token_url`, and a `409 MCP_SERVER_NAME_CONFLICT` means the server name is already registered for that agent. The full runbook is [Troubleshooting: MCP server registration rejected](/troubleshooting/mcp-server-registration); all three codes are in the [Error Code Reference](/reference/error-codes).

### Why did my A2A task fail or my discovery setting reject?

Discovery is an explicit per-agent opt-in, off by default: the dashboard A2A tab answers `422 A2A_DISCOVERY_DISABLED` until the agent's discovery mode flips to `public` (or `tenant`), and an inbound task answered with a fixed `401` means the envelope's HMAC `X-A2A-Signature` header was missing, malformed, stale, or signed with the wrong secret. A `502 A2A_PEER_ERROR` on an outbound delegation means the peer's agent errored the task — read the peer-error envelope in the task detail before retrying. The decision checklist for all of these lives in [Troubleshooting: A2A federation discovery and peer tasks](/troubleshooting/a2a-federation); the discovery modes and signing scheme are documented on the [A2A federation model](/concepts/a2a-federation-model) concept page.

### Why did my agent version promotion return `422 PROMOTION_GATE_FAILED` or `RED_TEAM_GATE_FAILED`?

Both are deliberate change-control gates on the promote call, not a general failure: the version was refused and the live config never flipped. `PROMOTION_GATE_FAILED` means the pinned eval suite (your golden sets) regressed against the candidate, a set could not run, or the aggregate pass rate fell below the gate's `min_pass_rate`. `RED_TEAM_GATE_FAILED` means the candidate's overall safety score fell under the gate's `min_safety_score` floor, or it newly compromised a probe the pinned baseline had resisted. The 422 response body carries the full gate report — `reasons`, which golden sets or red-team categories failed, and the thresholds the decision was made against — and the same explanation appears inline in the dashboard under **Agents → \[agent] → Versions → Promotion gate**. Read the report, fix the prompt or adjust the gate thresholds (`GET|PUT /agents/:id/promotion-gate/settings` and `GET|PUT /agents/:id/red-team/gate-settings`), then promote again. The full decode-and-recover runbook is [Troubleshooting: promotion gate blocks a version](/troubleshooting/agent-promotion-gate-blocks).

### What are guardrails?

Guardrails are rules that validate AI agent outputs before they reach end users. They can enforce:

* No PII exposure
* No profanity or harmful content
* Brand voice consistency
* Factual accuracy checks

### Why did my agent's completion return `422 GUARDRAIL_VIOLATION`?

That code name is a **reserved guardrail-refusal code**, listed as such in the [Error Code Reference](/reference/error-codes) — reserved means the platform may return it from any guardrail surface rather than one single endpoint. The surface that currently fires such a refusal is the agent's **output validation**: a policy violation on the generated reply (PII leakage beyond the allowed class, a missing required citation on a knowledge-grounded reply, a harmful-content finding, or a factual-inconsistency verdict when `block_ungrounded_speech` is enabled) blocks the reply before it is spoken or texted to the end user, and the assistant's fallback message is used instead. Tune your guardrails and policies — thresholds, enabled classes — rather than retrying the same prompt. The per-agent and policy mechanics are on the guardrails pages (for example [Guardrail effectiveness](/agents/guardrail-effectiveness) and [Sensitive words guardrail](/agents/sensitive-words-guardrail)).

### What is Orby?

Orby is the operator assistant embedded in the Orbit dashboard — distinct from an AI agent, which faces your customers. It answers questions about your own workspace, orients operators ("where do I set quiet hours?"), and proposes data-changing actions (send a message, reassign a conversation, pause a campaign) behind an explicit approval card, so nothing executes until an operator confirms it. Orby accepts a dashboard session only — API-key calls return `403 ORBY_SESSION_REQUIRED` — and it inherits exactly the signed-in operator's role, so it can reach nothing the operator couldn't reach through the dashboard itself. See the [Orby architecture concept](/concepts/orby-operator-assistant), the [in-dashboard workflow guide](/guides/orby-in-dashboard), and the [Orby API reference](/api-reference/orby).

### Why does an Orby action sit at pending instead of executing?

A pending action is Orby's approval gate, not a stuck call: a tool whose confirmation policy is `always` — sends, reassignment, campaign state — writes a durable proposal with the exact arguments and stops there until an operator approves it, and a `threshold` policy pauses only above a dollar cut-off. Reads and drafts fire inline under the `never` policy; the proposal expires untouched after its TTL. The [action-approvals model](/concepts/action-approvals-model) holds the transition rules and the durable event trail, and [Orby tool approval triage](/troubleshooting/orby-tool-approval) covers the `PENDING_ACTION_*` refuses.

***

## Integrations

### Why is my Salesforce / HubSpot sync failing?

One of three codes lands on the failure — `CRM_CONNECTION_MISSING` (no active connection to the provider on your organization), `CRM_AUTH_EXPIRED` (the OAuth token the stored connection holds has expired or been revoked on the CRM side), or `CRM_DISPATCH_FAILED` (a queued CRM activity-log or CDP segment-sync item couldn't dispatch — the provider endpoint rejected or timed out the outbound call). Decode which one the failed envelope carries before you touch anything: connection-class codes need one OAuth re-install from **Settings → Integrations** (Owner or Admin role required), and dispatch-class codes need the provider's underlying 4xx/5xx read first — re-queueing blind just re-stamps the same failure. The full decode table, the per-leg locate step (agent CRM tool vs inbound contact resolution vs CDP `POST /api/v1/cdp/crm-sync/run`), and the do-not-retry rules are in [Troubleshooting: CRM integration sync error codes](/troubleshooting/crm-integration-errors); the saved-connection status endpoint (`GET /api/v1/integrations/{id}/status`) answers `connected: true|false` when you want the current membership, not the last failure.

### Why does my CDP event get rejected with `422 TRACKING_PLAN_VIOLATION`?

A tenant-authored tracking plan declares what `_track`, `_page`, and `_screen` events may carry, and one row of that plan is enforced **strict** for the event name you sent — so the ingest gate compared your payload against the declared `properties_schema` and refused the write. The refusal is deterministic: replaying the same payload returns the same 422 until either the payload or the plan changes, and the error's `details.violations` list names each mismatched property (`missing_required`, `type_mismatch`, `enum_mismatch`, `extra_property`) with the expected and actual shape so you can fix the sender, not guess at it.

Three fix paths, and they do not compete — pick per send:

1. **Fix the payload to match the plan.** If the property is genuinely wrong (a string where the plan declares a number, a required field absent), correct the sender and re-send the event; the schema already encoded the correct shape, so matching it is the intended outcome.
2. **Update the plan row's schema to accept the new shape.** When the producer has deliberately started sending a new or different property, PATCH the plan row to reflect it — the plan follows the producer, not the other way around.
3. **Relax enforcement for that event.** If you want the mismatch recorded but not refused, change the plan row's `enforcement` from `strict` back to `soft` (or add a `soft` plan row) — violations still land in the violations feed with `accepted: true`, so nothing is silently dropped, but ingest stops refusing.

The unknown-event case (`unknown_event`) never 422s, even in strict mode — the platform permanently accepts catalogless events rather than dropping a send whose event name simply wasn't declared yet. The full declare-validate-triage loop, the soft-vs-strict decision, and the version-control pattern are on [CDP tracking plan](/guides/cdp-tracking-plan); the violation feed is `GET /api/v1/cdp/tracking-plan/violations`, and the code is in the [Error Code Reference](/reference/error-codes) under CDP.

***

## Billing

### What pricing model does Orbit use?

Orbit is **pay-as-you-go** with no monthly subscription tiers. You pre-load credits; outbound usage (messages, voice minutes, AI agent invocations) deducts at per-channel rates. There are no Starter, Growth, or Business subscription plans — only PAYG and Enterprise (negotiated rate card).

### What is the Enterprise plan?

Enterprise customers get negotiated per-channel rate cards, volume discounts, dedicated support, SLA guarantees, and a custom contract. Contact [sales@devotel.io](mailto: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](/quickstart#step-5-go-live). See the [Pricing page](https://orbit.devotel.io/pricing) 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](/troubleshooting/spend-caps-hit) walks the decision tree, and [Billing](billing/spend-caps) 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 with `402 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 (codes `payment_intent_requires_payment_method` and `no_default_payment_method`), and a card that needs authentication before it charges off-session (`payment_intent_requires_action`).
* **`AUTO_TOPUP_MONTHLY_CAP_REACHED`** — the `max_monthly_minor` cap 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.

To recover (as Owner or Admin):

1. 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.
2. 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.
3. 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.

Confirm wallet state before retrying a send: open **Settings → Billing** in the dashboard, or poll `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](/guides/billing-auto-topup), and both codes are listed in the [Error Code Reference](/reference/error-codes).

***

## Webhooks

### How do I verify webhook signatures?

Every webhook includes an `X-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](/webhooks/security) for verification examples and code samples in multiple languages.

### Signature verification fails on my webhook endpoint — why?

Your endpoint returns 200 on unverified deliveries but rejects the recomputed HMAC, or a local verifier can't match Orbit's signed payload. The signed string is `<t>.<raw_body>` — HMAC-SHA256 (see the [glossary term](/reference/glossary#hmac-sha256)) of the Unix timestamp and the exact raw request bytes, keyed by the endpoint's `whsec_` secret. Work the five failure classes:

1. **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).
2. **Wrong header** — prefer the canonical `X-Orbit-Signature`; a rotation grace window adds `X-Orbit-Signature-Next` (previous secret), and the legacy `X-Devotel-Signature` can carry two `v1=` candidates — accept if any candidate matches.
3. **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.
4. **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 from `GET /api/v1/webhooks/{id}`.
5. **Replaying an old delivery** — a captured payload re-POSTed after its timestamp window closes fails the same check; let Orbit retry instead.

A minimal verifier — HMAC over the timestamp plus raw body, compared with `crypto.timingSafeEqual`:

```javascript theme={null}
const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`, 'utf8').digest();
const candidate = Buffer.from(v1, 'hex');
const ok = candidate.length === expected.length && crypto.timingSafeEqual(candidate, expected);
```

Where to check next: [Troubleshooting signature failures](/webhooks/troubleshooting-signature-failures) (per-class fixes with copy-paste verifiers) and the [Webhook Security](/webhooks/security) protocol spec.

### 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](/reference/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 permanent `401`/`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](/reference/glossary#dlq-dead-letter-queue), the [Webhook Security guide](/webhooks/security), and the [failed-deliveries troubleshooting](/troubleshooting/webhook-deliveries) 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 deliver `message.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](/troubleshooting/webhook-event-dedup)), then ordering takes over. The full symptom table, remedies, and a reordered-pair fixture are on [Webhook ordering and fan-out](/troubleshooting/webhook-ordering-fanout); the delivery model itself is [Webhook delivery semantics](/concepts/webhook-delivery-semantics).

### Can I subscribe to specific event types?

Yes. When creating a webhook, specify the events you want to receive:

```json theme={null}
{ "events": ["message.delivered", "message.failed", "call.completed"] }
```

Use `["*"]` 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](/troubleshooting/webhook-event-dedup) and [failed-deliveries troubleshooting](/troubleshooting/webhook-deliveries); event names in the [Webhook Events reference](/reference/webhook-events).

### 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](/troubleshooting/webhook-endpoint-creation).

### 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](mailto: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?

| Language | Package | Status |
| - | - | - |
| Node.js / TypeScript (server) | `@devotel-orbit/node` | Published on npm |
| Browser / web (client) | `@devotel-orbit/web` | Published on npm |
| Python | `orbit_sdk` (module) | Published on PyPI: `pip install devotel-orbit-sdk` |

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](/sdks).

### Do I need an SDK to use Orbit?

No. The Orbit API is a standard REST API. The Node and Web SDKs will be convenience wrappers once published; the same endpoints are reachable from any HTTP client today.

***

## Compliance

### What does the emergency stop do on my whole organization?

The emergency stop is an org-wide, owner/admin-only kill switch that halts every outbound dispatch path — SMS, MMS, voice origination, dialer campaign activation — with a fail-closed `403 ORG_COMPLIANCE_EMERGENCY_STOP` before provider dispatch, so no balance is debited on blocked sends. Transactional Verify/OTP and email are deliberately ungated because they run separate delivery paths; lift the switch with a second call when the incident resolves. The [Emergency Stop](/compliance/emergency-stop) spec holds the flag semantics and recovery path, and [Run an emergency org halt](/guides/compliance-emergency-stop) is the operational guide.

### How do I run a fraud alert end to end?

Open the triage list (`GET /api/v1/compliance/fraud-reviews`, sorted critical-first with an `actionable` flag from your thresholds), read the single alert, then acknowledge it (`ack`) as a resolution hint or dismiss it (`dismiss`) — writes are owner/admin. Re-raise thresholds when `actionable` misfires, or export the chargeback-evidence CSV; the [list → open → acknowledge/dismiss loop](/compliance/fraud-review-triage) page walks it end to end on top of the [Fraud Shield](/compliance/fraud-shield) payload reference.

### Why can't I edit my compliance profile?

Once a compliance profile leaves the draft stage, its fields are frozen for accuracy of the carrier-side review — only `draft`, `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](/compliance/troubleshooting-pending-gated-surfaces).

### 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](/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](/compliance/posture-overview).

### Why did my SMS send refuse with `403 MESSAGING_TCPA_KNOWN_LITIGATOR`?

The recipient's phone number matched the TCPA known-litigator screen — a check that flags numbers associated with TCPA-litigation risk — and your send to a matched number is refused pre-send until you hold **verifiable consent** for it. The screen runs on SMS/MMS only and deliberately fails open: a screening outage never blocks your sends. It is also an **opt-in tenant control** — it only applies once you enable `tcpa_check_enabled` in your organization's settings (it ships off by default because it is a US-TCPA construct; traffic that never touches `+1` recipients gets nothing from the check — see [Compliance posture FAQ](/compliance/posture-faq)).

Resolve a blocked recipient one of two ways, both on your side:

1. **Record the consent proof** — if the recipient genuinely opted in, file the record through `POST /api/v1/compliance/consent` (or **Contacts → Consent** in the dashboard); the gate then allows the send and writes an audit entry for post-hoc review, so the refusal clears.
2. **Remove the recipient from the campaign** — a matched number with no consent record on file stays blocked, and no retry changes that outcome. Legitimate, recurring traffic (clinics, service reminders) usually resolves this by collecting written consent at intake; a bulk import pre-flags matched recipients once per import so the group refuses consistently rather than one-by-one.

The refusal is tenant-owned, not platform-owned: Orbit screens against the litigator source you opted into and enforces the consent records **you** file — it never decides whether a blocked recipient should receive your traffic. The error response masks the destination (`details.to` is truncated) and reports which source matched (`numeracle`, `seed`, `cache`, `manual_override`, or the pre-flagged `contact_flag`) plus a 0–100 risk `score` where one exists. The code is listed in the [Error Code Reference](/reference/error-codes).

### Why did my GDPR erasure status check return `404 ERASURE_REQUEST_NOT_FOUND`?

Every erasure request is filed through `POST /api/v1/contacts/:id/gdpr/erasure-request` (or the org-level `POST /api/v1/contacts/gdpr/erasure-request`), which returns the request's tenant-scoped id (for example `num_8fb2d1b7c00c4ec9a1d3f5e7b9c1d3`). Any read you do afterwards — `GET /api/v1/cdp/erasure/{erasureId}/propagate`, the certificate fetch, or a propagation POST — must pass exactly that id. `404 ERASURE_REQUEST_NOT_FOUND` means the id does not resolve to a row in your organization's erasure ledger, and the two causes are distinct:

1. **A malformed or mistyped id.** If you shortened or copied only part of the id, or the id is a `contact_id` rather than the request id the original POST returned, the lookup misses. Re-list the requests with `GET /api/v1/compliance/dsar/erasure-requests` and copy the `id` field verbatim — the list endpoint shows the exact id per row alongside its lifecycle `status`.
2. **A cross-tenant lookup.** A request filed under a different organization never resolves here — each organization sees only its own erasure ledger. If you hold a valid id from another workspace, the same id returns 404 (not 403) on the wrong tenant, because an unresolvable id is indistinguishable from a foreign id. Verify you are calling with the API key for the organization that filed the request.

Do not re-file in either case until you are certain the original request genuinely is absent — a blind re-POST hits the duplicate-filing gate `409 ERASURE_COOLING_OFF_ACTIVE` (see the `ERASURE_COOLING_OFF_ACTIVE` entry under "Data residency & privacy" above) whenever a prior request exists. The erasure lifecycle, propagation trail, and certificate endpoints are documented on the [compliance endpoints reference](/api-reference/endpoints/compliance) and the [DSAR runbook](/compliance/dsar).

### Why does my consent propagation endpoint return `404 CONSENT_CONTACT_NOT_FOUND`?

`POST /api/v1/cdp/consent/{contactId}/{channel}/propagate-revocation` pushes a contact's opt-out OUT to your connected destinations, and it needs the contact's identifiers (email, phone, external id) to do that mapping. `404 CONSENT_CONTACT_NOT_FOUND` means the `contactId` in the path did not resolve to a row in your organization's contacts table — so there is nothing to resolve identifiers from. The two mismatches behind it:

1. **Wrong scope.** The contact lives in a different organization than the API key you are calling with. Consent records are per-organization, so a foreign (but real) contact id returns 404 rather than 403 — verify the key before anything else.
2. **Wrong id shape.** A `contactId` here is the contacts-table id (returned by `GET /api/v1/contacts` and `GET /api/v1/contacts/{id}`), not an email, phone number, or `external_id`. If you are holding an email or phone, resolve it to a contact id first with `GET /api/v1/contacts?email=...` (or the equivalent lookup), then call the propagate route with that id.

The lookup-first pattern protects both causes: call `GET /api/v1/contacts/{id}` before any consent-propagation POST, and only propagate when that read returns the row. When the contact is found and the propagation route instead returns `409 CONSENT_NOT_REVOKED`, the gate is telling you the contact has no active opt-out on record for that channel — record the revocation first, then propagate. The endpoint contract is on the [CDP endpoints reference](/api-reference/endpoints/cdp), and the consent ledger model behind it is on [Consent and suppression](/concepts/consent-and-suppression-model).

***

## 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 on `GET /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](/reference/webhook-events)). The full mechanics are in [Post-Call Surveys (CSAT & NPS)](/api-reference/voice#post-call-surveys-csat--nps) and the [Voice-of-Customer guide](/guides/surveys-voc).

### 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 on `GET /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](/reference/webhook-events)). The end-to-end template → send → results → benchmark loop is in [Surveys end to end](/guides/surveys-voc), the [customer survey guide](/guides/post-call-surveys) covers setup, and the [Surveys API reference](/api-reference/endpoints/surveys) 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](/voice/queue-sla-forecast-callback) and the [voice queues guide](/guides/voice-queues); the alert/escalation rules on top of the same metrics are on [Queue SLA escalation policies](/voice/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](/voice/queue-sla-forecast-callback); the sibling advisory forecast for callers already waiting is on [Forecasted pre-suggestion + callback](/voice/sla-breach-forecast-callbacks).

### 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 as `voicemail` / `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](/guides/voice-voicemail-boxes) and [Inbound number routing](/guides/inbound-number-routing).

### Which built-in CDP predictive models does Orbit ship?

Four server-owned model keys, listed by `GET /api/v1/cdp/predictive-models` with each model's kind and output unit:

| Model key | Kind | Output unit |
| - | - | - |
| `churn_propensity` | classification | probability |
| `conversion_intent` | classification | probability |
| `lifetime_value` | regression | value in cents |
| `engagement_fatigue` | classification | probability |

Scores become segments through the activation flow — the full walkthrough (train → score → activate) is in the [CDP predictive models guide](/guides/cdp-predictive-models). The same guide covers the per-model feature set (six first-party behavioral signals like `recency_days` and `events_30d`) on its catalog section. For score→segment specifics per model, see the [CDP API reference](/api-reference/endpoints/cdp).

***

## WFM, Quality & Team Chat

### What is WFM, and do I need a separate workforce-management product?

No — workforce management is a shipped domain inside Orbit, not a third-party integration. The whole surface is mounted under `/api/v1/wfm` and answers the four contact-center questions: how much work is coming in (forecasts), when each agent should work (schedules and assignments), did the day go as planned (adherence), and how to rebalance when reality drifts (intraday staffing). The planning side runs from `GET /api/v1/wfm/forecasts` — per-channel, per-interval required-agent rows, refreshed by `POST /api/v1/wfm/forecasts/recompute` and measured for trust with `GET /api/v1/wfm/forecasts/accuracy` — through `POST /api/v1/wfm/schedules/generate` (draft) → `POST /api/v1/wfm/schedules/generate/commit` (publish). The during-the-day side reads required vs. scheduled headcount on `GET /api/v1/wfm/intraday/staffing` and fixes the drift with `POST /api/v1/wfm/rebalancing/recommend` → `POST /api/v1/wfm/rebalancing/apply`. The model-map for all six domains is [The WFM model](/concepts/wfm-model); dash-following on the workflow side is in [WFM intraday adherence and requests](/guides/wfm-intraday-adherence-and-requests).

### How do I run QA autoscore against recent queue calls?

Call quality autoscore is opt-in per tenant, and the readiness gates are readable before you enable it. The [auto-QA readiness guide](/guides/qa-autoscore-readiness-panel) pre-flights the whole pipeline: the **Generation switch** gate checks **Settings → AI Auto-QA** is on, the **Active scorecard form** gate checks at least one evaluation form is active, and the **Coverage** bar shows the share of eligible calls (agent-attributed, transcribed) actually scored over the trailing three days. The same snapshot is queryable over the API on `GET /api/v1/quality/auto-qa-readiness`, and the coverage/flag-share rollup feeds `GET /api/v1/quality/autoscore-coverage`. Setup itself is one toggle plus one threshold, documented in [AI Auto-QA configuration](/guides/qa-autoscore-settings): enable `qa_autoscore` under **Settings → AI Auto-QA**, and the once-a-minute sweep scores every completed agent-handled call's transcript against your most recently updated active evaluation form, writing a `qa_evaluations` row with `auto_scored: true` and `flagged_for_review` set when the score falls below your flag threshold (default 70). Reviewers pick up flagged rows through the same `GET /api/v1/quality/evaluations` ledger as manual scores; the scorecard authoring endpoint is `POST /api/v1/quality/evaluation-forms` ([Quality evaluations](/guides/quality-evaluations)).

### When do I use Team Chat versus an inbox thread?

Use Team Chat for intra-team coordination that should never touch a customer; use Inbox for customer-facing threads. Team Chat (mounted at `/api/v1/team-chat`) is internal messaging for the operators running your workspace — channels, DMs, reactions, presence, and huddles (quick talk-it-through rooms started from a channel or DM). Nothing posted there reaches a customer, and no outbound customer message flows through it — that separation is the point. Inbox, by contrast, is the customer-facing ticket/thread surface where a reply goes out to the requester. The workflow rubric: on-call handoffs, deploy heads-ups, and quick huddles while triaging an incident belong in a team-chat channel (e.g. `on-call-platform`); anything the customer should read belongs in Inbox. The operator walkthrough is [Team Chat](/guides/team-chat), the concept model is [Team chat model](/concepts/team-chat-model), and the endpoint contract is [Team Chat API reference](/api-reference/team-chat).

### Which WFM levers require QA setup first?

Only the coaching/evaluation-flavored ones — not the WFM core. WFM planning and adherence (forecasts, schedules, assignments, staffing, rebalancing) stand on their own. Where QA setup becomes a precondition is wherever the WFM surface funnels into evaluation- or coaching-shaped output: the QA autoscore pipeline needs an **active evaluation form** (authored in **Quality → Evaluation forms** or `POST /api/v1/quality/evaluation-forms`) before the sweep can score, and the auto-QA readiness card names that gate explicitly ([QA autoscore readiness](/guides/qa-autoscore-readiness-panel)). Persona/evaluation leads and coaching cards read from those auto-scored rows, so enable AI Auto-QA and activate a form before expecting coaching-driven WFM follow-ups to fire. Plain WFM drift repair — rebalancing recommendations, intraday adherence, open-shift bidding — does not consult QA at all.

### Can I silence a forecast spike with one rebalance?

One rebalance call answers today's drift; it does not rewrite the forecast itself. The intraday loop works like this: the 30-minute reforecast keeps `wfm_forecasts.required_agents` live, `POST /api/v1/wfm/rebalancing/recommend` compares that live requirement per (channel, interval) against the scheduled headcount you pass (or the latest forecast row when you omit it), and returns three levers in cost order — `MOVE` (re-route a cross-trained agent from an over-staffed channel to an under-staffed one, zero headcount), `ADD` (pull-in/overtime/open-shift for a residual deficit), `RELEASE` (VTO/early release for a residual surplus). `POST /api/v1/wfm/rebalancing/apply` then extends or trims assignments to enact them. If the spike is a genuine traffic shift rather than a one-off blip, the durable fix is `POST /api/v1/wfm/forecasts/recompute` (rebuild the forecast from current history) followed by a schedule regenerate — rebalance buys you the intraday relief while you do that. The supervisor-side workflow lives in [WFM intraday adherence and requests](/guides/wfm-intraday-adherence-and-requests); forecast accuracy (`GET /api/v1/wfm/forecasts/accuracy`) tells you how much to trust the next recompute.

***

## Troubleshooting and runbooks

### Why can't I find my error on the troubleshooting hub?

The hub indexes runbooks by the surface they work on — messaging, voice, video, webhooks, Verify, billing, channels — and a newly shipped runbook is routed into the matching accordion even when the page itself went live first. If the hub still doesn't list the runbook you need, three fallback paths reach it:

1. **The [Error Code Reference](/reference/error-codes).** Read the `code` on the error envelope your API call returned and match it there first — the reference is complete, and most codes link their dedicated runbook directly.
2. **The sidebar.** Every runbook is wired into the sidebar navigation the moment it ships, so expand the **Troubleshooting** group and scan the page titles — the page is reachable there whether or not the hub already routed it.
3. **The "From an error code to a runbook" table on the hub.** At the bottom of the [hub page](/reference/troubleshooting-hub), an accordion maps error-code classes to runbooks and falls back to the Error Code Reference for anything without a dedicated page — use it when you know the envelope's `code` but not the runbook's name.

### When should I use the troubleshooting hub versus the FAQ?

Three waypoints sit between a symptom and a fix — the hub page, the FAQ, and the individual runbooks — and each does a different job:

* **The [Troubleshooting hub](/reference/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.

The triage decision tree is status-first: for a message that did not land, identify where it stalled before you open any channel-specific page.

1. **`queued`** — the message has not been handed to a provider yet; the hold is pre-delivery. See [Troubleshooting: message stuck in queued](/reference/troubleshooting).
2. **`failed` or `undelivered`** — a terminal non-delivered outcome. See [Troubleshooting: message undelivered or failed](/troubleshooting/message-undelivered-failed).
3. **`sent` but no receipt** — the provider accepted the message, but a `delivered`/`undelivered` outcome never lands. See [Troubleshooting: message sent but no delivery receipt](/troubleshooting/submitted-no-receipt) and, when the receipt itself needs recovering, [Recover a failed or late DLR](/troubleshooting/failed-dlr-recovery).
4. **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.

By contrast the FAQ is organized by topic (General, Messaging, Voice, Verify, Billing, Compliance, and so on) because questions cluster by what you are asking about, not by where a failure surfaced. Jump straight to a runbook whenever the failure is one envelope `code` — match the code against the [Error Code Reference](/reference/error-codes) and skip the whole entry-point question.

***

## Support

### How do I contact support?

* **Email:** [support@devotel.io](mailto:support@devotel.io)
* **Dashboard:** In-app chat via the help widget
* **Enterprise:** Dedicated Slack channel and account manager

### Support history FAQ

Six answers about the dashboard's **Support → Timeline** page — the reader over your organization's own support history. The full walkthrough is the [Support history guide](/guides/support-timeline).

**Who can see the Support history?**
Any authenticated member of your organization whose role includes the surface. The page re-reads the same org-scoped inbox ticket endpoints the operator queue uses, so every request your organization has opened appears to every member who can reach it.

**What does a restricted role see?**
An access-restricted notice instead of the list. Dashboard section restriction is a tenant-owned control — an owner or admin adjusts the member's role to include the surface.

**How do I reply on a past request?**
The **Continue this conversation** button on the expanded thread deep-links to the ticket-comments composer for that request, where your reply is composed and sent under your own name. A member whose role is read-only can read the thread but cannot answer from either surface.

**Can I download attachments?**
Yes — files shared during the conversation appear under **Attachments** on the expanded thread as read-only downloads. Internal agent notes and their files are excluded; the page shows the public thread only.

**How do I export the history?**
`GET /api/v1/inbox/tickets/export` downloads the organization's request list as CSV or JSON (with `include_comments=true` to inline the public threads), and the list endpoint's filters (`status`, `priority`, `opened_after`, …) narrow the slice. Overflow past the server cap is flagged with `truncated: true` so you can narrow and pull the remainder.

**How far back does the history reach?**
Nothing is removed — a resolved request stays readable. The page loads the 100 most recent requests first; older history is preserved, reachable through support or the export endpoint.

### Where can I check service status?

Visit [status.orbit.devotel.io](https://status.orbit.devotel.io) for real-time platform status, incident history, and maintenance schedules.

### How do I report a security vulnerability?

Email [security@devotel.io](mailto:security@devotel.io). We respond within 24 hours and follow responsible disclosure practices.

***

## Insights & Analytics

### Why does my SMS click-through show as 0 after a sent campaign?

The click-through counter on [SMS click-through](/insights/sms-ctr) only moves for sends that carried a tracked short link — a plain-text URL in the body cannot emit a click event, so an untracked campaign correctly shows `Tracked: 0` and a `—` or `0` CTR. Tracked-link minting happens three ways, in order: an explicit `metadata.shorten_urls` on the send payload, the tenant's SMS auto-shorten channel setting (default ON, rewrites URLs over 30 characters when the minted link is strictly shorter), and the WhatsApp auto-track counterpart. If minting failed silently (fire-and-forget — the original URL ships untouched), the send lowers Tracked rather than failing the message. The [one-line diagnosis](/insights/sms-ctr#reading-the-unattributed-bucket) of where the counter lives: if `unattributed` is your biggest bucket, stamp `campaign_id` at send time or fix queue resolution on your messaging service — the fix is upstream in the send payload, not on the panel. A compact summary embedded at **Messages → SMS messages** shows a fixed 7-day window; open **Insights → SMS click-throughs** for the 24h/7d/30d control. The endpoint behind it all is `GET /api/v1/analytics/sms-click-through`.

### When does an abandoned call count toward my queue SLA forecast?

A hang-up only enters the forecast's denominator once the caller has waited past the queue's `target_service_level_seconds` (the short-abandon window filter drops sub-threshold misdials from the service-level math). The forecasted wait is `waiting callers × recent average handle time ÷ available agents`, live inputs — so the wallboard verdict and the entry gate cannot drift. When the forecast breaches the target, the queue's `slaCallbackPolicy` decides: `suggest` flags the supervisor surface, `block` diverts the caller to the press-1 virtual-callback consent flow and the queue-entry call answers `503` with `slaCallbackBlocked: true`. The per-interval counter lives on `GET /api/v1/voice/queues/{id}/stats` (`abandoned_24h`); the full abandonment breakdown on `GET /api/v1/voice/queues/{id}/analytics`. Mechanism: [Inbound queue SLA forecast + virtual callback gate](/voice/queue-sla-forecast-callback).

### How do I read my squad's routing numbers?

The glossary's [Squad (Agent Squad)](/reference/glossary) entry defines the construct: a classifier front door, 2–6 specialist members, optional fallback and daily cost cap. Its analytics question — "is the squad routing traffic correctly, and which specialist is stuck?" — is answered by `GET /api/v1/agents/squads/{id}/analytics`, which reports the four observability rollups: handoff success rate (resolved-terminal shares per target), drop-off (handoffs that bailed to a human or errored), specialist utilization (which member carries the conversation load), and **containment lift** (the squad's end-to-end containment minus the classifier-alone baseline). The percentages are a window rollup, not a live-dedup: the route is tenant-scoped and degrades to empty/zero rather than 500 when a provisioning gap hits. The hub that carries every member-accessible analytics surface (and cross-links the wallboard) is the [Insights overview](/insights/overview) — build/test routing specifics in [Agent squads](/agents/squads).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.