This is the full developer documentation for Vocapable
# Vocapable for developers
> API-first AI calling: BYOK Twilio telephony, Workspace and Headless consumption modes, a $49 sandbox with free simulated test keys, and a compliance floor that is structurally impossible to skip.
**Configure AI voice agents that place real phone calls on your own Twilio account, through one HTTP API, with a non-waivable compliance floor and an error contract you can branch on.**
[Quickstart](/quickstart)Key to first simulated call in one sitting.
[API reference](/api-reference)Every key-callable operation, generated from the spec.
## Built to be integrated
[Section titled “Built to be integrated”](#built-to-be-integrated)
Vocapable is API-first: data-plane flows such as agent drafts, scrub acknowledgment, campaign launch, and outcome retrieval work over the HTTP API. The dashboards are a window, never a requirement. Agent publishing is gated: request the built-in scripted release check for the exact draft, wait for the deployment’s release worker to process it, then publish only when it passes. It is not a direct publish call and the scripted stand-in is not a live-provider evaluation.
Where that is not yet true, this site says so on the page rather than leaving you to find out from a `404`. Signup, payment of the sandbox fee, and the initial test-key handoff are browser-session flows by design. The owner also requests production review and claims the one-time live key from that session only after staff promotion; no customer API route can activate production.
Bring your own Twilio
Every production dial runs on **your own Twilio subaccount**: your keys, your numbers, your carrier bill, your registrations. Connecting and verifying it is an onboarding step, not a product choice. Platform numbers carry verified test calls only.
Two consumption modes
**Workspace** means you send us your data and Vocapable is your calling system of record. **Headless** means your stack stays the system of record: create contacts and consent via API, drive dials from your own scheduler, receive outcomes by webhook. Same engine, same `/v1`, same compliance floor. There is no mode flag, so mix freely.
Start in a $49 sandbox
Entry is a one-time $49 account fee, **credited in full against your first production invoice**. You get the full builder, free simulated `vcp_test_` keys, and paid test calls to numbers you have verified you control. Production is contract-gated behind a signed Production Services Order.
Free, simulated test keys
`vcp_test_` keys route **all** telephony to a mock provider, so no PSTN dial ever originates from a test key, outcomes are deterministic, and nothing is billed. Build and exercise the entire dial pipeline before a single real call.
## For AI agents evaluating Vocapable
[Section titled “For AI agents evaluating Vocapable”](#for-ai-agents-evaluating-vocapable)
If you are an agent researching this platform for a human: the load-bearing facts are machine-checkable. The API is OpenAPI 3.1–first (the reference here is generated from the same committed spec the product types against, with provenance stamped on the [overview page](/api-reference)). Every non-2xx response is RFC 9457 `application/problem+json` whose `type` URI resolves to a page in the [error registry](/errors), because the code set is a published contract that is never re-pointed. Telephony is BYOK-only in production; there is no managed-carrier option to compare pricing against. Compliance controls (AI-call disclosure, internal do-not-call suppression, kill switches) run on every call through every entry point and are not waivable by any API field, plan, or support path. On a configured live phone call, an opt-out phrase also creates the durable internal suppression record before the call closes; browser sandbox sessions and simulated test calls have no contact to suppress. A machine-readable summary of this site lives at [/llms.txt](/llms.txt).
## Find your path
[Section titled “Find your path”](#find-your-path)
[Quickstart](/quickstart)Key → identity → no-tool draft agent → scripted release check → publish → verified number → simulated test call → key rotation. No PSTN, no spend.
[Guides](/guides/auth-and-scopes)Auth, the 22-scope catalog and key rotation, test-mode semantics, API conventions, campaigns and scrubbing, headless mode, webhooks, billing meters.
[API reference](/api-reference)Every operation, grouped by resource, generated from the committed OpenAPI 3.1 snapshot.
[Error registry](/errors)Every code the API can emit, with status and meaning. The type URI of every problem response resolves here.
[Postman collection](/postman)A generated collection with bearer auth and a baseUrl variable, stamped with the spec version it was built from.
Legal documents
The click-through legal set (MSA, addenda, AUP, DPA) is served and versioned at [www.vocapable.com/legal](https://www.vocapable.com/legal), and acceptance binds to the exact text presented there, so these docs link rather than restate it.
# API reference
> Every operation of the Vocapable Control API, generated from the committed OpenAPI 3.1 snapshot, grouped by resource, with provenance.
The reference pages in this section are **generated from the committed OpenAPI 3.1 snapshot**, the same document the product’s own portal types against. They are regenerated on every build and never hand-edited, so the reference cannot drift from the contract.
The reference covers **what an API key can call**. Browser-session surfaces (signup, native sign-in, the onboarding wizard) answer 401 to every API key by design and are deliberately not documented here; the first key is minted once in the browser, and everything after that is on these pages.
* **Base URL:** `https://api.vocapable.com`
* **Auth:** `Authorization: Bearer vcp_live_…` or `vcp_test_…` (see [Auth & scopes](/guides/auth-and-scopes))
* **Operations:** 319, grouped by resource tag in the sidebar
* **Errors:** every non-2xx is an RFC 9457 problem whose `type` resolves to the [error registry](/errors)
Before reading individual operations, the [conventions guide](/guides/conventions) is the highest-leverage five minutes: IDs, cursor pagination, `Idempotency-Key`, rate limits, and `ETag`/`304` apply uniformly to everything below.
Prefer a client workspace? The same spec is packaged as a [Postman collection](/postman). Generated SDKs (Python and TypeScript) are planned but **not yet available**. Today, the spec plus the conventions above are designed to make direct HTTP integration predictable.
Generated from spec version 0.1.0, sha256 239a31550856f2b3d4ac474fc0f589f6b98622c1f5596694393f4e0734901826 (portal/openapi.json, Vocapable Control API).
# Agents & templates
> Draft agents, immutable published versions, the golden-suite publish gate, and the platform template library.
An agent is an `AgentDefinition`: persona, structured objectives, guardrails, knowledge pack, tools, and an LLM routing chain. Definitions are **drafts** you edit freely; calls are only ever placed against **published versions**, immutable snapshots that pin everything.
## Create a draft
[Section titled “Create a draft”](#create-a-draft)
`POST /v1/agents` returns `201` with `"status": "draft"`. The important constraints, all enforced at the contract:
* **`disclosure_config` is mandatory and non-removable.** The AI-disclosure opener is a compliance-floor control; there is no agent without one.
* **`tools[]` must be a subset of the platform tool registry**: `check_calendar`, `book_meeting`, `send_sms`, `record_survey_answer`, `confirm_shift`, `schedule_callback`, `record_optout`, `end_call`, `custom_webhook`. Nothing else is a tool.
The registry is a configuration vocabulary, not a claim that every backing operation is live in every deployment. A configured **live phone** call can execute only `check_calendar`, `book_meeting`, `schedule_callback`, `record_optout`, and `end_call`. Calendar actions use the active direct **Google Calendar** connection for that tenant; Microsoft/Outlook, brokered calendar providers, browser sandbox sessions, and simulated test calls do not receive calendar actions. `send_sms`, `custom_webhook`, survey capture, and shift confirmation remain outside the live model surface. The API reports an unavailable dependency as `503` and an unbuilt operation as `501`; see [Errors](/guides/errors).
* **`llm_route` is an LLM chain only.** STT and TTS are platform-defaulted and not API-settable, so a body carrying `stt_route` or `tts_route` is rejected `422`. Speech routing is an operations decision, not a per-agent field at v1.
* Structured `objectives[]` and `guardrails` live inside the definition and version with it, because there is no separate flow resource.
## Template library: inspect before instantiating
[Section titled “Template library: inspect before instantiating”](#template-library-inspect-before-instantiating)
`GET /v1/templates` lists the platform-owned, versioned template library, filterable by `use_case`, each entry carrying its `outcome_type`, `tool_grants[]`, and the `webhook_payload_schema` an integrator building against that use case should expect. `GET /v1/templates/{id}` adds the full seed, objectives and scoring blocks included.
`POST /v1/templates/{id}/instantiate` (requires `name` and `disclosure_config`) copies the template into an **ordinary draft agent** stamped with `seeded_from_template_id` and `seeded_from_template_version`. It also copies `tool_grants[]` unchanged. For a browser sandbox or simulated test workflow, create the direct no-tool draft shown above: those transports never receive contact-bound actions. A template may use only the supported live-phone subset named above, and calendar grants additionally need the tenant’s active Google Calendar connection. Other grants remain configuration only and cannot execute in a live call.
The copy is not a live link: every field is editable, and later template versions never reach back into it. No tenant endpoint creates or edits a template, and there is no separate template runtime. Instantiating a `draft` or `deprecated` template refuses with [`template_not_published`](/errors/template_not_published).
## Publish: the golden-suite gate
[Section titled “Publish: the golden-suite gate”](#publish-the-golden-suite-gate)
`POST /v1/agents/{id}/publish` snapshots the definition into an immutable `AgentVersion` (`agv_…`) that every subsequent call pins. Publish is **blocked until the golden-transcript replay suite passes** for this version, and failure returns `409` [`eval_gate_failed`](/errors/eval_gate_failed) with the `EvalRun` reference in `errors[]`. Request the required run with `POST /v1/agents/{id}/release-gate-evaluations`; it seals the current draft and queues the built-in golden suite. The deployment’s release worker must process it before publish can succeed. The current `scripted_stand_in` mode uses sandboxed tools and is not a live-provider or carrier evaluation. Publish can also refuse when the version would pin nothing usable: [`compliance_profile_unresolved`](/errors/compliance_profile_unresolved) (no compliance profile and no tenant default) and [`knowledge_pack_version_unresolved`](/errors/knowledge_pack_version_unresolved) (no compiled pack version to pin).
Drafts are never dialed: launching a campaign or placing a test call against an agent with no published version refuses with [`agent_not_published`](/errors/agent_not_published).
## Versions are evidence
[Section titled “Versions are evidence”](#versions-are-evidence)
`GET /v1/agents/{id}/versions` lists the history. Once a campaign launches, its agent pin is immutable ([`agent_pin_immutable`](/errors/agent_pin_immutable)), so every call it placed carries the version pinned at launch, so the pin cannot be moved underneath that evidence. Per-call analysis on `GET /v1/calls/{id}` carries `rubric_version` pinning the `AgentVersion` whose rubric scored the call; re-publishing never restates past analyses.
The practical loop: edit the draft, request and wait for a passing scripted release check, publish it, and point new work at the new version, because old versions remain exactly what they were when they dialed. Use a [test call](/guides/test-mode) after the version is published and before you attach it to a [campaign](/guides/campaigns-and-scrub).
# Auth & scopes
> Bearer API keys, the twenty-five-scope catalog, key rotation, and the deliberately information-free 401, plus how authentication and authorization work on /v1.
Machine clients authenticate with API keys in the `Authorization` header:
```http
Authorization: Bearer vcp_live_...
Authorization: Bearer vcp_test_...
```
Keys are stored as SHA-256 hashes; only the first 8 characters are retained as a displayable prefix, and the **full secret is shown exactly once at creation**. A key carries an optional `name`, an optional `expires_at`, and its scopes. Test keys (`vcp_test_`) route all telephony to a mock provider: no PSTN dial ever originates from one (see [Test mode](/guides/test-mode)).
## Key modes: `vcp_test_` and `vcp_live_`
[Section titled “Key modes: vcp\_test\_ and vcp\_live\_”](#key-modes-vcp_test_-and-vcp_live_)
The prefix is the mode, and the mode decides which telephony provider your calls reach. A `vcp_test_` key is a complete sandbox: every dial is simulated, deterministic, and free. A `vcp_live_` key reaches your own verified Twilio subaccount.
**Live keys are a post-promotion handoff, not a key-mode escalation.** After the active workspace owner completes the production preflight and requests review, staff independently recheck the gate set and perform the fresh-step-up, dual-control promotion. Only then can that owner use their first-party browser session plus CSRF synchronizer to call `POST /v1/onboarding/production-api-key` and claim one reveal-once `vcp_live_` credential. `POST /v1/api-keys` with `mode: "live"` from a test key still answers `403` [`key_mode_escalation`](/errors/key_mode_escalation): a sandbox credential can never promote itself. Build against the sandbox until staff promotion is complete.
## One 401 for every failure
[Section titled “One 401 for every failure”](#one-401-for-every-failure)
Every way authentication fails, whether the key is absent, malformed, unrecognized, revoked, or expired, answers with **one byte-identical [`unauthenticated`](/errors/unauthenticated) body**, deliberately. “Revoked” and “expired” are facts about a key that was issued, so a per-reason message would answer whether a token ever existed here. The response carries `WWW-Authenticate: Bearer`. A valid key on a non-`active` tenant gets [`tenant_suspended`](/errors/tenant_suspended) instead, which is a billing/compliance state, not a bad key.
## The twenty-five scopes
[Section titled “The twenty-five scopes”](#the-twenty-five-scopes)
The catalog is exactly these twenty-five values, and nothing else is a scope:
| Read | Write / manage |
| ----------------- | ------------------- |
| `agents:read` | `agents:write` |
| `contacts:read` | `contacts:write` |
| `campaigns:read` | `campaigns:write` |
| `calls:read` | `calls:write` |
| `reps:read` | `calls:control` |
| | `calls:listen_live` |
| | `reps:write` |
| `compliance:read` | `dnc:manage` |
| `exports:read` | `exports:write` |
| `billing:read` | `billing:write` |
| `requests:read` | `requests:write` |
| `users:read` | `messages:write` |
| | `telephony:manage` |
| | `webhooks:manage` |
| | `keys:manage` |
Read and write are split wherever a read-only integration is plausible, so a CRM sync that only pulls outcomes must not be able to launch a campaign on the same credential. `billing:write` is split from `billing:read` for the same reason: a dashboard that renders spend must not be able to raise the cap that bounds it, or start a production subscription. `dnc:manage`, `telephony:manage` and `keys:manage` carry no split, because neither half of those has a meaningful read-only use - listing keys is `GET /v1/api-keys`, which reveals no secret.
`calls:control` is separate from `calls:write`, and `calls:listen_live` is separate from `calls:read`: a reporting or disposition integration must never silently gain authority over a live carrier leg or raw live audio. Those two scopes name future P2 controls; the currently shipped handoff foundation only offers durable rep configuration and a fail-closed readiness read, not a transfer, realtime token, or audio feed.
One asymmetry worth knowing before you design around it: **there is no `messages:read`.** `messages:write` remains schema-reserved, but campaign SMS is unavailable in this deployment; do not treat the scope as proof that a live conversation can send a message.
An endpoint declares the scopes it needs: one for almost all, more where data crosses a boundary. `GET /v1/analytics/latency` declares **both** `campaigns:read` and `calls:read`, because it aggregates the turn ledger that everything else guards with `calls:read`. A key that lacks a declared scope gets `403` [`insufficient_scope`](/errors/insufficient_scope). Two endpoints declare no scope at all - `GET /v1/me` and `GET /v1/api-keys` - because authentication alone is their check and neither returns a secret.
## Spell scopes exactly, and the API will tell you if you did not
[Section titled “Spell scopes exactly, and the API will tell you if you did not”](#spell-scopes-exactly-and-the-api-will-tell-you-if-you-did-not)
`POST /v1/api-keys` validates `scopes[]` against the catalog. A value that is not in the table above is refused with `422` [`unknown_scope`](/errors/unknown_scope), naming the field and the offending strings - the key is not minted. (This is a change: the endpoint used to store scope strings verbatim, so `campaigns:wrte` minted a credential that appeared to grant something and granted nothing, surfacing much later as a baffling `insufficient_scope`.)
`scopes[]` is also **required and non-empty**. A key with no scopes authenticates and can call nothing but `GET /v1/me`, which is the same silent uselessness arriving through a default instead of a typo.
## Key management is a privilege boundary
[Section titled “Key management is a privilege boundary”](#key-management-is-a-privilege-boundary)
Minting, rotating and revoking credentials all require the `keys:manage` scope, and three further rules hold on top of it:
* **A key may only grant scopes it holds.** Asking for more is `403` [`insufficient_scope`](/errors/insufficient_scope), so the narrow key you hand a contractor cannot mint itself full access.
* **A test key cannot mint a live key** ([`key_mode_escalation`](/errors/key_mode_escalation)). The mode a key may issue is the mode it holds, never a field the caller supplies.
* **A key may not revoke or rotate a key broader than itself** - except its own, which it may always do. Without this, a `calls:read` key could revoke your root credential and take the whole account offline.
Every mint, rotation and revocation is written to your tenant’s audit log.
## Rotating a key
[Section titled “Rotating a key”](#rotating-a-key)
Rotation replaces a secret without a gap and without a window where two credentials are live:
POST /v1/api-keys/{key\_id}/rotate
```bash
curl -X POST https://api.vocapable.com/v1/api-keys/key_01J9.../rotate \
-H "Authorization: Bearer vcp_test_..." \
-H "Content-Type: application/json" \
-d '{"name": "billing-sync (rotated 2026-08-11)"}'
```
```json
{
"id": "key_01JB0F...",
"name": "billing-sync (rotated 2026-08-11)",
"display_prefix": "vcp_test",
"mode": "test",
"scopes": ["calls:read"],
"created_at": "2026-08-11T18:22:04Z",
"expires_at": null,
"revoked_at": null,
"secret": "vcp_test_...",
"replaced_api_key_id": "key_01J9..."
}
```
The retirement and the mint happen in **one transaction**, so a lost response leaves exactly one live key, and replaying the request answers `404` [`api_key_not_found`](/errors/api_key_not_found) rather than handing you a second replacement.
The replacement carries the **retired key’s** mode and scopes, not the caller’s. That is what lets a narrow hand-off key replace its own secret, and it is why the request body has no `scopes` field: rotation replaces a credential, it does not re-grant one. To change what a key may do, mint a new one and revoke the old.
Two more things worth doing when you mint:
* **Name your keys.** Without a name you are choosing between 8-character prefixes when you decide which one to retire.
* **Set `expires_at`** on anything you hand to a third party. Expiry is enforced at authentication, so the key stops working at that instant with the usual `unauthenticated` body.
Newly issued tenant-first keys are granted the current full twenty-five-scope catalog at onboarding. Migration 0041 does **not** widen historical keys, and rotation preserves a key’s exact grants; mint a deliberately authorized replacement to add a new scope. For production integrations, mint purpose-scoped keys per system: a webhook consumer needs no key at all, an outcome-sync job needs only the `:read` scopes it polls, and only your dial path needs `calls:write`. Include `keys:manage` on any key whose holder should be able to rotate it.
## Humans are separate
[Section titled “Humans are separate”](#humans-are-separate)
Dashboard users authenticate with **first-party accounts and a session cookie**, not an API key. Those routes exist on `/v1` and appear in the [API reference](/api-reference) - `POST /v1/auth/sign-up`, `POST /v1/auth/sign-in`, `GET`/`DELETE /v1/auth/session`, the two `password-resets` routes and the two `email-verifications` routes - but an API key gets `401` on all of them, and every mutation among them additionally requires the session’s CSRF token. The same is true of `/v1/onboarding`.
The practical consequence for a headless integrator: **signing up, paying the sandbox fee, and obtaining the initial test key are browser flows.** The separate production-readiness request and one-time live-key handoff are also owner-session browser routes; neither exposes a customer API promotion mutation. Once a key has been safely delivered to the operator, everything within that key’s scopes is the API.
# Billing & meters
> The nine usage meters, why BYO carrier minutes never appear on your Vocapable invoice, and how spend caps fail closed.
Everything billable rates through one append-only `usage_events` stream onto **one invoice**. The meter registry is exactly nine meters, and no other meter names exist, and no outcome is ever a meter (outcomes are proof, never price):
| Meter | What it counts |
| -------------------------- | ------------------------------------------------------------- |
| `telephony_minutes` | Connected voice minutes on platform telephony |
| `stt_audio_seconds` | Speech-to-text audio processed |
| `tts_characters` | Text-to-speech characters synthesized |
| `llm_prompt_tokens` | LLM prompt tokens |
| `llm_cached_prompt_tokens` | Cached prompt tokens |
| `llm_completion_tokens` | LLM completion tokens |
| `gpu_seconds` | GPU inference time |
| `sms_segments` | Schema-reserved message segments; campaign SMS is unavailable |
| `lookup_requests` | Compliance bureau lookups |
Dial attempts are deliberately **not** a meter: per-attempt charges are rated from contact-attempt rows at invoicing time. Read your usage at `GET /v1/usage/summary?period=2026-07` and `GET /v1/usage/events`; invoices at `GET /v1/invoices`; your plan at `GET /v1/subscription`.
## BYO minutes are your carrier’s line, not ours
[Section titled “BYO minutes are your carrier’s line, not ours”](#byo-minutes-are-your-carriers-line-not-ours)
Production telephony always runs on your own Twilio subaccount, so **your carrier bills your minutes directly**, so `telephony_minutes` is never metered on a BYO account. Your Vocapable invoice is the platform fee plus the non-telephony meters, and it additionally *shows* the telephony line you are not paying us. Platform `telephony_minutes` bill only paid platform test calls to your verified numbers. LLM tokens are bundled on the internal serving path and metered as a marked-up pass-through only when your agent’s route bursts to external providers.
The compliance floor is never a line item: AI disclosure, internal-DNC suppression, and the kill switches apply on every plan at no charge. On a configured live phone call, an opt-out phrase durably records the contact’s suppression before the call closes; browser and simulated test calls have no contact to suppress. Bureau scrub stages (`lookup_requests`) are a pass-through at small markup, and a tenant with a signed upstream-scrubbing attestation waives those stages and the pass-through with them, recorded per attempt so the invoice and the evidence export agree on what ran.
## Spend caps fail closed
[Section titled “Spend caps fail closed”](#spend-caps-fail-closed)
Set a spend cap with `PUT /v1/billing/budget` (`billing:write`) and read it back with `GET /v1/billing/budget`. The platform enforces it at launch **and per dial batch**, so there is no side door:
* [`spend_cap_exceeded`](/errors/spend_cap_exceeded) means the period’s cap is spent. The detail names the amount, the cap, the period start, and where the cap came from. Running campaigns are unaffected.
* [`spend_state_unknown`](/errors/spend_state_unknown) means the ledger holds chargeable usage on a meter your plan carries no rate for, so period-to-date spend cannot be established. Unknown spend state does not launch: price the meter or clear the cap.
The `usage.threshold` [webhook event](/guides/webhooks) fires at 50%, 80%, and 100% of an included quantity or the spend cap, so wire it to your alerting rather than polling the summary.
### Reading and setting the budget
[Section titled “Reading and setting the budget”](#reading-and-setting-the-budget)
GET /v1/billing/budget
```bash
curl https://api.vocapable.com/v1/billing/budget \
-H "Authorization: Bearer vcp_test_..."
```
```json
{
"currency": "USD",
"balance_minor": 4900,
"spend_cap_minor": 250000,
"spend_cap_period": "month",
"low_balance_threshold_minor": 25000,
"period_spend_minor": 18240,
"unpriced_meters": []
}
```
PUT /v1/billing/budget
```bash
curl -X PUT https://api.vocapable.com/v1/billing/budget \
-H "Authorization: Bearer vcp_test_..." \
-H "Content-Type: application/json" \
-d '{
"spend_cap_minor": 500000,
"spend_cap_period": "month",
"low_balance_threshold_minor": 50000
}'
```
`GET` needs `billing:read`; `PUT` needs `billing:write`. `spend_cap_period` is `day` or `month`; any field sent as `null` clears that setting. `unpriced_meters` is what `spend_state_unknown` is about: while it is non-empty, period-to-date spend cannot be established and launches refuse.
Neither budget route emits `RateLimit-*` headers today. Honor `429` and `Retry-After` rather than keying your back-off on the headers alone.
## Converting to production
[Section titled “Converting to production”](#converting-to-production)
`POST /v1/subscription` starts an eligible tenant’s production subscription. It takes the `workload_class` you declared and a `term`, creates the subscription, and redeems the sandbox credit against it; it does **not** promote the workspace to live:
POST /v1/subscription
```bash
curl -X POST https://api.vocapable.com/v1/subscription \
-H "Authorization: Bearer vcp_test_..." \
-H "Content-Type: application/json" \
-d '{"workload_class": "sales", "term": "three_year"}'
```
The response names the plan, the term, the base price, the session counts, the period, and `sandbox_credit_redeemed_minor`. Watch `sandbox_credit_unconfirmed`: when it is present, the subscription exists and the credit redemption did not confirm, so the amount it names is still owed to you and support will apply it - the subscription is not rolled back for it.
`workload_class` is one of `engage`, `workforce`, `sales` - the class you declared during onboarding, which the screening floor validated. `term` is `mtm` or `three_year`; the term moves the AI-minute rate, never the base fee. A `three_year` order requires the current 3-Year Commitment Addendum to be accepted first (`POST /v1/agreements/accept`) - without it the purchase answers [`agreements_required`](/errors/agreements_required), naming the outstanding document, and nothing is charged.
`extra_sessions` is optional (default `0`): the **quantity** of the published extra-concurrent-session add-on to start with, an integer ≥ 0 - never a price, which the catalog owns. On a deployment where the add-on is not yet configured to bill, a non-zero quantity answers [`extra_sessions_not_purchasable`](/errors/extra_sessions_not_purchasable) and nothing is created.
The endpoint needs `billing:write`. It does **not** accept `Idempotency-Key`: the server commits one tenant-unique provisioning operation before it calls Stripe. If a provider outcome is unknown, it answers [`subscription_provisioning_pending`](/errors/subscription_provisioning_pending) and will not submit another order. Wait for the signed Stripe subscription webhook, then refresh `GET /v1/subscription`; do not change the plan and click again. Once the local mirror exists, another attempt answers [`subscription_exists`](/errors/subscription_exists).
## Extra concurrent sessions
[Section titled “Extra concurrent sessions”](#extra-concurrent-sessions)
`GET /v1/subscription` reports `extra_sessions` (the purchased add-on quantity) and `total_sessions` (`plan.concurrency_limit + extra_sessions` - the one entitlement number session admission enforces), and `plan.term` (`mtm` or `three_year`; `null` on operator-authored plans that predate the vocabulary).
Change the purchased quantity with `PUT /v1/subscription/extra-sessions` (`billing:write`). The body carries the **absolute** target, never a delta - setting 3 twice leaves you with 3 - so an ambiguous outcome cannot double a fee and retrying the same number is always safe:
PUT /v1/subscription/extra-sessions
```bash
curl -X PUT https://api.vocapable.com/v1/subscription/extra-sessions \
-H "Authorization: Bearer vcp_test_..." \
-H "Content-Type: application/json" \
-d '{"extra_sessions": 3}'
```
A definite success answers the full refreshed Subscription DTO. The refusals:
* [`subscription_not_found`](/errors/subscription_not_found) (404) - no production subscription exists yet.
* [`subscription_inactive`](/errors/subscription_inactive) (409) - the subscription is not in an active state; nothing changed.
* [`subscription_unmanaged`](/errors/subscription_unmanaged) (409) - the subscription carries no Stripe reference (dev-seeded or externally provisioned), so there is no subscription item to change.
* [`extra_sessions_not_purchasable`](/errors/extra_sessions_not_purchasable) (409) - the add-on has no configured price in this deployment.
* [`extra_sessions_update_failed`](/errors/extra_sessions_update_failed) (502) - Stripe definitively refused the change; the local quantity is unchanged and the detail carries the refusal.
* [`extra_sessions_update_unconfirmed`](/errors/extra_sessions_update_unconfirmed) (502) - Stripe did not answer, so the outcome is **unknown**, not failed: the change may still have applied. The local quantity stays unchanged until the signed subscription webhook reconciles it. Refresh `GET /v1/subscription` shortly rather than assuming failure; because the quantity is absolute, re-sending the same number is safe.
## Sandbox and production
[Section titled “Sandbox and production”](#sandbox-and-production)
Entry is the **$49 sandbox**, a one-time account fee, credited in full against your first production invoice. It includes the full builder, free simulated `vcp_test_` calls (which produce **no billable usage**, ever), and paid platform test calls to verified numbers with starter credit included. Production arms behind an executed Production Services Order, a plan keyed to your declared workload class, payment, and BYO carrier verification. When those persisted requirements are complete, the active owner requests staff review; only staff’s fresh-step-up, dual-control promotion makes the workspace live and permits the one-time live-key handoff.
Current published rates live on the [pricing page](https://www.vocapable.com/pricing), because the rate card is public so you can price a campaign before the first call, and this site does not duplicate numbers that page owns. Legal terms live at [www.vocapable.com/legal](https://www.vocapable.com/legal).
# Campaigns & scrub
> The launch path: import contacts, run a scrub, acknowledge the report, and launch, with every refusal the gate can raise.
A campaign never launches on an unexamined list. The path is: import → scrub → **acknowledge the scrub report** → create → launch. Each arrow is a gate with a named refusal.
## Import contacts
[Section titled “Import contacts”](#import-contacts)
`POST /v1/contact-imports` (multipart CSV + column-mapping JSON) returns `202` with an import job. Poll `GET /v1/contact-imports/{id}` to a terminal status, because imports are not in the webhook catalog, so polling is the completion signal. Per-row failures (bad E.164, duplicate, missing required column) are reported at `GET /v1/contact-imports/{id}/errors` as JSON or CSV (`Accept: text/csv`). Rows land in a contact list (`cl_…`).
### Provenance: two tiers, and only one of them is a refusal
[Section titled “Provenance: two tiers, and only one of them is a refusal”](#provenance-two-tiers-and-only-one-of-them-is-a-refusal)
Scraped contacts - and anything else obtained by unauthorized extraction - are **refused on the platform**, however they arrive: upload, API, or CRM sync. No supplier agreement stands behind data that was taken, so there is nothing for an attestation to attest to.
Lists you **purchased, rented, or had appended** are a different tier. They are your data, acquired under your own supplier agreement, dialed from your own carrier account, so you may import and call them once your account carries the executed **Cold Outreach & Data Provenance Waiver** (published with the rest of the set at [www.vocapable.com/legal](https://www.vocapable.com/legal)). The same waiver covers outreach to people who have not consented to be called. You sign it, and it places the duty for that program on you: the platform’s controls are operational safeguards, and none of them is a determination that a given call is permitted.
Which tier you are in is **not a field on the import**. A contact list’s `source` describes how the rows arrived (`upload` or `api`) and never where they came from; there is no `purchased` value to send, and no endpoint asks the question at import time. It is answered once on your account’s onboarding screen, and the waiver is what carries the answer. Every import still sends `consent_attestation` (signer name, signer title, affirmation, and one `consent_basis` of `pewc`, `pec`, `ebr`, or `transactional`), which the list records permanently: `PATCH /v1/contact-lists/{id}` renames a list and deliberately cannot rewrite its basis or attestation ref. That basis is not paperwork. It is read on every attempt, and what it says decides whether the dial happens - see [Dial time](#dial-time-reads-the-consent-basis-you-sent).
## Scrub and acknowledge
[Section titled “Scrub and acknowledge”](#scrub-and-acknowledge)
`POST /v1/scrub-runs {"target": {"contact_list_id": "cl_…"}}` → `202`; poll the run until `completed`. The report carries `totals` (contacts/dialable/blocked), a `blocked_breakdown` by reason (federal DNC, internal DNC, reassigned, litigator, state-blocked, line type), the `dataset_versions` it scrubbed against, and an `expires_at`.
Scrub stages fail closed, with one documented exception: if the litigator-scrub vendor is down, the run completes flagged `"risk_flags": ["litigator_unscreened"]` rather than blocking.
A human or system must then **acknowledge the report** with `POST /v1/scrub-runs/{id}/acknowledge`, recording actor, timestamp, and the totals as seen. Acknowledging totals that do not match the run refuses with [`scrub_totals_mismatch`](/errors/scrub_totals_mismatch): the acknowledgment attests to what you *saw*. Runs go stale at `expires_at` ([`scrub_run_expired`](/errors/scrub_run_expired)).
## Create the campaign
[Section titled “Create the campaign”](#create-the-campaign)
`POST /v1/campaigns` names the published agent version, the target list, a number pool, a compliance profile, the schedule, retry policy, and the `scrub_run_id`. Caller ID is **always a pool reference**, and the API never accepts an inline array of numbers; rotation order and per-number daily caps are properties of the pool. Calling windows are evaluated in each contact’s timezone, and the gate may apply stricter policy restrictions for that contact. The current portal and API expose no calling-policy configuration. `telephony_account_id` defaults to your account; when supplied it must be the account behind every number in the pool.
## Launch, and every way it refuses
[Section titled “Launch, and every way it refuses”](#launch-and-every-way-it-refuses)
`POST /v1/campaigns/{id}/launch` (with `Idempotency-Key`, required) returns `200 {"status": "running", "dialable_contacts": …}`. The gate refuses with a `409` problem when:
* [`scrub_required`](/errors/scrub_required) means no scrub run covers the target, or it has not finished
* [`scrub_unacknowledged`](/errors/scrub_unacknowledged) means the report is fresh but nobody acknowledged it
* [`scrub_expired`](/errors/scrub_expired) means the run went stale; re-run it
* [`agent_not_published`](/errors/agent_not_published) means drafts are never dialed
* [`telephony_account_unverified`](/errors/telephony_account_unverified) means an unverified carrier account cannot dial
* [`spend_cap_exceeded`](/errors/spend_cap_exceeded) / [`spend_state_unknown`](/errors/spend_state_unknown) means the spend cap is spent, or cannot be measured at all; both fail closed
At dial time each attempt still passes the ordered pacer governors, abandon-rate ceiling, policy re-check, per-account CPS budget, spend cap, caller-ID rotation caps. AMD-detected machines are hung up and recorded as attempts only.
## Dial time reads the consent basis you sent
[Section titled “Dial time reads the consent basis you sent”](#dial-time-reads-the-consent-basis-you-sent)
The waiver decides what your account may run. It does not change what the gate computes for an individual contact, and integrators are caught by that distinction more than by anything else on this page.
Every attempt resolves the campaign purpose to a required consent class and compares it against the basis on record for that contact:
* a basis **below** the requirement refuses with `consent_insufficient` - this is also what an expired consent record produces, because an expired basis is held as none
* a basis high enough but **scoped to another purpose** refuses with `consent_scope_mismatch`; consent to appointment reminders does not authorize a sales campaign
Both are policy refusals rather than scrub blocks, which is a distinction your reconciliation code can see: they land at `GET /v1/campaigns/{id}/outcomes` as `disposition: "blocked_by_policy"`, where a contact stopped at materialization lands as `scrub_blocked`. On the ad-hoc path the same reasons ride the `errors[]` array of the `409` [`contact_not_dialable`](/errors/contact_not_dialable) problem. Accepting the waiver clears neither, and neither is a defect: a cold program still has to describe the basis it holds, and `pewc`, `pec`, `ebr`, and `transactional` remain the only four values the API accepts.
The floor controls are in the same position. The AI-disclosure opener, internal-DNC suppression at batch and at dial time, and opt-out capture run on cold outreach exactly as on consented traffic; no agreement you sign and no field you send reaches them.
## Monitor and reconcile
[Section titled “Monitor and reconcile”](#monitor-and-reconcile)
Poll `GET /v1/campaigns/{id}/stats` at 5 seconds (it carries an `ETag`, so unchanged polls are `304`), follow row-level activity at `GET /v1/campaigns/{id}/attempts?updated_at.gte=…`, and reconcile one row per targeted contact at `GET /v1/campaigns/{id}/outcomes`, including contacts blocked at materialization (`disposition: "scrub_blocked"`). Bulk extraction is `POST /v1/exports`. Outcome pushes ride [webhooks](/guides/webhooks).
# Conventions
> Prefixed ULIDs, cursor pagination, Idempotency-Key, rate limits, E.164, RFC 3339, and ETag/304: the rules every endpoint follows.
The API is predictable on purpose: one set of conventions holds everywhere, so what you learn on one resource transfers to all of them.
## IDs: prefixed ULIDs
[Section titled “IDs: prefixed ULIDs”](#ids-prefixed-ulids)
Every ID is a prefixed ULID: `ten_…` tenants, `call_…` calls, `camp_…` campaigns, `agnt_…` agents, `cont_…` contacts, plus per-entity prefixes for the rest (`agv_` agent versions, `tmpl_` templates, `att_` contact attempts, `cl_` contact lists, `scrun_` scrub runs, `imp_` imports, `exp_` exports, `evt_` events, `whe_` webhook endpoints, `tel_` telephony accounts, `pn_` phone numbers, `pool_` number pools, `vn_` verified numbers). IDs are text, k-sortable, and self-describing in logs and support tickets. Never parse them, but do log them.
## Pagination: cursors only
[Section titled “Pagination: cursors only”](#pagination-cursors-only)
All list endpoints paginate by cursor: `?limit=50&cursor=…`, responses wrap as `{"data": […], "next_cursor": "…", "has_more": true}`. There is no offset pagination anywhere, and there is no exception to the envelope - including `GET /v1/api-keys`, which answered a bare array until 2026-08-11 and broke a generic list walker on the first resource most integrations touch. Cursors are **opaque**, so pass back a verbatim `next_cursor` or get [`invalid_cursor`](/errors/invalid_cursor); never construct one.
`limit` defaults to 50 and caps at 200. Walk a collection until `has_more` is `false`:
```bash
cursor=""
while :; do
page=$(curl -s "https://api.vocapable.com/v1/calls?limit=200&cursor=$cursor" \
-H "Authorization: Bearer vcp_test_...")
echo "$page" | jq -c '.data[]'
[ "$(echo "$page" | jq -r .has_more)" = "true" ] || break
cursor=$(echo "$page" | jq -r .next_cursor)
done
```
## Filtering
[Section titled “Filtering”](#filtering)
Flat query params with operator suffixes: `?status=running`, `?created_at.gte=2026-07-01T00:00:00Z`. Comma lists are OR: `?sub_code=booked,interested`. An unsupported field, operator, or value returns [`invalid_filter`](/errors/invalid_filter) naming the offender.
## Idempotency
[Section titled “Idempotency”](#idempotency)
`Idempotency-Key: ` is honored where an endpoint declares it. The key and response body are stored for 24 hours; a replay returns the original result with `Idempotent-Replay: true`. Reusing a key for a *different* request is refused ([`idempotency_key_reuse`](/errors/idempotency_key_reuse)); a retry racing an in-flight original gets [`idempotency_key_in_flight`](/errors/idempotency_key_in_flight), so wait and retry.
**Which endpoints honor it, today:**
| Endpoint | `Idempotency-Key` |
| -------------------------------- | ---------------------------------------------------------------------------------------- |
| `POST /v1/campaigns/{id}/launch` | **Required** ([`idempotency_key_required`](/errors/idempotency_key_required) without it) |
| `POST /v1/agents/{id}/test-call` | Optional |
| `POST /v1/messages` | Optional |
| Everything else | Not read |
The [API reference](/api-reference) is the authority: an endpoint that honors the header declares an `Idempotency-Key` parameter, and one that does not **ignores it** rather than honoring it silently. This page previously said every POST accepted the header. It did not, and a client that retried a timed-out `POST /v1/contacts` on that basis created a duplicate dial record.
Until the guard covers more of the surface, treat a timeout on any POST without the parameter as **unknown**: reconcile by reading the collection (`GET /v1/contacts?phone_e164=…`, `GET /v1/campaigns`) rather than by retrying blind.
One endpoint will never accept it: `POST /v1/agents/{id}/voice-session`. Its response carries a live single-use session token, and the idempotency ledger persists response bodies.
## Rate limits
[Section titled “Rate limits”](#rate-limits)
Token bucket per API key: **600 read requests/min and 120 write requests/min** by default; bulk endpoints (imports, exports) draw from a separate budget. Exceeding the budget returns `429` [`rate_limited`](/errors/rate_limited) with `Retry-After`.
A rate-limited response carries `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset`. Honor those headers rather than hard-coding the numbers, because per-plan multipliers exist - **and treat their absence as “no information”, not as “unlimited”.** Every key-authenticated `/v1` route runs the limiter today, and the server refuses to start if one does not, so the headers should be on every response you see; the guidance stands anyway because a header you did not receive tells you nothing about the budget. A client that back-offs only when `RateLimit-Remaining` reaches zero is fragile; a client that also honors `429` and `Retry-After` is correct everywhere.
## Polling with ETag/304
[Section titled “Polling with ETag/304”](#polling-with-etag304)
Hot read endpoints (`GET /v1/campaigns/{id}/stats`, `/attempts`, `/survey-rollup`) carry an `ETag`; send `If-None-Match` and unchanged polls return `304` with no body. A 5-second stats poll costs 12 req/min, well inside the read budget. Use `updated_at.gte` delta filters on `/attempts` so polls return only the rows that moved. This polling loop **is** the v1 realtime surface; there is no push channel at v1 beyond webhooks.
## Formats
[Section titled “Formats”](#formats)
* **Timestamps** are RFC 3339 UTC with `Z`, as in `2026-08-03T14:05:02Z`.
* **Money** is integer minor units plus a currency code, as in `{"amount_minor": 41250, "currency": "USD"}`. Never floats.
* **Phone numbers** are E.164 only, as in `+13125550188`.
## Versioning
[Section titled “Versioning”](#versioning)
The path carries the major version (`/v1`). Additive changes ship unversioned; breaking changes require `/v2` plus an `api_version` pin, per tenant for request/response shapes, and per webhook endpoint for event payload shapes, so a receiver never has to redeploy in lockstep with the platform. Contract validation failures return **422** [`invalid_request`](/errors/invalid_request) with per-field entries in `errors[]`; see [Errors](/guides/errors) for the problem shape.
# Custom questions
> Typed survey objectives with scoring, and per-call questions on POST /v1/calls - ask something different on every call without republishing the agent.
Your agent asks questions two ways: **objectives** configured on the agent (every call of that version asks them), and **per-call questions** attached to a single `POST /v1/calls` launch. Both record answers through the same tool, land in the same `SurveyResponse`, and arrive in the same webhooks.
## Objectives on the agent
[Section titled “Objectives on the agent”](#objectives-on-the-agent)
Objectives live on the agent definition and freeze into each published version. A scored question carries a `scoring` block:
```json
{
"objectives": [
{
"id": "csat_overall",
"prompt": "How satisfied were you with your visit, one to five?",
"scoring": {
"scale": "csat",
"mapping": { "1": 1, "2": 2, "3": 3, "4": 4, "5": 5 },
"weight": 1
}
},
{
"id": "would_return",
"prompt": "Would you come back?",
"scoring": {
"scale": "ordinal",
"points": 3,
"mapping": { "no": 1, "maybe": 2, "yes": 3 }
}
},
{ "id": "verbatim", "prompt": "Anything else you want us to know?" }
]
}
```
Four scales: `csat` (1–5), `nps` (0–10), `boolean` (`"true"`/`"false"` keys), and `ordinal` (declare `points` and a mapping with exactly that many choices). A question with no `scoring` block is unscored - the answer is still recorded verbatim. A scored question requires an `id` (answers key on it) and question text.
The shape is contract-enforced: a malformed block - an unknown scale, mapping keys outside the scale’s answer set, `ordinal` without a matching `points` - is a `422` at write time and a `409 objective_scoring_invalid` at publish, with `errors[]` naming each broken field. It cannot silently publish and yield `score: null`.
## Captured summary answers
[Section titled “Captured summary answers”](#captured-summary-answers)
An objective can also declare `capture_type` as `string`, `number`, or `boolean` for information extracted into post-call summaries. For example:
```json
{
"id": "years_experience",
"prompt": "How many years of driving experience do you have?",
"capture_type": "number"
}
```
Typed captures require a unique `id` or legacy `name` and question text. In the portal objective editor, choose Text, Number, or Yes or no under **Captured answer type**. Unspecified preserves the existing behavior. This setting controls summary extraction; it does not alter survey scoring or make an assignment or hiring decision.
Where automatic summaries are enabled, missing or refused answers remain absent; an explicit no or zero remains a captured answer. Replies with incorrect types are rejected and retried instead of silently converting strings into answers. Declaring a type does not enable automatic summaries for a deployment without a qualified model.
## Per-call questions
[Section titled “Per-call questions”](#per-call-questions)
`POST /v1/calls` accepts up to 10 additional questions and up to 20 `context` facts for that one call:
```json
{
"agent_id": "agnt_01J9…",
"contact_id": "cont_01J9…",
"questions": [
{
"id": "q_delivered",
"prompt": "Did yesterday's delivery arrive on time?",
"scoring": { "scale": "boolean", "mapping": { "true": 1, "false": 0 } }
}
],
"context": { "order_number": "A-1009", "delivery_date": "yesterday" }
}
```
The rules:
* **Additive only.** A per-call question may not reuse an id the version’s objectives already carry - `409 call_question_conflict`. The published, evaluation-gated objectives are never overridden per call.
* **The version must grant `record_survey_answer`.** Otherwise the answers would have no tool to record them - `409 call_questions_unsupported` at launch, not a silent no-op after the call.
* **Context keys are snake\_case strings**, ≤500 characters each, ≤8 KiB total. Platform-authored keys (`current_datetime`, `contact_name`, …) are refused - you cannot shadow what the platform tells the agent.
Per-call questions ride outside the agent’s cached prompt prefix, so they add no latency penalty, and they score into the same `SurveyResponse` as version objectives.
## Reading the answers
[Section titled “Reading the answers”](#reading-the-answers)
* Push: `survey.completed` and `call.ended` webhooks carry `answers` and `score` inline - see [Receive results](/guides/receive-results).
* Pull: `GET /v1/survey-responses?call_id=…` returns `{answers: {id: {answer, verbatim_quote, confidence}}, score, completion_status}`.
* Aggregate: `GET /v1/campaigns/{id}/survey-rollup` for campaign questions.
The score is computed once, at call completion, from the exact question set the call ran - deterministic and reproducible; editing the agent later never restates a past response.
# Errors
> RFC 9457 problem responses with stable, machine-matchable type URIs that resolve to the error registry on this site.
Every non-2xx response is an RFC 9457 `application/problem+json` body:
```json
{
"type": "https://docs.vocapable.com/errors/contact_not_dialable",
"title": "Contact blocked by scrub",
"status": 409,
"detail": "Contact cont_01JA0M… is on the federal DNC list (dataset 2026-07-27).",
"instance": "/v1/campaigns/camp_01JA0V.../launch",
"errors": [{ "field": "contact_id", "reason": "dnc_federal" }]
}
```
* **`type`** is stable and machine-matchable, and it resolves: every code the API can emit has a page at `https://docs.vocapable.com/errors/{code}`, indexed in the [error registry](/errors).
* **`errors[]`** carries field-level detail for validation failures. [`invalid_request`](/errors/invalid_request) is the only code that routinely carries many entries.
* **`detail`** is human-oriented prose. Log it; never branch on it.
## Branch on the code, not the status
[Section titled “Branch on the code, not the status”](#branch-on-the-code-not-the-status)
The final path segment of `type` is the error code (`snake_case`), and the registry is a promise: **a code is never re-pointed at a different meaning** A changed meaning is a new code, because the old one is something an integrator wrote an `if` against. Branch on the code alone and treat the status as redundant confirmation:
```ts
const problem = await response.json();
const code = new URL(problem.type).pathname.split("/").pop();
switch (code) {
case "scrub_unacknowledged":
// fresh report exists: acknowledge it, then relaunch
break;
case "rate_limited":
// honor Retry-After
break;
}
```
## The two families of 5xx
[Section titled “The two families of 5xx”](#the-two-families-of-5xx)
Neither is ever the caller’s fault, and neither is fixed by changing the request:
* **`503`** means *this deployment is missing a dependency it needs* (the database, a live telephony provider, the calendar aggregator). The request may succeed later or on another environment. Retry with backoff.
* **`501`** means *the code path is deliberately unbuilt here*, and retrying will never help until it ships (for example [`knowledge_upload_extractor_unavailable`](/errors/knowledge_upload_extractor_unavailable)). Surface it; do not retry.
## Codes worth special handling
[Section titled “Codes worth special handling”](#codes-worth-special-handling)
* [`unauthenticated`](/errors/unauthenticated) (401) is **byte-identical** for absent, malformed, unrecognized, revoked, and expired keys, so you cannot distinguish them, by design. Rotate the credential.
* [`rate_limited`](/errors/rate_limited) (429) arrives with `Retry-After` and the `RateLimit-*` headers; see [Conventions](/guides/conventions#rate-limits).
* [`contact_not_dialable`](/errors/contact_not_dialable) (409) carries `blocked_reasons[]` in `errors[]`. When it is raised by a compliance-floor control it is **never waivable**, so treat it as terminal for that contact, not as a retry candidate.
* The idempotency trio, [`idempotency_key_required`](/errors/idempotency_key_required), [`idempotency_key_reuse`](/errors/idempotency_key_reuse), [`idempotency_key_in_flight`](/errors/idempotency_key_in_flight), is covered in [Conventions](/guides/conventions#idempotency).
## Validation errors are 422, structured
[Section titled “Validation errors are 422, structured”](#validation-errors-are-422-structured)
Body or query contract failures return **422** [`invalid_request`](/errors/invalid_request), with one `errors[]` entry per offending field, as in `[{"field": "schedule.start_date", "reason": "…"}]`. Bad filters and cursors are **400** ([`invalid_filter`](/errors/invalid_filter), [`invalid_cursor`](/errors/invalid_cursor)). Wire your client to render `errors[]` next to the fields it names; the platform will never return a bare 500 for a validation problem.
The full registry, listing every code, its status, and its meaning, grouped exactly as the API documentation registers them, lives at [/errors](/errors).
# Headless mode
> Your stack as the system of record: contacts created via API with a consent basis, calls driven from your own scheduler, outcomes returned by webhook, with no CSV and no list upload.
Vocapable has two first-class consumption modes on the same engine and the same `/v1`, same compliance floor, same pricing. In **Workspace mode** you send data and Vocapable is your calling system of record. In **Headless mode** your stack stays the system of record and Vocapable is consumed as `/v1` + webhooks.
**There is no mode flag in the product.** A tenant simply uses the surfaces it wants, and may mix them freely: a campaign for the quarterly survey, API-driven dials from your own scheduler, both on one tenant.
## What headless looks like today
[Section titled “What headless looks like today”](#what-headless-looks-like-today)
The loop below runs entirely on `/v1` with no CSV, no bulk upload, and no one opening the dashboard:
1. **Create the contact** with `POST /v1/contacts`, singly or in batches, as an operational dial record. Its initial consent lands atomically with the contact, and its timezone and jurisdictions are durable gate facts that the platform never guesses.
2. **Queue one call** with `POST /v1/calls`, naming only that existing `contact_id` and an existing `agent_id`. The server pins the agent’s latest published version, chooses the verified default route/caller configuration, and writes a campaign-less attempt.
3. **Receive supported call-lifecycle events by [webhook](/guides/webhooks)**, such as `call.ended`. `appointment.booked` is also emitted after a configured live phone call successfully writes through the tenant’s active direct Google Calendar connection; `shift.confirmed` and `survey.completed` remain schema-reserved. Your scheduler never polls for supported call outcomes.
For a live rehearsal of the dial pipeline without a campaign at all, use `POST /v1/agents/{id}/test-call` against a number your tenant has verified. On a `vcp_test_` key that runs the full gate → materialize → seize → originate → outcome path synchronously and for free ([Test mode](/guides/test-mode)).
1\. Create the contact and its initial consent
```bash
curl -X POST https://api.vocapable.com/v1/contacts \
-H "Authorization: Bearer vcp_test_..." \
-H "Content-Type: application/json" \
-d '{
"phone_e164": "+13125550188",
"first_name": "Dana",
"timezone": "America/Chicago",
"jurisdictions": ["US", "US-IL"],
"consent": {
"consent_basis": "pewc",
"captured_at": "2026-08-04T16:11:09Z",
"scope": {"purposes": ["marketing"], "channels": ["voice"]},
"evidence": {
"artifact_type": "web_form",
"artifact_ref": "crm://consent/lead-784",
"content_hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
},
"disclosure_text_shown": "By checking this box you agree to receive automated calls..."
}
}'
```
2\. Queue the contact-bound ad-hoc call
```bash
curl -X POST https://api.vocapable.com/v1/calls \
-H "Authorization: Bearer vcp_test_..." \
-H "Idempotency-Key: 8f14e45f-ceea-467a-9f6b-1c2d3e4f5a6b" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "agnt_01...",
"contact_id": "cont_01..."
}'
```
`Idempotency-Key` is **required** on this endpoint because a retry can dial someone twice. The body is closed: it has no `to`, `caller_id`, `telephony_account_id`, or `variables` field. To make many calls, use the campaign route and its scrub workflow; this endpoint is the one-contact headless counterpart, not a bulk-dial backdoor.
## Consent attaches atomically to the contact
[Section titled “Consent attaches atomically to the contact”](#consent-attaches-atomically-to-the-contact)
Headless `POST /v1/contacts` requires an initial `consent` object. It includes `consent_basis` (`pewc`, `pec`, `ebr`, or `transactional`), the actual `captured_at` time, a non-empty purpose/channel `scope`, and durable evidence (`artifact_type`, `artifact_ref`, and its SHA-256 `content_hash`). For PEWC, include the verbatim `disclosure_text_shown` too. The server resolves the current API Source Contact Consent Attestation itself and appends the matching consent record in the same transaction; callers cannot supply a provenance or attestation id.
An API key creating contacts needs the account agreements and current API Source Contact Consent Attestation. A live key additionally needs the production agreement set. This proves contact provenance; it never replaces the per-contact evidence above.
Provenance is delivery-mechanism-neutral, and so is the line drawn through it. **Scraped contacts are refused on the platform however they arrive** - one `POST` at a time changes nothing about data that was extracted without authorization. **Purchased, rented, and appended lists are a different tier**: they are yours under your own supplier agreement, and they run - over the API exactly as over an upload - under the executed Cold Outreach & Data Provenance Waiver your account signs, which is also what covers calling people who have not consented to be called. Signing it puts that program’s duty on you; the platform’s controls stay operational safeguards and clear nothing on your behalf.
What the API asks of you is unchanged in both tiers. `POST /v1/contacts` still requires the whole `consent` object shown above - one of the four `consent_basis` values, a true `captured_at`, a non-empty `scope`, durable evidence and its hash - and the waiver adds no fifth “none” value, no `provenance` field, and no bypass flag to that body. That object is not a provenance declaration; it is the input the dialability gate reads at dial time, which is why cold outreach still has to describe the basis it holds. A contact whose recorded basis or scope cannot carry the purpose is refused there with `consent_insufficient` or `consent_scope_mismatch` among the `errors[]` reasons of the `409` below, whatever your account has signed.
One asymmetry to design around: `POST /v1/calls` names no campaign, so the attempt declares **no purpose class and the gate applies the strictest one** rather than guessing a lenient one - an ad-hoc dial is evaluated as marketing. A contact whose basis would clear a reminder campaign can therefore still be refused on the ad-hoc path.
## No side door
[Section titled “No side door”](#no-side-door)
API-driven dials pass **the same dialability gate as every other dial**: internal DNC, scrub/policy verdict, consent basis, quiet hours, frequency caps, and (for live calls) subscription, spend, and dunning gates. A blocked contact returns `409` [`contact_not_dialable`](/errors/contact_not_dialable) with field-level `errors[]` naming the blocked `contact_id` and reason.
Two fail-closed refusals matter more in headless mode because your records may be thinner than an imported list: the gate needs a resolvable timezone and jurisdiction for every dial, and it never guesses. The current portal and API expose no calling-policy configuration; this guide describes the gate’s evaluated state, not a determination of what the law requires. [`contact_timezone_unresolved`](/errors/contact_timezone_unresolved) and [`jurisdiction_unresolved`](/errors/jurisdiction_unresolved) are what you get otherwise, so carry timezone and jurisdiction facts on your contacts - as the example above does.
The compliance floor runs on every call, in every telephony mode, through every entry point: the AI-disclosure opener, internal-DNC suppression, and the platform kill switches. A configured live phone call durably records an opt-out before it closes; browser and simulated test calls have no contact to suppress. Each waived element is written into the attempt’s `policy_snapshot` as an explicit election, so an evidence export shows which checks ran, which were waived, and on whose signature.
## Your Twilio, your production contract
[Section titled “Your Twilio, your production contract”](#your-twilio-your-production-contract)
Every production dial rides your own verified Twilio subaccount, so connect-and-verify is an onboarding step (`POST /v1/telephony-accounts`, then `…/verify`, and `PATCH /v1/telephony-accounts/{id}` to rotate the credentials later). An ad-hoc live call uses the tenant’s verified **default** BYO account and only an active, tenant-assigned, carrier-confirmed voice number on it; the request cannot select another route or caller ID. Headless production uses the **same Production Services Order** as everything else: the `production` agreements scope arms live ad-hoc calls, campaign create and launch, telephony-account create, and imports of non-verified numbers ([`agreements_required`](/errors/agreements_required) until it is executed). Completing that gate set does not self-activate the workspace: the active owner requests review and staff must complete the fresh-step-up, dual-control promotion before the owner can claim the one-time live key.
### Reading an `agreements_required` 403
[Section titled “Reading an agreements\_required 403”](#reading-an-agreements_required-403)
Every agreement gate answers in one shape, so an integrator can cure a whole set in one pass instead of discovering it one refusal at a time. The `type` resolves to `agreements_required`, `detail` names which set the surface needs, and `errors[]` carries one entry per outstanding document:
403 agreements\_required
```json
{
"type": "https://docs.vocapable.com/errors/agreements_required",
"title": "The current legal agreements have not been accepted",
"status": 403,
"detail": "This surface creates live production calling, so it requires the production document set (ADR-0008) on top of the account set. Accept the current version of each via POST /v1/agreements/accept. Outstanding: production_services_order, scrub_attestation.",
"instance": "/v1/calls",
"errors": [
{ "field": "agreement_kind", "reason": "production_services_order" },
{ "field": "agreement_kind", "reason": "scrub_attestation" }
]
}
```
Cure it with `GET /v1/agreements` → `POST /v1/agreements/accept`, echoing the exact `version` and `content_sha256` you were served; an acceptance of bytes the API did not serve is refused, and any version or hash change re-arms the affected gate automatically. The `gate_scope` on each document tells you which surfaces it arms - `account`, `production`, `headless` (the API Source Contact Consent Attestation), or `commitment` (the 3-year term). Branch on `errors[].reason`, never on the prose in `detail`.
Cold outreach waiver: gate not shipped yet
The Cold Outreach & Data Provenance Waiver is a published document that onboarding asks you to accept, and it is **not yet a served entry in the agreements registry**. So `GET /v1/agreements` does not list it today, no `403 agreements_required` names it today, and there is no `cold_outreach` gate scope - the four above are the whole set.
**TODO (backend):** the specified entry is agreement kind `cold_outreach_waiver` inside the existing `production` scope, re-verified at production activation against the recorded acceptance, signer, timestamp and content hash. When it lands it becomes one more `errors[].reason` in exactly the 403 above, which is why an integrator that branches on `errors[].reason` rather than counting documents needs no change. Do not build against it until this note goes away.
You decide who to call and under what basis; the API describes what it checked and refuses what it cannot clear.
## The reference is the contract
[Section titled “The reference is the contract”](#the-reference-is-the-contract)
Every endpoint named on this site exists in the [API reference](/api-reference), which is generated from the server’s own OpenAPI document. If a page here and the reference disagree, the reference is right - and tell us, because the disagreement is the bug.
Where a dashboard surface has no API behind it yet, the honest list is kept in the API reference rather than implied here. The headless contact and one-call flow above are available over `/v1`.
# Receive results
> Every call's answers, disposition, and analysis pushed to your endpoint - payloads, the test ping, and self-service redrive.
Configure one endpoint and the platform pushes the results you subscribe to. Call completion, saved business actions and later analysis are separate events; enable the ones you need with `PUT /v1/webhook-endpoint {"url": …, "enabled_events": […]}`.
## Review results in the workspace
[Section titled “Review results in the workspace”](#review-results-in-the-workspace)
**Home → Recent conversations** and **Activity → Calls** show up to three current recorded responses from each call, including qualification status, shift responses, and saved or proposed caller details. These remain available when an AI summary is unavailable. If a call has more responses, the preview shows the total; open the call for every response, its caller evidence and correction history.
Proposed contact changes require review before changing an existing contact. A recorded shift response does not confirm an assignment. Qualification notes describe what was recorded and do not establish a booked appointment or callback.
The call-list API adds `recorded_responses` and `recorded_response_count` separately from AI `summary` and `captured_fields`. Preview notes and values may be shortened; their `note_truncated` and `value_truncated` flags identify shortened values. The full receipt remains in the call detail’s `conversation_actions` collection.
## The result events
[Section titled “The result events”](#the-result-events)
**`call.ended`** - appended in the same transaction that finalizes the call:
```json
{
"call_id": "call_01J9…",
"contact_id": "cont_01J9…",
"agent_version_id": "agv_01J9…",
"direction": "outbound",
"duration_ms": 93000,
"disposition": "completed_no_goal",
"sub_code": "survey_completed",
"recording_available": true,
"transcript_available": true,
"transcript_url": "/v1/calls/call_01J9…/transcript",
"survey_response": {
"survey_response_id": "srv_01J9…",
"completion_status": "complete",
"score": 0.83,
"answers": {
"csat_overall": { "answer": 4, "verbatim_quote": "a four, pretty good" },
"q_delivered": { "answer": true, "verbatim_quote": "yes it did" }
}
},
"analysis_pending": true
}
```
The completion event carries call metadata and inline survey/appointment results. Saved action receipts and later analysis arrive separately. Transcript **text** stays out by design - `transcript_url` points at the authenticated API. `analysis_pending: true` says the post-call judge hasn’t run yet; its verdict arrives as `call.analyzed`.
**`survey.completed`** - when the call produced a survey response: the ids, `completion_status`, `score`, and the same inline `answers`.
**`call.analyzed`** - minutes later, the judge’s verdict: `success`, `score`, a bounded `summary`, `extracted_fields`, per-dimension results (goal completion, disclosure, opt-out honored, knowledge grounding), and latency percentiles - the same shape `GET /v1/calls/{id}` serves in `analysis`.
Answer entries carry the caller’s `verbatim_quote`, so the events you enable are your consent surface for conversation-derived data reaching your endpoint.
## One company, several workflows
[Section titled “One company, several workflows”](#one-company-several-workflows)
A snow-service company can use one receiver for customer reception, worker support, shoveler outreach and customer qualification. Route each verified event inside your integration; subscribing another workflow does not require replacing the company’s existing webhook endpoint.
| Work | Events to consider | What to save in your system |
| ----------------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Customer or worker support | `message.taken`, `ticket.opened`, `ticket.resolved` | The caller-request ID, call/contact references and the recorded message or ticket state. Use the request ID to correlate resolution. |
| Availability for several shifts | `conversation.action_recorded` with `data.kind = "shift_response"` | A response for each call and `subject_ref` (shift), plus its source turns and correction history. Accepted, declined and tentative are separate responses. |
| Customer fit or candidate screening | `conversation.action_recorded` with `data.kind = "qualification"` | The `sales` or `recruiting` subject, recorded status, summary and configured answers in `payload`. Preserve `answered`, `unknown` and `refused` rather than converting missing answers to “no.” |
| Call summary | `call.ended`, then `call.analyzed` | Call completion first, then the bounded summary and extracted fields. Analysis can arrive later and does not prove an action succeeded. |
| Suppression | `optout.recorded` | The suppression reference and originating call, routed to your contact/suppression workflow. “Not interested” alone is a qualification response, not this event. |
A worker’s accepted response is **not a staffing assignment**. Your staffing system or dispatcher confirms the assignment. Likewise, captured interest or a next-step note is not a qualified customer, booked callback or hiring decision. Require the corresponding saved action or human decision before promising it.
`conversation.action_recorded` carries an immutable receipt in `data`: its `id`, `call_id`, nullable `contact_id`, `kind`, `subject_ref`, `revision`, `response`, `payload`, `source_turns` and `created_at`, plus confirmation evidence when present. This event is committed with the receipt. The legacy `shift.confirmed` event is a compatibility projection; use the receipt collection when one call discusses several shifts.
### Keep corrections and separate shifts
[Section titled “Keep corrections and separate shifts”](#keep-corrections-and-separate-shifts)
Keep receipt history by `data.id`. For a current-response view, group by `(tenant_id, call_id, kind, subject_ref)`. Rank receipts by the highest `source_turns[].turn_number` first (zero when absent), then `revision` as a tie-breaker. Arrival time and revision alone cannot determine the current answer: a delayed older turn can be saved with a higher database revision.
For example, these are reduced views of four shift-response receipts in one call:
| Shift (`subject_ref`) | Response | Highest source turn | Revision | Current response |
| --------------------- | --------- | ------------------- | -------- | ----------------------------------------- |
| morning-route | accepted | 4 | 1 | Replaced by the later caller correction |
| afternoon-route | tentative | 6 | 1 | Tentative for this separate shift |
| morning-route | declined | 9 | 2 | Declined for the morning shift |
| morning-route | accepted | 3 | 3 | Delayed earlier evidence; keep in history |
Serialize updates to each current-response record or use a transactional compare and update so concurrent deliveries cannot overwrite newer evidence. Scope records to the verified tenant. Dedupe the envelope’s `event.id`, save the receipt and update the current view in one transaction, or first persist the whole event in a durable queue and process it idempotently. Return `2xx` only after durable acceptance. The signature verifier below does not implement this storage step.
For recovery, `GET /v1/calls/{call_id}` with `calls:read` returns the call’s `conversation_actions` history. Rebuild each subject’s current response using the same ordering. A call’s general disposition or analysis summary does not replace the per-subject receipts.
### Preserve the existing connection
[Section titled “Preserve the existing connection”](#preserve-the-existing-connection)
Read `GET /v1/webhook-endpoint` before changing subscriptions. Preserve its URL, `api_version_pin` and existing `enabled_events`; explicitly add the selected workflow events to that set. `PUT /v1/webhook-endpoint` replaces the configuration, so sending only the new workflow’s events can stop the existing support feed. Endpoint setup requires `webhooks:manage`; reading calls requires `calls:read`. Keep API keys and signing secrets on your server.
Use the portal’s integration setup or your own receiver to forward verified records into a CRM, staffing system or helpdesk. These are API/webhook integration paths; they do not imply a native connector to a particular vendor.
## Verify signatures
[Section titled “Verify signatures”](#verify-signatures)
Every delivery is HMAC-signed: `Vocapable-Signature: t=,v1=`. Verify with the snippets in the [Webhooks guide](/guides/webhooks) - reject skew over 5 minutes, and accept either `v1` entry during a secret rotation’s 24-hour overlap.
## Prove the pipe before you need it
[Section titled “Prove the pipe before you need it”](#prove-the-pipe-before-you-need-it)
```bash
curl -X POST https://api.vocapable.com/v1/webhook-endpoint/test \
-H "Authorization: Bearer $VOCAPABLE_API_KEY"
```
Sends one signed sample (`data.test: true`) with production mechanics - same envelope, same signature, same secrets, same 10-second success rule - and reports `{delivered, status_code}`. A receiver that verifies the ping verifies real traffic. Recognize `data.test: true` and acknowledge it without creating customer work.
## Recover after an outage
[Section titled “Recover after an outage”](#recover-after-an-outage)
Deliveries retry on a ladder (1m, 5m, 30m, 2h, 6h, then 6-hourly) for 24 hours, then mark `exhausted`. If your receiver was down longer than that:
```bash
curl https://api.vocapable.com/v1/webhook-deliveries?status=exhausted \
-H "Authorization: Bearer $VOCAPABLE_API_KEY"
curl -X POST https://api.vocapable.com/v1/webhook-deliveries/whd_01J9…/redrive \
-H "Authorization: Bearer $VOCAPABLE_API_KEY"
```
Each redrive grants exactly one more attempt and is available only for an exhausted delivery. Delivery remains at-least-once, so receivers must dedupe on `event.id`.
## Or poll
[Section titled “Or poll”](#or-poll)
Everything pushed is also pullable: `GET /v1/calls/{id}`, `/transcript`, `GET /v1/survey-responses?call_id=…`, `GET /v1/campaigns/{id}/survey-rollup`, and bulk `POST /v1/exports`. Webhooks are the fast path, not the only path.
# Test mode
> vcp_test_ keys route all telephony to a mock provider: free, no PSTN, with deterministic outcomes selected by the destination's last digit.
Test keys are a **security property, not a flag**: under a `vcp_test_` key, *all* telephony routes to a mock provider. No PSTN dial ever originates from a test key, in either telephony mode, and a test-mode call never touches your Twilio subaccount. Dial outcomes (ringing, answering-machine detection, answer) are simulated deterministically and produce **no billable usage**.
## Deterministic outcomes
[Section titled “Deterministic outcomes”](#deterministic-outcomes)
Outcomes derive from the destination number, never from randomness, so the same number always behaves the same way and you can pick the behavior you want to exercise. The **last digit** selects the outcome:
| Last digit | Outcome |
| ---------- | ---------------------------------------------------- |
| `0`–`5` | A human answers |
| `6`–`7` | An answering machine, so AMD reports `machine_start` |
| `8` | Nobody answers |
| `9` | The carrier rejects the call |
Want to test your retry policy? Dial a number ending in `8`. Want to see how AMD-detected machines land as attempts? End in `6`. What the dialer does with a `machine_start` verdict today is hang the call up and record a contact attempt in the `machine` state - a machine is never a conversation - and your fixtures can rely on that in both key modes.
Voicemail drop: policy changed, mechanism in flight
Leaving an artificial-voice voicemail is **permitted under the executed Cold Outreach & Data Provenance Waiver** - one of the programs that document covers, on the same terms as the rest: your list, your carrier, your duty. The policy gate that used to refuse it is gone. An artificial-voice voicemail is itself a regulated call, so it carries the AI-disclosure opener and every other floor control when it runs.
**TODO (voice runtime / campaign engine):** the *mechanism* is being built and is not described here, because nothing in `/v1` drops a message yet. No request field turns it on, `machine_start` still ends in a hangup, and the deterministic `6`–`7` fixtures above are unaffected. When the drop ships, its request surface, its outcome state, and its metering land in the [API reference](/api-reference) first - that document is generated from the server, so it is the one that cannot describe a mechanism that does not run.
## Test calls run in-request
[Section titled “Test calls run in-request”](#test-calls-run-in-request)
`POST /v1/agents/{id}/test-call` behaves differently and honestly on the two key types:
* **`vcp_test_` key** (`"telephony": "mock"`): the dial runs **synchronously, in-request**, running the full dial pipeline (dialability gate, materialization, seize, originate, AMD/status outcome), free, no PSTN. The `202` response’s `state` already carries the deterministic outcome. It exercises the dial pipeline, **not the audio leg**, so you will not hear your agent on a mock call.
* **Live key** (`"telephony": "platform"`): nothing originates in-request. The `202` returns `state: "scheduled"`, the origination loop dials on your default telephony account as paid platform test-call minutes, and the destination must be an OTP-verified number you control. Live test calls are volume-capped per tenant per UTC day ([`test_call_cap_exceeded`](/errors/test_call_cap_exceeded)); **mock test calls never draw the cap down**.
## What test mode does not skip
[Section titled “What test mode does not skip”](#what-test-mode-does-not-skip)
The compliance floor runs on every call through every entry point, test keys included: the AI-disclosure opener, internal-DNC suppression, and the platform kill switches. A configured live phone call durably records a contact’s opt-out before it closes. The synchronous mock test-call path does not enter the audio leg or exercise contact-bound tools, so test mode removes the carrier and that tool exercise, never the rules; a refusal like [`contact_not_dialable`](/errors/contact_not_dialable) raised by a floor control behaves identically under a test key. Usage metering also still applies where the docs say it does; mock telephony simply never generates billable telephony usage.
## Where test mode fits your pipeline
[Section titled “Where test mode fits your pipeline”](#where-test-mode-fits-your-pipeline)
Build the tenant-facing parts of your integration against a test key first: agent creation, contact import, scrub runs, campaign create and launch, and webhook consumption. Use a configured live phone call to exercise the supported contact-bound tools; calendar actions require an active direct Google Calendar connection. Agent publishing requires the same scripted golden-suite release check in test and live modes: request it for the exact draft, wait for the deployment’s release worker, then publish only after it passes. The current scripted stand-in is not a live-provider or carrier evaluation. The deterministic digit map gives you fixture-quality outcomes in CI without a single real call. When you move to a live key, the only things that change are the telephony plane (your verified Twilio subaccount) and the gates that production arms: the executed Production Services Order, payment, BYO carrier verification, and staff’s reviewed fresh-step-up/dual-control promotion. See the [Quickstart](/quickstart) for the five-step version of this loop.
# Webhooks
> One endpoint per tenant, call-lifecycle delivery, and HMAC-signed verification snippets in TypeScript and Python.
At v1 each tenant configures **exactly one** webhook endpoint: `PUT /v1/webhook-endpoint` with `url`, `enabled_events[]`, and an `api_version_pin` that freezes payload shapes so your receiver never has to redeploy in lockstep with the platform. Deliveries are inspectable at `GET /v1/webhook-deliveries?event_id=…`.
## Envelope and catalog
[Section titled “Envelope and catalog”](#envelope-and-catalog)
Every delivery is one event:
```json
{
"id": "evt_01JA4R…",
"type": "call.ended",
"created_at": "2026-08-03T18:04:11Z",
"api_version": "2026-07-01",
"tenant_id": "ten_01J9XV…",
"data": { "call_id": "call_01JA2…", "disposition": "completed_goal", "duration_ms": 143000 }
}
```
The schema catalog includes call lifecycle and analysis, campaign lifecycle, appointments, shift and survey results, recorded conversation actions, opt-outs, scrub/export completion, caller messages, tickets and lead capture. See [Receive results](/guides/receive-results) for routing support, workforce and qualification receipts into one company integration.
`appointment.booked` is emitted when a configured live phone call successfully creates an event through the tenant’s active direct Google Calendar connection; it means the event write succeeded, not that an attendee mailbox received the invite. `optout.recorded` is emitted when a durable suppression is recorded. Tool-result events require that tool to be available and successfully save its result; enabling an event does not grant a tool or make an action succeed. `usage.threshold` remains schema-reserved with no threshold-crossing emitter, so it does not currently send notifications. Some resources are polled rather than pushed: import terminal states, agent publish verdicts, invoice finalization, and endpoint auto-disable (delivered by email, since a dead endpoint cannot receive its own obituary).
## Verify the signature
[Section titled “Verify the signature”](#verify-the-signature)
Every delivery carries:
```http
Vocapable-Signature: t=,v1=
```
Verify against the **raw request bytes** before parsing anything, and reject timestamps skewed more than 5 minutes. `POST /v1/webhook-endpoint/rotate-secret` keeps the old and new secrets valid for 24 hours, so verify against every secret you currently hold, and rotation never drops a delivery.
verify.ts
```ts
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 5 * 60;
export function verifyVocapableSignature(
header: string,
rawBody: Uint8Array,
secrets: string[],
nowSeconds: number = Math.floor(Date.now() / 1000),
): boolean {
let timestamp: string | undefined;
const signatures: Buffer[] = [];
for (const part of header.split(",")) {
const separator = part.indexOf("=");
if (separator < 1) return false;
const key = part.slice(0, separator).trim();
const value = part.slice(separator + 1).trim();
if (key === "t") {
if (timestamp !== undefined || !/^(0|[1-9][0-9]*)$/.test(value)) return false;
timestamp = value;
} else if (key === "v1") {
if (!/^[0-9a-fA-F]{64}$/.test(value)) return false;
signatures.push(Buffer.from(value, "hex"));
}
}
const t = Number(timestamp);
if (!Number.isSafeInteger(t) || !Number.isFinite(nowSeconds) || signatures.length === 0) return false;
if (Math.abs(nowSeconds - t) > TOLERANCE_SECONDS) return false;
return secrets.some((secret) => {
const expected = createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest();
return signatures.some((received) => timingSafeEqual(expected, received));
});
}
```
verify.py
```python
import hashlib
import hmac
import math
import re
import time
TOLERANCE_SECONDS = 5 * 60
def verify_vocapable_signature(
header: str, raw_body: bytes, secrets: list[str], now_seconds: float | None = None
) -> bool:
timestamp = None
signatures = []
try:
for part in header.split(","):
key, value = (item.strip() for item in part.split("=", 1))
if key == "t":
if timestamp is not None or not re.fullmatch(r"0|[1-9][0-9]*", value):
return False
timestamp = int(value)
elif key == "v1":
if not re.fullmatch(r"[0-9a-fA-F]{64}", value):
return False
signatures.append(bytes.fromhex(value))
except ValueError:
return False
now = time.time() if now_seconds is None else now_seconds
if timestamp is None or timestamp > 2**53 - 1 or not signatures or not math.isfinite(now):
return False
if abs(now - timestamp) > TOLERANCE_SECONDS:
return False
payload = f"{timestamp}.".encode() + raw_body
return any(
hmac.compare_digest(hmac.new(secret.encode(), payload, hashlib.sha256).digest(), received)
for secret in secrets
for received in signatures
)
```
These examples retain every `v1` value so either signature can match during rotation. Pass the original bytes (`Buffer` in Node), before JSON parsing or reserialization. Answer `2xx` after verification and durable acceptance; reject an unverifiable delivery without processing it.
## Delivery semantics: at-least-once, unordered
[Section titled “Delivery semantics: at-least-once, unordered”](#delivery-semantics-at-least-once-unordered)
A delivery counts on any 2xx within 10 seconds; otherwise exponential backoff with jitter (1m, 5m, 30m, 2h, 6h, then 6-hourly) for up to 24 hours, after which the event is marked exhausted. Three consecutive exhausted events **auto-disable the endpoint**, and you are notified by email.
Delivery is **at-least-once** and **unordered** at v1, so there is no `sequence` field and no ordering guarantee. Two rules make a receiver correct:
1. **Dedupe on `event.id`.** Persist processed ids; a replay of an id you have seen is a no-op.
2. **Treat every handler as idempotent, and re-fetch the resource by id** when you need current state, because a `call.ended` may arrive before the `call.started` it follows.
Respond fast: accept, persist, return `2xx`, process async. Ten seconds includes your cold starts.
# Postman collection
> Download the generated Postman collection, wire up the two variables, and make your first request against the sandbox.
The collection is generated at build time from the same committed OpenAPI 3.1 snapshot as the [API reference](/api-reference), with requests foldered by resource tag.
[](/postman/vocapable.postman_collection.json)
[Download the collection](/postman/vocapable.postman_collection.json)
## Set two variables
[Section titled “Set two variables”](#set-two-variables)
The collection authenticates every request with collection-level bearer auth referencing `{{apiKey}}`, against `{{baseUrl}}`:
| Variable | Value |
| --------- | --------------------------------------- |
| `baseUrl` | `https://api.vocapable.com` (pre-set) |
| `apiKey` | Your key; start with a `vcp_test_…` key |
In Postman: import the file, open the collection’s **Variables** tab, paste your key into `apiKey`, and save. Use a **test key first**: `vcp_test_` keys are free, route all telephony to a simulated provider, and exercise the full dial pipeline without a single PSTN call; see [Test mode](/guides/test-mode).
## A sensible first run
[Section titled “A sensible first run”](#a-sensible-first-run)
1. `identity → GET /v1/me` confirms the key, tenant, mode, and scopes.
2. `agents → GET /v1/templates` browses the template library.
3. Follow the [Quickstart](/quickstart) order for the rest: create a no-tool draft, request the built-in scripted release check, wait for the deployed release worker to pass that exact draft, then publish it. Then verify a number and place a simulated test call. Simulated calls do not execute contact-bound tools. On configured live phone calls, use only `check_calendar`, `book_meeting`, `schedule_callback`, `record_optout`, and `end_call`; calendar actions also need an active direct Google Calendar connection.
Two conventions the collection cannot click for you: add an `Idempotency-Key` header (any UUID) on `POST /v1/campaigns/{id}/launch`, where it is required, and honor `Retry-After` on `429`. Details in [Conventions](/guides/conventions).
Generated from spec version 0.1.0, sha256 239a31550856f2b3d4ac474fc0f589f6b98622c1f5596694393f4e0734901826 (portal/openapi.json). The collection’s description carries the same stamp, so a downloaded copy is always traceable to the exact spec bytes it was built from.
# Quickstart
> From API key to a draft agent, a scripted release check, a simulated test call, and back to a rotated key.
Everything below uses a **`vcp_test_` key**. When you have published a version, its test calls route to a mock provider: no PSTN dial ever originates, outcomes are simulated deterministically, and nothing is billed. Your sandbox tenant’s first key is granted the full scope set at onboarding; use it as the bearer token here.
You get that first key from the browser, once, at the end of signup - the onboarding routes are session-authenticated and not key-reachable ([Auth & scopes](/guides/auth-and-scopes)). Everything after it is the API.
The base URL is `https://api.vocapable.com`. Every request authenticates with `Authorization: Bearer vcp_test_…`.
## 1. Create an API key
[Section titled “1. Create an API key”](#1-create-an-api-key)
Keys are shown **exactly once** at creation, because only a SHA-256 hash and the first 8 characters are retained. `scopes[]` is required, non-empty, and validated against the catalog - a value that is not a scope is refused with `422` [`unknown_scope`](/errors/unknown_scope) rather than stored, so a typo fails here instead of surfacing later as a baffling `403`. Minting requires `keys:manage`, and a key may only grant scopes it holds itself (see [Auth & scopes](/guides/auth-and-scopes)).
POST /v1/api-keys
```bash
curl -X POST https://api.vocapable.com/v1/api-keys \
-H "Authorization: Bearer vcp_test_YOUR_ONBOARDING_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "test",
"name": "quickstart",
"scopes": [
"agents:read", "agents:write",
"calls:read", "calls:write",
"telephony:manage",
"keys:manage"
]
}'
```
`keys:manage` is in the list on purpose: without it, the key you just minted cannot rotate itself, and rotation is the only way to replace its secret without a gap.
`name` and the optional `expires_at` are worth setting on anything you hand to another system - the name is what you will use to decide which credential to retire, and expiry is enforced at authentication.
## 2. Confirm who you are
[Section titled “2. Confirm who you are”](#2-confirm-who-you-are)
`GET /v1/me` returns the authenticated principal: tenant, key id, mode, and scopes. It declares no scope of its own: authentication alone is its check.
GET /v1/me
```bash
curl https://api.vocapable.com/v1/me \
-H "Authorization: Bearer vcp_test_..."
```
## 3. Create a no-tool draft
[Section titled “3. Create a no-tool draft”](#3-create-a-no-tool-draft)
Create an ordinary **draft** agent directly. `name`, `use_case`, and `disclosure_config` are required; the AI-disclosure opener is mandatory and non-removable. Keep `tools` empty for this browser/simulated-test-first workflow: those transports have no contact and never invoke contact-bound actions. On a configured live phone deployment, the supported subset is `check_calendar`, `book_meeting`, `schedule_callback`, `record_optout`, and `end_call`; calendar actions additionally require the tenant’s active direct Google Calendar connection.
Do not use `POST /v1/templates/{template_id}/instantiate` unless every `tool_grants[]` entry is in that supported live-phone subset. Instantiation copies grants into the new agent unchanged; browser and simulated test calls still do not exercise them.
POST /v1/agents
```bash
curl -X POST https://api.vocapable.com/v1/agents \
-H "Authorization: Bearer vcp_test_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Qualification draft, pilot",
"use_case": "sdr",
"tools": [],
"disclosure_config": {
"en-US": "Hi, this is Ava, an automated assistant calling for Acme..."
}
}'
```
## Publishing uses a queued scripted release check
[Section titled “Publishing uses a queued scripted release check”](#publishing-uses-a-queued-scripted-release-check)
Drafts are never dialed. `POST /v1/agents/{agent_id}/publish` snapshots a definition into an immutable `AgentVersion`, but it is blocked until the golden-transcript replay suite has produced a passing `EvalRun` for this exact draft revision. Request that check with `POST /v1/agents/{agent_id}/release-gate-evaluations`, wait for the deployment’s release worker to complete it, then call publish. A changed draft needs a new check; an old pass cannot authorize it.
The current worker runs built-in golden conversations with sandboxed tools in `scripted_stand_in` mode. It is not a live model/provider/carrier evaluation. If the release worker is not deployed, the request truthfully remains `queued` and publish continues to refuse with `409` [`eval_gate_failed`](/errors/eval_gate_failed).
POST /v1/agents/{agent\_id}/release-gate-evaluations
```bash
curl -X POST https://api.vocapable.com/v1/agents/agnt_01.../release-gate-evaluations \
-H "Authorization: Bearer vcp_test_..."
```
## After you publish a version
[Section titled “After you publish a version”](#after-you-publish-a-version)
### Register and verify your own number
[Section titled “Register and verify your own number”](#register-and-verify-your-own-number)
A test call may only dial a number your tenant has **proven it controls**. Register it, receive a one-time code (SMS to mobiles, a short voice readout to landlines), and confirm it.
That message verifies control of the destination; it is not campaign SMS and does not enable an agent’s `send_sms` tool.
POST /v1/verified-numbers → check
```bash
curl -X POST https://api.vocapable.com/v1/verified-numbers \
-H "Authorization: Bearer vcp_test_..." \
-H "Content-Type: application/json" \
-d '{"phone_e164": "+14155552671", "label": "Founder cell"}'
curl -X POST https://api.vocapable.com/v1/verified-numbers/vn_01.../check \
-H "Authorization: Bearer vcp_test_..." \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'
```
Only a currently `verified` number counts, because `pending` is a registration, not proof. An unverified destination refuses with [`test_call_destination_not_verified`](/errors/test_call_destination_not_verified).
### Place a simulated test call
[Section titled “Place a simulated test call”](#place-a-simulated-test-call)
POST /v1/agents/{agent\_id}/test-call
```bash
curl -X POST https://api.vocapable.com/v1/agents/agnt_01.../test-call \
-H "Authorization: Bearer vcp_test_..." \
-H "Content-Type: application/json" \
-d '{
"to": "+14155552671",
"timezone": "America/Los_Angeles",
"jurisdictions": ["US"]
}'
```
The response is `202` with a campaign-less contact attempt (`att_…`). Under a `vcp_test_` key the dial runs synchronously against mock telephony through the **full dial pipeline** of gate, materialization, seize, originate, AMD, and status outcome, and the response’s `state` already carries the outcome the destination’s **last digit** deterministically selects:
| Last digit | Simulated outcome |
| ---------- | ------------------------------------------ |
| `0`–`5` | A human answers |
| `6`–`7` | An answering machine (AMD `machine_start`) |
| `8` | Nobody answers |
| `9` | The carrier rejects the call |
It exercises the dial pipeline, not the audio leg, and mock test calls never draw down the daily live test-call cap.
## 4. Rotate the key you just made
[Section titled “4. Rotate the key you just made”](#4-rotate-the-key-you-just-made)
Not a ceremony - the point of doing it now is that you find out your integration survives a secret change before it is carrying traffic.
POST /v1/api-keys/{key\_id}/rotate
```bash
curl -X POST https://api.vocapable.com/v1/api-keys/key_01J9.../rotate \
-H "Authorization: Bearer vcp_test_..." \
-H "Content-Type: application/json" \
-d '{"name": "quickstart (rotated)"}'
```
The response carries the new `secret` and the `replaced_api_key_id` of the one it retired, in a single transaction: there is no moment where both work and no moment where neither does. The replacement keeps the retired key’s mode and scopes, so a narrow key can rotate itself.
From here: read [Test mode](/guides/test-mode) for the full mock semantics, then [Campaigns & scrub](/guides/campaigns-and-scrub) for the production loop, or [Headless mode](/guides/headless-mode) if your own stack is the system of record.
# API contract
Generated from the same OpenAPI document the reference pages render (https://docs.vocapable.com/openapi.json).
Base URL: https://api.vocapable.com - bearer auth with a vcp_live_/vcp_test_ API key.
Schema $refs resolve against the Schemas section at the end of this file.
## GET /healthz
Healthz
Response 200: {"additionalProperties":{"type":"string"},"type":"object","title":"Response Healthz Healthz Get"}
## GET /readyz
Readyz
Ready means the database answers *and* both isolation checks still hold.
The admin verification runs only when the admin plane is wired: an unwired
plane is a stated condition, not unreadiness (ADR-0016 §8).
Response 200: {"additionalProperties":{"type":"string"},"type":"object","title":"Response Readyz Readyz Get"}
## GET /v1/account-deletion
Read Account Deletion
Show only this account's current impact and protected request status.
Response 200: {"$ref":"#/components/schemas/AccountDeletionOverview"}
## POST /v1/account-deletion/requests
Create Account Deletion Request
Confirm a password-account request with a fresh password and explicit phrase.
Request schema: {"$ref":"#/components/schemas/DeleteAccountRequest"}
Response 200: {"$ref":"#/components/schemas/AccountDeletionOverview"}
## POST /v1/account-deletion/email-confirmations
Start Account Deletion Email Confirmation
Send fresh proof to a passwordless account's already verified mailbox.
Response 200: {"$ref":"#/components/schemas/EmailConfirmationAccepted"}
## POST /v1/account-deletion/email-confirmations/confirm
Confirm Account Deletion Email
Spend a one-time email proof and create/reconfirm its account's request.
This route intentionally does not take a cookie: a person may open the link on a
different device. The high-entropy, one-use token is the credential; trusted-origin
validation ensures only the Vocapable portal can submit it from the fragment ceremony.
Request schema: {"$ref":"#/components/schemas/ActionTokenRequest"}
Response 200: {"$ref":"#/components/schemas/AccountDeletionOverview"}
## GET /v1/me
Describe the authenticated principal
Response 200: {"$ref":"#/components/schemas/CurrentPrincipalResource"}
## GET /v1/users
The tenant's user directory (read-only)
Who work can be attributed or assigned to - never a management surface.
Invitations, role changes, and deactivation have no `/v1` route on purpose; this
exists so the portal can render an assignee picker and resolve "assign to me"
(ADR-0037) without holding anything wider than a read.
Parameters: limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_TenantUserSummary_"}
## GET /v1/api-keys
List this tenant's API keys
Cursor-paginated like every other list endpoint (docs/04-api.md §1).
It used to answer a bare JSON array - the one exception on the surface - which broke
a generated client's generic list walker on the first resource most developers touch.
Parameters: limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_ApiKeySummary_"}
## POST /v1/api-keys
Create an API key (the secret is returned exactly once)
Request schema: {"$ref":"#/components/schemas/ApiKeyCreateRequest"}
Response 201: {"$ref":"#/components/schemas/ApiKeyCreateResponse"}
## POST /v1/api-keys/{key_id}/rotate
Replace a key with a new secret carrying the same authority
Retire one key and mint its replacement, in one transaction.
Rotation exists because the two-call alternative - create, then delete - has no
safe order. Create-then-delete leaves both live if the second call is lost;
delete-then-create leaves the integration with no credential at all. Here the
revoke is the claim (`revoke_returning`), so a lost response leaves exactly one
live key and a replaying client gets `404 api_key_not_found` rather than a second
replacement.
**The replacement carries the retired key's mode and scopes, not the caller's.**
That is what lets a narrow contractor key rotate itself without the caller having to
hold what it holds - and it is why the request body has no `scopes` field: there is
no widening path here to guard, only one that does not exist.
Parameters: key_id (path, required)
Request schema: {"$ref":"#/components/schemas/ApiKeyRotateRequest"}
Response 201: {"$ref":"#/components/schemas/ApiKeyRotateResponse"}
## DELETE /v1/api-keys/{key_id}
Revoke an API key
Parameters: key_id (path, required)
Response 204: empty
## GET /v1/mobile/authorize
Approve a mobile PKCE authorization request in the first-party browser
Issue a one-use code or show a workspace chooser for the signed-in account.
Browser login navigation belongs to the first-party portal bridge. This code-issuing
endpoint stays native-cookie-only: an absent, stale, or forged browser session receives
the normal opaque 401 from :class:`NativeVerifiedDashboard`, never a portal redirect or
a weaker bearer-token fallback.
Parameters: membership_id (query), client_id (query), response_type (query), redirect_uri (query, required), code_challenge (query, required), code_challenge_method (query), scope (query), installation_id (query, required), platform (query, required), device_label (query), state (query, required)
Response 200: {}
## POST /v1/mobile/token
Exchange a mobile PKCE code or rotate a refresh credential
Request schema: {"$ref":"#/components/schemas/MobileTokenRequest"}
Response 200: {"$ref":"#/components/schemas/MobileTokenResponse"}
## GET /v1/mobile/me
Bootstrap the authenticated mobile workspace
Response 200: {"$ref":"#/components/schemas/MobileBootstrapProfile"}
## GET /v1/mobile/devices
List this membership's registered mobile devices
Response 200: {"items":{"$ref":"#/components/schemas/MobileDeviceSummary"},"type":"array","title":"Response List Mobile Devices V1 Mobile Devices Get"}
## PUT /v1/mobile/devices/current/push
Register, rotate, or clear this device's Expo push endpoint
Request schema: {"$ref":"#/components/schemas/MobileDevicePushRegistrationRequest"}
Response 200: {"$ref":"#/components/schemas/MobileDeviceSummary"}
## GET /v1/mobile/devices/current/notification-preferences
Read this device's routine notification and urgent-alert preferences
Response 200: {"$ref":"#/components/schemas/MobileNotificationPreferences"}
## PUT /v1/mobile/devices/current/notification-preferences
Set this device's routine notification and urgent-alert preferences
Request schema: {"$ref":"#/components/schemas/MobileNotificationPreferencesUpdateRequest"}
Response 200: {"$ref":"#/components/schemas/MobileNotificationPreferences"}
## POST /v1/mobile/devices/current/notification-preferences/test
Queue one generic notification test for this device only
Queue an ID-only generic test without copying a push route into the response.
Response 200: {"$ref":"#/components/schemas/MobileNotificationTestReceipt"}
## DELETE /v1/mobile/devices/{device_id}
Revoke one of this membership's mobile devices and every session on it
Parameters: device_id (path, required)
Response 204: empty
## POST /v1/mobile/logout
Revoke the current mobile refresh session
Response 204: empty
## GET /v1/mobile/team
List mobile customer-team memberships
Response 200: {"items":{"$ref":"#/components/schemas/MobileTeamMemberSummary"},"type":"array","title":"Response List Mobile Team V1 Mobile Team Get"}
## GET /v1/mobile/team/invitations
List team invitation metadata without invitation credentials
Response 200: {"items":{"$ref":"#/components/schemas/TenantInvitationSummary"},"type":"array","title":"Response List Mobile Team Invitations V1 Mobile Team Invitations Get"}
## POST /v1/mobile/team/invitations
Invite an admin, manager, or representative to this workspace
Request schema: {"$ref":"#/components/schemas/TenantInvitationCreateRequest"}
Response 201: {"$ref":"#/components/schemas/TenantInvitationSummary"}
## DELETE /v1/mobile/team/invitations/{invitation_id}
Revoke a pending team invitation
Parameters: invitation_id (path, required)
Response 204: empty
## GET /v1/mobile/team/{membership_id}/transfer-destination-verification
Read non-PII transfer-destination verification progress for a team member
Parameters: membership_id (path, required)
Response 200: {"anyOf":[{"$ref":"#/components/schemas/MobileTransferDestinationVerificationSummary"},{"type":"null"}],"title":"Response Get Transfer Destination Verification V1 Mobile Team Membership Id Transfer Destination Verification Get"}
## POST /v1/mobile/team/{membership_id}/transfer-destination-verification
Deliver an OTP before changing a linked representative's transfer destination
Parameters: membership_id (path, required)
Request schema: {"$ref":"#/components/schemas/MobileTransferDestinationVerificationStartRequest"}
Response 201: {"$ref":"#/components/schemas/MobileTransferDestinationVerificationSummary"}
## POST /v1/mobile/team/{membership_id}/transfer-destination-verification/check
Confirm a delivered OTP and atomically change the linked transfer destination
Parameters: membership_id (path, required)
Request schema: {"$ref":"#/components/schemas/MobileTransferDestinationVerificationCheckRequest"}
Response 200: {"$ref":"#/components/schemas/MobileTransferDestinationVerificationSummary"}
## PATCH /v1/mobile/team/{membership_id}
Change a team member's role, status, or explicit human-rep link
Parameters: membership_id (path, required)
Request schema: {"$ref":"#/components/schemas/TenantMembershipUpdateRequest"}
Response 200: {"$ref":"#/components/schemas/TenantMembershipSummary"}
## POST /v1/mobile/invitations/accept
Accept a delivered team invitation in a signed-in first-party browser
Spend a fragment-delivered invite only for the matching verified account email.
Request schema: {"$ref":"#/components/schemas/TenantInvitationAcceptanceRequest"}
Response 200: {"$ref":"#/components/schemas/TenantMembershipSummary"}
## GET /v1/mobile/integrations/readiness
Read mobile-safe integration readiness
Return tenant-scoped status to Admins and Managers only.
A Rep has no integration-management responsibility and receives no integration state.
The response is deliberately no-store because a reconnection can change booking readiness
outside the app while it is backgrounded.
Response 200: {"$ref":"#/components/schemas/MobileIntegrationReadiness"}
## GET /v1/mobile/integrations/health
Read privacy-minimized mobile integration health
Return the finite, repair-oriented registry for Admins and Managers.
Calendar connection/booking reflect only the deployed Google Calendar adapter. CRM is
represented explicitly as not shipped rather than guessing from generic webhook rows.
Webhook health observes a bounded recent aggregate and never exposes endpoint/delivery
identities, URLs, secrets, recipients, raw provider errors, or response data.
Response 200: {"$ref":"#/components/schemas/MobileIntegrationHealthRegistry"}
## GET /v1/mobile/integrations/timeline
Read current privacy-minimized integration incidents
Return current evidence-backed repair work for Admins and Managers.
This is intentionally not an audit timeline or integration browser. It offers only a
finite affected capability, a durable last-known health timestamp, and a named fixed
repair surface. No row/resource identifiers, endpoint URLs, credentials, provider
diagnostics, delivery payloads, recipient data, or customer content crosses this API.
Response 200: {"$ref":"#/components/schemas/MobileIntegrationHealthTimeline"}
## GET /v1/mobile/receptionist/insights
Read bounded comparative receptionist insights
Return leader-only aggregates for the current and immediately-prior equal window.
Parameters: days (query)
Response 200: {"$ref":"#/components/schemas/MobileReceptionistInsights"}
## GET /v1/mobile/receptionist/live-calls
List live receptionist calls
Parameters: limit (query)
Response 200: {"type":"array","items":{"$ref":"#/components/schemas/MobileLiveCall"},"title":"Response List Live Calls V1 Mobile Receptionist Live Calls Get"}
## GET /v1/mobile/receptionist/activity
Recent receptionist activity
Parameters: limit (query)
Response 200: {"type":"array","items":{"$ref":"#/components/schemas/MobileReceptionistActivity"},"title":"Response List Receptionist Activity V1 Mobile Receptionist Activity Get"}
## GET /v1/mobile/receptionist/activity/page
Page receptionist activity with a stable cursor
Page the merged activity timeline without offset drift under live call updates.
Parameters: limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_MobileReceptionistActivity_"}
## GET /v1/mobile/receptionist/metrics
Read bounded aggregate receptionist metrics
Return compact operational counts without exposing the calls behind the totals.
Parameters: start (query), end (query)
Response 200: {"$ref":"#/components/schemas/MobileReceptionistMetrics"}
## GET /v1/mobile/receptionist/activity/calls/{call_id}/transcript
Read an authorized receptionist call transcript
Serve the durable transcript read model for Admin/Manager activity review.
A mobile bearer has no access to the generic API-key calls router. This adapter keeps
that boundary while preserving the exact tenant and recording-consent behavior of the
existing transcript reader.
Parameters: call_id (path, required)
Response 200: {"$ref":"#/components/schemas/MobileCallTranscript"}
## POST /v1/mobile/receptionist/activity/calls/{call_id}/ai-output-reports
Report AI-generated receptionist call output for review
Create one durable, enum-only content report for an authorized call.
This is a safety intake, not a live-call control or a transcript-copy endpoint. The
report is tied to a tenant-scoped call proven by the existing repository; its body has
no free-text field, and the audit record contains only opaque IDs and the enum reason.
A repeated tap returns the original receipt without a second queue or audit entry.
Parameters: call_id (path, required)
Request schema: {"$ref":"#/components/schemas/MobileAiContentReportRequest"}
Response 201: {"$ref":"#/components/schemas/MobileAiContentReportReceipt"}
## GET /v1/mobile/receptionist/activity/calls/{call_id}/recording
Redirect to an authorized short-lived receptionist recording link
Issue the same short-lived playback link as the main call review surface.
The response is deliberately a redirect, never audio bytes or a storage locator. The
registered recording adapter rechecks retention and tenant ownership before it signs a
target, so native activity cannot turn a durable recording id into a reusable URL.
Parameters: call_id (path, required)
## GET /v1/mobile/receptionist/activity/voicemails/{voicemail_id}
Read an authorized inbound voicemail receipt
Return receipt state and timing; use the call recording route for any audio.
Parameters: voicemail_id (path, required)
Response 200: {"$ref":"#/components/schemas/MobileInboundVoicemail"}
## GET /v1/mobile/receptionist/activity/appointments/{appointment_id}
Read an authorized appointment detail
Read by opaque activity id, with the repository's tenant predicate in every query.
Parameters: appointment_id (path, required)
Response 200: {"$ref":"#/components/schemas/MobileAppointment"}
## GET /v1/mobile/receptionist/transfers
Recent transfer history
Parameters: limit (query)
Response 200: {"type":"array","items":{"$ref":"#/components/schemas/MobileTransferHistoryEntry"},"title":"Response List Transfer History V1 Mobile Receptionist Transfers Get"}
## GET /v1/mobile/receptionist/transfers/page
Page tenant transfer history with a stable cursor
Page all tenant handoffs; destination numbers remain absent from every page.
Parameters: limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_MobileTransferHistoryEntry_"}
## GET /v1/mobile/receptionist/transfers/{handoff_id}
Read one transfer history entry
Parameters: handoff_id (path, required)
Response 200: {"$ref":"#/components/schemas/MobileTransferHistoryEntry"}
## GET /v1/mobile/receptionist/my-transfers
List the authenticated rep's own transfer history
Parameters: limit (query)
Response 200: {"type":"array","items":{"$ref":"#/components/schemas/MobileTransferHistoryEntry"},"title":"Response List My Transfer History V1 Mobile Receptionist My Transfers Get"}
## GET /v1/mobile/receptionist/my-transfers/page
Page the authenticated rep's transfer history
Keep historical cursor traversal bound to the server-linked rep identity.
Parameters: limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_MobileTransferHistoryEntry_"}
## GET /v1/mobile/receptionist/my-transfers/{handoff_id}
Read one transfer only when it belongs to the authenticated rep
Parameters: handoff_id (path, required)
Response 200: {"$ref":"#/components/schemas/MobileTransferHistoryEntry"}
## GET /v1/mobile/receptionist/handoffs/{handoff_id}/live
Read no-cache carrier-confirmed handoff lifecycle
Poll a safe lifecycle projection; carrier callbacks and workers remain the only writers.
Admins and Managers may review any inbound receptionist handoff in their tenant. A Rep
receives the same shape only when their server-linked human-rep id is a durable target
of the request. The 404 remains identical for absent and unauthorized rep rows.
Parameters: handoff_id (path, required)
Response 200: {"$ref":"#/components/schemas/MobileLiveHandoff"}
## GET /v1/mobile/receptionist/handoff-capability
Read whether cellular handoff controls are currently available
Response 200: {"$ref":"#/components/schemas/MobileHandoffCapability"}
## GET /v1/mobile/receptionist/call-ai/contacts
List existing contacts selectable for one mobile AI call
Return live contact labels only; launch-time compliance remains server-side.
Parameters: limit (query)
Response 200: {"type":"array","items":{"$ref":"#/components/schemas/MobileCallAiContact"},"title":"Response List Mobile Call Ai Contacts V1 Mobile Receptionist Call Ai Contacts Get"}
## GET /v1/mobile/receptionist/call-ai/contacts/search
Search existing safe-label contacts for one mobile AI call
Return one virtualized picker page without searching raw contact destinations.
The response is a narrow selection aid rather than a dialability assertion. The
subsequent Call AI command still proves contact ownership and executes all consent,
DNC, jurisdiction, caller-ID, agreement, spend, and idempotency gates server-side.
Parameters: q (query, required), cursor (query), limit (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_MobileCallAiContact_"}
## GET /v1/mobile/receptionist/call-ai/agents
List active published agents selectable for one mobile AI call
Parameters: limit (query)
Response 200: {"type":"array","items":{"$ref":"#/components/schemas/MobileCallAiAgent"},"title":"Response List Mobile Call Ai Agents V1 Mobile Receptionist Call Ai Agents Get"}
## POST /v1/mobile/receptionist/call-ai
Queue one compliant live/BYO mobile AI call
Launch exactly one resource-bound call through the shared production pipeline.
The native admission dependency enforces rate limits, current production agreements,
and a membership/device idempotency key before this handler runs. ``mock=False`` is
intentional: mobile Call AI has no test or managed-carrier branch, and the shared core
refuses anything except the tenant's verified default BYO route.
Parameters: Idempotency-Key (header, required)
Request schema: {"$ref":"#/components/schemas/MobileCallAiRequest"}
Response 202: {"$ref":"#/components/schemas/MobileCallAiResponse"}
## GET /v1/mobile/receptionist/numbers
List tenant-owned receptionist numbers for mobile routing controls
List only policy-routed business voice lines, never caller or contact numbers.
Parameters: limit (query)
Response 200: {"type":"array","items":{"$ref":"#/components/schemas/MobileReceptionistNumber"},"title":"Response List Receptionist Numbers V1 Mobile Receptionist Numbers Get"}
## GET /v1/mobile/receptionist/numbers/{number_id}/schedule
Read the mobile-editable answering-policy schedule
Parameters: number_id (path, required)
Response 200: {"$ref":"#/components/schemas/AnsweringPolicySchedule"}
## PATCH /v1/mobile/receptionist/numbers/{number_id}/schedule
Version-check and update answering-policy business hours or holidays
Parameters: number_id (path, required)
Request schema: {"$ref":"#/components/schemas/AnsweringPolicySchedulePatchRequest"}
Response 200: {"$ref":"#/components/schemas/AnsweringPolicySchedule"}
## GET /v1/mobile/receptionist/numbers/{number_id}/emergency-override
Read the current emergency route override
Parameters: number_id (path, required)
Response 200: {"anyOf":[{"$ref":"#/components/schemas/EmergencyOverride"},{"type":"null"}],"title":"Response Get Emergency Override V1 Mobile Receptionist Numbers Number Id Emergency Override Get"}
## PUT /v1/mobile/receptionist/numbers/{number_id}/emergency-override
Set a mandatory-expiry emergency route override
Parameters: number_id (path, required)
Request schema: {"$ref":"#/components/schemas/EmergencyOverrideRequest"}
Response 200: {"$ref":"#/components/schemas/EmergencyOverride"}
## DELETE /v1/mobile/receptionist/numbers/{number_id}/emergency-override
Clear an emergency route override and restore the base policy
Parameters: number_id (path, required)
Request schema: {"$ref":"#/components/schemas/EmergencyOverrideClearRequest"}
Response 204: empty
## GET /v1/mobile/operations/tasks
List Operations Inbox tasks
Parameters: limit (query), cursor (query), state (query), severity (query)
Response 200: {"$ref":"#/components/schemas/MobileOperationTaskPage"}
## GET /v1/mobile/operations/tasks/{task_id}
Read one Operations Inbox task
Parameters: task_id (path, required)
Response 200: {"$ref":"#/components/schemas/MobileOperationTask"}
## POST /v1/mobile/operations/tasks/{task_id}/acknowledge
Acknowledge an Operations Inbox task
Parameters: task_id (path, required), Idempotency-Key (header)
Request schema: {"$ref":"#/components/schemas/MobileOperationTaskAcknowledgeRequest"}
Response 200: {"$ref":"#/components/schemas/MobileOperationTask"}
## PUT /v1/mobile/operations/tasks/{task_id}/assignee
Assign or clear an Operations Inbox task owner
Parameters: task_id (path, required), Idempotency-Key (header)
Request schema: {"$ref":"#/components/schemas/MobileOperationTaskAssignRequest"}
Response 200: {"$ref":"#/components/schemas/MobileOperationTask"}
## POST /v1/mobile/operations/tasks/{task_id}/resolve
Resolve an Operations Inbox task without changing its source system
Parameters: task_id (path, required), Idempotency-Key (header)
Request schema: {"$ref":"#/components/schemas/MobileOperationTaskResolveRequest"}
Response 200: {"$ref":"#/components/schemas/MobileOperationTask"}
## GET /v1/mobile/operations/resolve-target/{event_type}/{resource_id}
Resolve a legacy ID-only notification into a safe mobile destination
Parameters: event_type (path, required), resource_id (path, required)
Response 200: {"$ref":"#/components/schemas/MobileOperationTarget"}
## GET /v1/mobile/receptionist/readiness
Read role-aware mobile field-operations readiness
Return current checks and their owned repair surfaces, never hidden diagnostics.
Response 200: {"$ref":"#/components/schemas/MobileReadiness"}
## POST /v1/mobile/step-up/requests
Begin Mobile Step Up
Start one browser confirmation for one canonical mobile mutation payload.
Request schema: {"$ref":"#/components/schemas/MobileStepUpBeginRequest"}
Response 201: {"$ref":"#/components/schemas/MobileStepUpBeginResponse"}
## GET /v1/mobile/step-up/requests/{request_id}
Get Mobile Step Up Request
Parameters: request_id (path, required)
Response 200: {"$ref":"#/components/schemas/MobileStepUpBrowserRequest"}
## POST /v1/mobile/step-up/requests/{request_id}/password
Complete Mobile Step Up Password
Verify the current password now; a previous sign-in does not count as freshness.
Parameters: request_id (path, required)
Request schema: {"$ref":"#/components/schemas/MobileStepUpPasswordRequest"}
Response 200: {"$ref":"#/components/schemas/MobileStepUpCompletion"}
## POST /v1/mobile/step-up/requests/{request_id}/email
Start Mobile Step Up Email
Send one fresh verified-email proof for a passwordless account's request.
Parameters: request_id (path, required)
Response 200: {"$ref":"#/components/schemas/MobileStepUpEmailStarted"}
## POST /v1/mobile/step-up/requests/{request_id}/email/confirm
Complete Mobile Step Up Email
Spend the fragment-held email credential only for the same signed-in account.
Parameters: request_id (path, required)
Request schema: {"$ref":"#/components/schemas/MobileStepUpEmailConfirmationRequest"}
Response 200: {"$ref":"#/components/schemas/MobileStepUpCompletion"}
## GET /v1/mobile/receptionist/hub
Read privacy-safe live receptionist operations by business number
Return operational facts, not a second agent/policy/calling-control surface.
Response 200: {"$ref":"#/components/schemas/MobileReceptionistHub"}
## GET /v1/mobile/receptionist/studio
List active receptionist lines and their bounded mobile Studio state
Response 200: {"items":{"$ref":"#/components/schemas/MobileReceptionistStudioSummary"},"type":"array","title":"Response List Receptionist Studio V1 Mobile Receptionist Studio Get"}
## GET /v1/mobile/receptionist/numbers/{number_id}/studio
Read one mobile receptionist Studio state
Parameters: number_id (path, required)
Response 200: {"$ref":"#/components/schemas/MobileReceptionistStudioSummary"}
## PATCH /v1/mobile/receptionist/numbers/{number_id}/studio
Version-check and update bounded receptionist business facts
Parameters: number_id (path, required)
Request schema: {"$ref":"#/components/schemas/MobileReceptionistStudioPatch"}
Response 200: {"$ref":"#/components/schemas/MobileReceptionistStudioSummary"}
## POST /v1/mobile/receptionist/numbers/{number_id}/studio/intelligence-snapshots
Copy a bounded Mobile Studio revision into canonical intelligence review
Write source evidence only; no policy, route, draft, or published version changes.
The endpoint gives a mobile operator a safe bridge into the same review queue the web
portal uses. It intentionally cannot promote the selected facts, change the phone
line's schedule/routing, or bypass source approval and release gates.
Parameters: number_id (path, required)
Request schema: {"$ref":"#/components/schemas/MobileStudioIntelligenceSnapshotRequest"}
Response 201: {"$ref":"#/components/schemas/MobileStudioIntelligenceSnapshotResult"}
## POST /v1/mobile/receptionist/numbers/{number_id}/test-receptionist
Authorize the system-dialer test target for a published receptionist
Return an owned business number for native ``tel:`` handoff, never place a call.
The client must display carrier-charge disclosure and invoke the operating system
dialer. This API has no microphone, call-log, contact, or carrier-control capability.
Parameters: number_id (path, required)
Response 200: {"$ref":"#/components/schemas/MobileReceptionistTestTarget"}
## GET /v1/mobile/workspace/overview
Read the native Owner Command Center workspace overview
Response 200: {"$ref":"#/components/schemas/MobileWorkspaceOverview"}
## GET /v1/mobile/workspace/setup
Read server-derived mobile Get Live work
Response 200: {"$ref":"#/components/schemas/MobileWorkspaceSetup"}
## GET /v1/mobile/workspaces
List active workspaces eligible for native browser reauthorization
Return safe labels only; a client must still run the browser PKCE exchange to switch.
Response 200: {"$ref":"#/components/schemas/MobileWorkspaceList"}
## GET /v1/mobile/billing/summary
Read a safe subscription and billing-health summary
Response 200: {"$ref":"#/components/schemas/MobileSubscriptionHealth"}
## PUT /v1/mobile/billing/budget
Set the mobile monthly/daily spend guardrail after fresh browser confirmation
CAS one displayed cap, preserving the wallet's non-mobile threshold and period.
A mobile retry with a lost response cannot replay the state change: the one-use grant
is consumed and the revision has advanced. The client receives a conflict on retry,
reloads the live value, and asks for a fresh browser proof rather than guessing.
Request schema: {"$ref":"#/components/schemas/MobileBillingBudgetUpdateRequest"}
Response 200: {"$ref":"#/components/schemas/MobileBillingBudget"}
## POST /v1/mobile/billing/portal
Mint one Stripe-hosted billing-management link after fresh browser confirmation
Open only the generic hosted billing home; no native payment or plan mutation exists.
Request schema: {"$ref":"#/components/schemas/MobileBillingPortalRequest"}
Response 200: {"$ref":"#/components/schemas/MobileBillingPortalSession"}
## GET /v1/mobile/change-feed
Poll opaque Owner Command Center change hints while the app is foregrounded
Return safe change hints after a tenant-local high-watermark.
The first request returns a small recent slice to invalidate freshly loaded screens;
it does not replay audit history. Each subsequent call must send ``next_cursor`` and
receives only entries committed after that immutable version. Cursor validation is
performed against the current tenant's allowed source rows, not merely by decoding it.
Parameters: limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/MobileChangeFeedPage"}
## GET /v1/mobile/coach/preferences
Read the current device's finite Mobile Coach preferences
Response 200: {"$ref":"#/components/schemas/MobileCoachPreferences"}
## PUT /v1/mobile/coach/recommendations/{key}
Dismiss, snooze, or restore one fixed Coach recommendation on this device
Parameters: key (path, required)
Request schema: {"$ref":"#/components/schemas/MobileCoachRecommendationPreferenceChangeRequest"}
Response 200: {"$ref":"#/components/schemas/MobileCoachRecommendationPreference"}
## PUT /v1/mobile/coach/telemetry-consent
Set this device's optional anonymous Coach telemetry election
Request schema: {"$ref":"#/components/schemas/MobileCoachTelemetryConsentUpdateRequest"}
Response 200: {"$ref":"#/components/schemas/MobileCoachPreferences"}
## POST /v1/mobile/coach/telemetry
Record one opted-in anonymous aggregate Coach telemetry bucket
Increment no bucket unless this current device explicitly opted in.
The repository function receives the five finite aggregate dimensions only. In
particular, it is never passed the tenant, membership, device, recommendation key,
request id, customer/call resource, or a raw duration.
Request schema: {"$ref":"#/components/schemas/MobileCoachTelemetryEventRequest"}
Response 200: {"$ref":"#/components/schemas/MobileCoachTelemetryReceipt"}
## GET /v1/mobile/availability/me
Read the caller's finite availability declaration
Response 200: {"$ref":"#/components/schemas/MobileAvailabilityWindow"}
## PUT /v1/mobile/availability/me
Declare the caller available until a selected expiry
Request schema: {"$ref":"#/components/schemas/MobileAvailabilitySetRequest"}
Response 200: {"$ref":"#/components/schemas/MobileAvailabilityWindow"}
## DELETE /v1/mobile/availability/me
End the caller's availability early
Response 204: empty
## GET /v1/mobile/availability
List current team availability without exposing transfer phone numbers
Response 200: {"items":{"$ref":"#/components/schemas/MobileAvailabilityWindow"},"type":"array","title":"Response List Team Availability V1 Mobile Availability Get"}
## POST /v1/mobile/receptionist/handoffs/{handoff_id}/booking/availability
Issue short-lived calendar choices for a carrier-confirmed handoff
Parameters: handoff_id (path, required)
Request schema: {"$ref":"#/components/schemas/MobileHandoffBookingAvailabilityRequest"}
Response 200: {"$ref":"#/components/schemas/MobileHandoffBookingAvailability"}
## GET /v1/mobile/receptionist/handoffs/{handoff_id}/booking
Read a handoff booking receipt without calendar or contact data
Parameters: handoff_id (path, required)
Response 200: {"$ref":"#/components/schemas/MobileHandoffBookingReceipt"}
## POST /v1/mobile/receptionist/handoffs/{handoff_id}/booking
Book one server-issued handoff calendar slot
Parameters: handoff_id (path, required), Idempotency-Key (header, required)
Request schema: {"$ref":"#/components/schemas/MobileHandoffBookingCreateRequest"}
Response 201: {"$ref":"#/components/schemas/MobileHandoffBookingReceipt"}
## GET /v1/agreements
The served legal documents and this tenant's acceptances
Response 200: {"$ref":"#/components/schemas/AgreementList"}
## POST /v1/agreements/accept
Accept the current version of one legal document
Request schema: {"$ref":"#/components/schemas/AgreementAcceptRequest"}
Response 201: {"$ref":"#/components/schemas/AgreementAcceptance"}
## GET /v1/agents
List this tenant's agents
Parameters: limit (query), cursor (query), created_at (query), created_at.gte (query), created_at.lte (query), created_at.gt (query), created_at.lt (query), created_at.in (query), status (query), status.gte (query), status.lte (query), status.gt (query), status.lt (query), status.in (query), use_case (query), use_case.gte (query), use_case.lte (query), use_case.gt (query), use_case.lt (query), use_case.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_AgentSummary_"}
## POST /v1/agents
Create an agent definition
Parameters: Idempotency-Key (header)
Request schema: {"$ref":"#/components/schemas/AgentCreateRequest"}
Response 201: {"$ref":"#/components/schemas/AgentResource"}
## GET /v1/agents/{agent_id}
Read one agent definition
Parameters: agent_id (path, required)
Response 200: {"$ref":"#/components/schemas/AgentResource"}
## PATCH /v1/agents/{agent_id}
Edit an agent definition
Parameters: agent_id (path, required)
Request schema: {"$ref":"#/components/schemas/AgentUpdateRequest"}
Response 200: {"$ref":"#/components/schemas/AgentResource"}
## GET /v1/agents/{agent_id}/release-gate-evaluations
List release-gate evaluations for an agent
Parameters: agent_id (path, required), limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_ReleaseGateEvaluation_"}
## POST /v1/agents/{agent_id}/release-gate-evaluations
Queue a scripted golden-transcript release evaluation
Queue one replay of the exact draft the customer sees now.
This route does not run a model or any tenant integration. It only snapshots the
draft, locks the definition while it deduplicates a request retry, and leaves the
deployment's scripted release worker to later replay built-in goldens with sandboxed tools.
Until that worker is deployed and claims the run, the customer-visible state remains queued.
Parameters: agent_id (path, required)
Response 202: {"$ref":"#/components/schemas/ReleaseGateEvaluationRequest"}
## GET /v1/agents/{agent_id}/receptionist-release
Read receptionist release readiness, evidence status, and rollback availability
Expose a compact control-center view without leaking sealed prompt knowledge.
A passing scripted replay keeps its historic meaning (publish-ready), while
``production_ready`` requires the separate live-model and carrier evidence that a
silent or misrouted PSTN session actually depends on.
Parameters: agent_id (path, required)
Response 200: {"$ref":"#/components/schemas/ReceptionistReleaseStatus"}
## POST /v1/agents/{agent_id}/receptionist-release/rollback
Immediately restore a policy-compatible healthy receptionist snapshot
Clone a prior healthy snapshot into a new active immutable version.
The prior row is never mutated. New inbound calls follow the definition's new latest
version, existing calls retain their original pinned version, and the endpoint refuses
if any current policy would select a location missing from the rollback target.
Parameters: agent_id (path, required), Idempotency-Key (header)
Request schema: {"$ref":"#/components/schemas/ReceptionistRollbackRequest"}
Response 200: {"$ref":"#/components/schemas/ReceptionistRollbackResult"}
## GET /v1/agents/{agent_id}/versions/{version}/voicemail-message
The exact voicemail message one published version leaves
Show the customer the text that will actually be spoken.
Served from the **same composer the drop uses**, on the **same pinned snapshot** the
drop reads, which is what makes this a proof rather than a mock-up: if this endpoint
and a live drop could disagree, the platform would not really be the one composing the
message. `estimated_seconds` is named an estimate because `` renders at Twilio's
rate, not ours.
The compliance-profile extension text is deliberately omitted, exactly as at publish:
it can only lengthen the opener, so the text here is the shortest lawful form of the
message rather than a different one.
Parameters: agent_id (path, required), version (path, required)
Response 200: {"$ref":"#/components/schemas/VoicemailMessage"}
## POST /v1/agents/{agent_id}/publish
Publish an immutable version of an agent
Parameters: agent_id (path, required)
Response 201: {"$ref":"#/components/schemas/AgentVersionResource"}
## GET /v1/agents/{agent_id}/versions
List an agent's published versions
Parameters: agent_id (path, required), limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_AgentVersionSummary_"}
## GET /v1/templates
List the platform template library
Parameters: limit (query), cursor (query), outcome_type (query), outcome_type.gte (query), outcome_type.lte (query), outcome_type.gt (query), outcome_type.lt (query), outcome_type.in (query), use_case (query), use_case.gte (query), use_case.lte (query), use_case.gt (query), use_case.lt (query), use_case.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_TemplateSummary_"}
## GET /v1/templates/{template_id}
Read one template, seed included
Parameters: template_id (path, required)
Response 200: {"$ref":"#/components/schemas/TemplateResource"}
## POST /v1/templates/{template_id}/instantiate
Create an editable agent from a template
Parameters: template_id (path, required), Idempotency-Key (header)
Request schema: {"$ref":"#/components/schemas/TemplateInstantiateRequest"}
Response 201: {"$ref":"#/components/schemas/AgentResource"}
## GET /v1/agents/{agent_id}/receptionist-intelligence
Read a receptionist's structured business intelligence draft
Parameters: agent_id (path, required)
Response 200: {"$ref":"#/components/schemas/ReceptionistIntelligenceSummary"}
## PUT /v1/agents/{agent_id}/receptionist-intelligence
Create or replace a receptionist's structured business intelligence draft
Parameters: agent_id (path, required)
Request schema: {"$ref":"#/components/schemas/ReceptionistIntelligenceUpsertRequest"}
Response 200: {"$ref":"#/components/schemas/ReceptionistIntelligenceSummary"}
## GET /v1/agents/{agent_id}/receptionist-intelligence/preview
Preview the compact business knowledge a receptionist version would receive
Parameters: agent_id (path, required)
Response 200: {"$ref":"#/components/schemas/ReceptionistIntelligencePreview"}
## GET /v1/agents/{agent_id}/receptionist-intelligence/sources
List immutable source snapshots backing receptionist intelligence
Parameters: agent_id (path, required)
Response 200: {"type":"array","items":{"$ref":"#/components/schemas/ReceptionistSourceSummary"},"title":"Response List Sources V1 Agents Agent Id Receptionist Intelligence Sources Get"}
## POST /v1/agents/{agent_id}/receptionist-intelligence/sources
Add a pending document or website snapshot for receptionist fact review
Parameters: agent_id (path, required)
Request schema: {"$ref":"#/components/schemas/ReceptionistSourceCreateRequest"}
Response 201: {"$ref":"#/components/schemas/ReceptionistSourceSummary"}
## GET /v1/agents/{agent_id}/receptionist-intelligence/sources/{source_id}
Read an immutable receptionist source snapshot for fact review
Parameters: agent_id (path, required), source_id (path, required)
Response 200: {"$ref":"#/components/schemas/ReceptionistSourceResource"}
## POST /v1/agents/{agent_id}/receptionist-intelligence/sources/{source_id}/approve
Approve an immutable source snapshot for cited receptionist facts
Parameters: agent_id (path, required), source_id (path, required)
Request schema: {"$ref":"#/components/schemas/ReceptionistSourceReviewRequest"}
Response 200: {"$ref":"#/components/schemas/ReceptionistSourceSummary"}
## POST /v1/agents/{agent_id}/receptionist-intelligence/sources/{source_id}/reject
Reject an immutable source snapshot and preserve the review reason
Parameters: agent_id (path, required), source_id (path, required)
Request schema: {"$ref":"#/components/schemas/ReceptionistSourceReviewRequest"}
Response 200: {"$ref":"#/components/schemas/ReceptionistSourceSummary"}
## POST /v1/agents/{agent_id}/receptionist-intelligence/sources/{source_id}/refresh
Create a manually refreshed immutable website snapshot
Parameters: agent_id (path, required), source_id (path, required)
Request schema: {"$ref":"#/components/schemas/ReceptionistSourceRefreshRequest"}
Response 201: {"$ref":"#/components/schemas/ReceptionistSourceResource"}
## GET /v1/agents/{agent_id}/receptionist-intelligence/versions
List a receptionist's immutable business-intelligence release history
Parameters: agent_id (path, required), limit (query)
Response 200: {"type":"array","items":{"$ref":"#/components/schemas/ReceptionistIntelligenceVersionSummary"},"title":"Response List Intelligence Versions V1 Agents Agent Id Receptionist Intelligence Versions Get"}
## POST /v1/agents/{agent_id}/receptionist-intelligence/versions
Publish the current reviewed receptionist intelligence as an immutable revision
Parameters: agent_id (path, required)
Response 201: {"$ref":"#/components/schemas/ReceptionistIntelligenceVersionResource"}
## GET /v1/agents/{agent_id}/receptionist-intelligence/versions/{version}
Read one immutable receptionist intelligence revision
Parameters: agent_id (path, required), version (path, required)
Response 200: {"$ref":"#/components/schemas/ReceptionistIntelligenceVersionResource"}
## GET /v1/agents/{agent_id}/receptionist-regression-cases
List the current immutable revisions of a receptionist's regression cases
Parameters: agent_id (path, required), include_archived (query)
Response 200: {"type":"array","items":{"$ref":"#/components/schemas/ReceptionistRegressionCaseSummary"},"title":"Response List Regression Cases V1 Agents Agent Id Receptionist Regression Cases Get"}
## POST /v1/agents/{agent_id}/receptionist-regression-cases
Create a versioned customer-authored receptionist regression case
Parameters: agent_id (path, required)
Request schema: {"$ref":"#/components/schemas/ReceptionistRegressionCaseCreateRequest"}
Response 201: {"$ref":"#/components/schemas/ReceptionistRegressionCaseSummary"}
## GET /v1/agents/{agent_id}/receptionist-regression-cases/{case_id}/versions
List an immutable receptionist regression case's revision history
Parameters: agent_id (path, required), case_id (path, required), limit (query)
Response 200: {"type":"array","items":{"$ref":"#/components/schemas/ReceptionistRegressionCaseVersionSummary"},"title":"Response List Regression Case Versions V1 Agents Agent Id Receptionist Regression Cases Case Id Versions Get"}
## GET /v1/agents/{agent_id}/receptionist-regression-cases/{case_id}/versions/{version}
Read one immutable receptionist regression case revision
Parameters: agent_id (path, required), case_id (path, required), version (path, required)
Response 200: {"$ref":"#/components/schemas/ReceptionistRegressionCaseVersionSummary"}
## GET /v1/agents/{agent_id}/receptionist-regression-cases/{case_id}
Read the current immutable revision of a receptionist regression case
Parameters: agent_id (path, required), case_id (path, required)
Response 200: {"$ref":"#/components/schemas/ReceptionistRegressionCaseSummary"}
## PUT /v1/agents/{agent_id}/receptionist-regression-cases/{case_id}
Append a new immutable receptionist regression case revision
Parameters: agent_id (path, required), case_id (path, required)
Request schema: {"$ref":"#/components/schemas/ReceptionistRegressionCaseUpdateRequest"}
Response 200: {"$ref":"#/components/schemas/ReceptionistRegressionCaseSummary"}
## POST /v1/agents/{agent_id}/receptionist-regression-cases/{case_id}/archive
Archive a regression case so later release gates no longer execute it
Parameters: agent_id (path, required), case_id (path, required)
Request schema: {"$ref":"#/components/schemas/ReceptionistRegressionCaseArchiveRequest"}
Response 200: {"$ref":"#/components/schemas/ReceptionistRegressionCaseSummary"}
## GET /v1/voices
List the platform stock-voice catalog
Parameters: limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_VoiceSummary_"}
## GET /v1/knowledge-packs
List this tenant's knowledge packs
Parameters: limit (query), cursor (query), created_at (query), created_at.gte (query), created_at.lte (query), created_at.gt (query), created_at.lt (query), created_at.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_KnowledgePackSummary_"}
## POST /v1/knowledge-packs
Create a knowledge pack
Parameters: Idempotency-Key (header)
Request schema: {"$ref":"#/components/schemas/KnowledgePackCreateRequest"}
Response 201: {"$ref":"#/components/schemas/KnowledgePackSummary"}
## POST /v1/knowledge-packs/{pack_id}/versions
Publish an edit as the pack's next immutable version
Parameters: pack_id (path, required)
Request schema: {"$ref":"#/components/schemas/KnowledgePackVersionCreateRequest"}
Response 201: {"$ref":"#/components/schemas/KnowledgePackVersionResource"}
## POST /v1/knowledge-packs/{pack_id}/documents
Ingest uploaded documents as the pack's next immutable version
Upload pdf/docx/csv/md/txt and publish the extraction as the next version.
Everything that can refuse the upload - decoding, extraction, sectioning, the real
token count, the hard cap - runs first and touches no database. Only a pack that has
already compiled inside the cap reaches `create_version`, which re-checks the cap
itself before its own first statement. The ordering CANON fact 12 depends on therefore
holds twice over: an over-cap upload consumes no version number and writes nothing.
Parameters: pack_id (path, required)
Request schema: {"$ref":"#/components/schemas/KnowledgePackIngestRequest"}
Response 201: {"$ref":"#/components/schemas/KnowledgePackIngestResponse"}
## GET /v1/knowledge-packs/{pack_id}/versions/{version}
Read one compiled version - the exact text the model sees
Parameters: pack_id (path, required), version (path, required)
Response 200: {"$ref":"#/components/schemas/KnowledgePackVersionResource"}
## GET /v1/voice-profiles
List this tenant's voice profiles
Parameters: limit (query), cursor (query), created_at (query), created_at.gte (query), created_at.lte (query), created_at.gt (query), created_at.lt (query), created_at.in (query), language (query), language.gte (query), language.lte (query), language.gt (query), language.lt (query), language.in (query), status (query), status.gte (query), status.lte (query), status.gt (query), status.lt (query), status.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_VoiceProfileSummary_"}
## POST /v1/voice-profiles
Withdrawn - voice profiles can no longer be created
## GET /v1/voice-rights-attestation
The Voice Rights & Consent Attestation this deployment serves
Response 200: {"$ref":"#/components/schemas/VoiceRightsAttestationDocument"}
## GET /v1/voice-clone-consent-form
The printable consent form the voice owner signs
Response 200: {"$ref":"#/components/schemas/VoiceRightsAttestationDocument"}
## GET /v1/voice-profiles/{profile_id}
Read one voice profile
Parameters: profile_id (path, required)
Response 200: {"$ref":"#/components/schemas/VoiceProfileSummary"}
## POST /v1/voice-profiles/{profile_id}/sample-recording
Withdrawn - voice samples are no longer accepted
Parameters: profile_id (path, required)
## POST /v1/voice-profiles/{profile_id}/consent-recording
Withdrawn - consent artifacts are no longer accepted
Parameters: profile_id (path, required)
## POST /v1/voice-profiles/{profile_id}/consent-proof
Withdrawn - consent releases are no longer accepted
Parameters: profile_id (path, required)
## GET /v1/contacts
List contacts
Parameters: limit (query), cursor (query), best_consent_class (query), best_consent_class.gte (query), best_consent_class.lte (query), best_consent_class.gt (query), best_consent_class.lt (query), best_consent_class.in (query), company (query), company.gte (query), company.lte (query), company.gt (query), company.lt (query), company.in (query), contact_list_id (query), contact_list_id.gte (query), contact_list_id.lte (query), contact_list_id.gt (query), contact_list_id.lt (query), contact_list_id.in (query), created_at (query), created_at.gte (query), created_at.lte (query), created_at.gt (query), created_at.lt (query), created_at.in (query), do_not_call (query), do_not_call.gte (query), do_not_call.lte (query), do_not_call.gt (query), do_not_call.lt (query), do_not_call.in (query), lead_status (query), lead_status.gte (query), lead_status.lte (query), lead_status.gt (query), lead_status.lt (query), lead_status.in (query), owner_user_id (query), owner_user_id.gte (query), owner_user_id.lte (query), owner_user_id.gt (query), owner_user_id.lt (query), owner_user_id.in (query), phone_e164 (query), phone_e164.gte (query), phone_e164.lte (query), phone_e164.gt (query), phone_e164.lt (query), phone_e164.in (query), q (query), q.gte (query), q.lte (query), q.gt (query), q.lt (query), q.in (query), tag (query), tag.gte (query), tag.lte (query), tag.gt (query), tag.lt (query), tag.in (query), timezone (query), timezone.gte (query), timezone.lte (query), timezone.gt (query), timezone.lt (query), timezone.in (query), updated_at (query), updated_at.gte (query), updated_at.lte (query), updated_at.gt (query), updated_at.lt (query), updated_at.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_ContactSummary_"}
## POST /v1/contacts
Create a contact
Parameters: Idempotency-Key (header)
Request schema: {"$ref":"#/components/schemas/ContactCreateRequest"}
Response 201: {"$ref":"#/components/schemas/Contact"}
## GET /v1/contacts/{contact_id}
Read a contact
Parameters: contact_id (path, required)
Response 200: {"$ref":"#/components/schemas/Contact"}
## PATCH /v1/contacts/{contact_id}
Update a contact
Parameters: contact_id (path, required)
Request schema: {"$ref":"#/components/schemas/ContactUpdateRequest"}
Response 200: {"$ref":"#/components/schemas/Contact"}
## DELETE /v1/contacts/{contact_id}
Soft-delete a contact
Parameters: contact_id (path, required)
Response 204: empty
## GET /v1/contacts/{contact_id}/dialability
Check whether this contact may be dialed right now
Evaluate the canonical dialability gate over this contact's record.
The same evaluator every dial passes, given the facts a contact row holds: the
denormalized internal-DNC flag, the consent class, the jurisdictions, and the callee's
local clock. Bureau scrub stages are billed per query and belong to a ScrubRun, so
none of them run here and `policy_snapshot.stages` is empty.
The same *resolution*, too - `outbound_effective_policy`, at this request's clock. A
preview that answered "dialable" for a state whose pack is not yet in force would be
reporting a permission the dial path itself refuses, which is the one direction a
preview must never be wrong in.
Parameters: contact_id (path, required), purpose (query)
Response 200: {"$ref":"#/components/schemas/ContactDialability"}
## GET /v1/contacts/{contact_id}/notes
List a contact's notes
Parameters: contact_id (path, required), limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_ContactNote_"}
## POST /v1/contacts/{contact_id}/notes
Leave a note on a contact
Append one working note.
A note is what a rep typed. It is not consent evidence and not a suppression: those
are `POST /v1/consents` and `POST /v1/dnc-entries`, both of which write records this
endpoint cannot produce and the dial gate actually reads. Writing "they asked not to
be called" here suppresses nothing.
`author_ref` is the credential that wrote it - the API key id here, because an API-key
principal names no person and inventing one would put a name in an audit trail that
nobody signed.
Parameters: contact_id (path, required)
Request schema: {"$ref":"#/components/schemas/ContactNoteCreateRequest"}
Response 201: {"$ref":"#/components/schemas/ContactNote"}
## DELETE /v1/contacts/{contact_id}/notes/{note_id}
Remove a note
Parameters: contact_id (path, required), note_id (path, required)
Response 204: empty
## POST /v1/contacts/bulk
Apply one action to up to 1000 contacts
The morning-after actions, in one request instead of one at a time.
Every branch is the *existing* per-contact write, run over a batch - the suppression
goes through the revocation path with all its derived writes, the delete is the same
soft delete `DELETE /contacts/{id}` performs, and list membership is the same join
`POST /contact-lists/{id}/members` writes. None of them invents a shortcut, which is
why a bulk suppress cannot end up weaker than a single one.
`suppress` additionally requires `dnc:manage`: it writes the internal DNC list, and a
key trusted to edit contacts is not thereby trusted to change what may be dialed.
Request schema: {"$ref":"#/components/schemas/ContactBulkRequest"}
Response 200: {"$ref":"#/components/schemas/ContactBulkResponse"}
## GET /v1/contact-lists
List contact lists
Parameters: limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_ContactListSummary_"}
## POST /v1/contact-lists
Create a contact list
Parameters: Idempotency-Key (header)
Request schema: {"$ref":"#/components/schemas/ContactListCreateRequest"}
Response 201: {"$ref":"#/components/schemas/ContactListSummary"}
## GET /v1/contact-lists/{list_id}/members
List the contacts on a list
Parameters: list_id (path, required), limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_ContactSummary_"}
## POST /v1/contact-lists/{list_id}/members
Add contacts to a list
Parameters: list_id (path, required)
Request schema: {"$ref":"#/components/schemas/ContactListMembersRequest"}
Response 200: {"$ref":"#/components/schemas/ContactListMembersResponse"}
## DELETE /v1/contact-lists/{list_id}/members
Remove contacts from a list
Take contacts off a list. The contacts themselves are untouched.
Mirrors `POST .../members` - same request shape, same 1000-id cap - because a caller
who can build the add call should not have to build a different one to undo it. A
customer who imported the wrong CSV into the wrong list unwinds it here.
Parameters: list_id (path, required)
Request schema: {"$ref":"#/components/schemas/ContactListMembersRequest"}
Response 200: {"$ref":"#/components/schemas/ContactListMembersRemovedResponse"}
## GET /v1/contact-lists/{list_id}
Read a contact list
Parameters: list_id (path, required)
Response 200: {"$ref":"#/components/schemas/ContactListSummary"}
## PATCH /v1/contact-lists/{list_id}
Rename a contact list
Rename it, and nothing else.
`consent_basis` and `attestation_ref` are not writable here. They record what a named
human affirmed about these rows at import time, and every dial made against the list
cites them - an attestation that can be edited afterwards is not evidence.
Parameters: list_id (path, required)
Request schema: {"$ref":"#/components/schemas/ContactListUpdateRequest"}
Response 200: {"$ref":"#/components/schemas/ContactListSummary"}
## DELETE /v1/contact-lists/{list_id}
Delete a contact list
Drop the list and its memberships. The contacts on it are untouched.
Refused while a campaign still targets it: the campaign's configuration is what its
calls were made under, and a target that could be deleted out from under a finished
campaign would break the record of who it was run against.
Parameters: list_id (path, required)
Response 204: empty
## GET /v1/contact-imports
List past imports
Import history, newest first.
Each row carries the consent attestation the import was submitted under - the named
signer, their title, the server-stamped address and time. Those are the evidence
behind every dial made against the list the import built, so the history is a record
to be kept rather than a convenience.
Parameters: limit (query), cursor (query), created_at (query), created_at.gte (query), created_at.lte (query), created_at.gt (query), created_at.lt (query), created_at.in (query), status (query), status.gte (query), status.lte (query), status.gt (query), status.lt (query), status.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_ImportJob_"}
## POST /v1/contact-imports
Import contacts from a CSV
Land a CSV in a contact list and report every row that could not be taken.
Answered at 202 with the documented poll contract on `GET /contact-imports/{id}`; the
P1 import runs inside this request, so the job is already terminal when it answers.
Parameters: Idempotency-Key (header)
Request schema: {"$ref":"#/components/schemas/ContactImportRequest"}
Response 202: {"$ref":"#/components/schemas/ImportJob"}
## GET /v1/contact-imports/{import_id}
Read an import job
Parameters: import_id (path, required)
Response 200: {"$ref":"#/components/schemas/ImportJob"}
## GET /v1/contact-imports/{import_id}/errors
List the rows an import rejected (JSON, or CSV with Accept: text/csv)
Parameters: import_id (path, required), limit (query), cursor (query), accept (header)
Response 200: {"$ref":"#/components/schemas/CursorPage_ImportRowError_"}
## GET /v1/contact-attempts
Follow-up feed: every attempt this tenant owns, across campaigns
The board an operator works the morning after, and the only place callbacks surface.
Every attempt this tenant owns - campaign-backed and campaign-less alike - filterable
on `next_eligible_at`, which is what makes "who is due before noon" a range query
instead of a scan: `?state=scheduled&next_eligible_at.lte=2026-08-11T17:00:00Z`.
It reports scheduling facts, not promises. `scheduled_for` is the earliest instant the
window layers allowed when the row was written; every attempt is re-gated at dial time
and one listed here can still be refused then (docs/07-compliance.md).
Parameters: limit (query), cursor (query), campaign_id (query), campaign_id.gte (query), campaign_id.lte (query), campaign_id.gt (query), campaign_id.lt (query), campaign_id.in (query), contact_id (query), contact_id.gte (query), contact_id.lte (query), contact_id.gt (query), contact_id.lt (query), contact_id.in (query), disposition (query), disposition.gte (query), disposition.lte (query), disposition.gt (query), disposition.lt (query), disposition.in (query), next_eligible_at (query), next_eligible_at.gte (query), next_eligible_at.lte (query), next_eligible_at.gt (query), next_eligible_at.lt (query), next_eligible_at.in (query), origin_kind (query), origin_kind.gte (query), origin_kind.lte (query), origin_kind.gt (query), origin_kind.lt (query), origin_kind.in (query), state (query), state.gte (query), state.lte (query), state.gt (query), state.lt (query), state.in (query), updated_at (query), updated_at.gte (query), updated_at.lte (query), updated_at.gt (query), updated_at.lt (query), updated_at.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_ContactAttemptSummary_"}
## GET /v1/campaigns
List campaigns
Parameters: limit (query), cursor (query), agent_definition_id (query), agent_definition_id.gte (query), agent_definition_id.lte (query), agent_definition_id.gt (query), agent_definition_id.lt (query), agent_definition_id.in (query), contact_list_id (query), contact_list_id.gte (query), contact_list_id.lte (query), contact_list_id.gt (query), contact_list_id.lt (query), contact_list_id.in (query), created_at (query), created_at.gte (query), created_at.lte (query), created_at.gt (query), created_at.lt (query), created_at.in (query), status (query), status.gte (query), status.lte (query), status.gt (query), status.lt (query), status.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_CampaignSummary_"}
## POST /v1/campaigns
Create a campaign
Parameters: Idempotency-Key (header)
Request schema: {"$ref":"#/components/schemas/CampaignCreateRequest"}
Response 201: {"$ref":"#/components/schemas/Campaign"}
## GET /v1/campaigns/overview
Read workspace campaign totals and recent call activity
Parameters: start (query, required), end (query, required)
Response 200: {"$ref":"#/components/schemas/CampaignOverview"}
## GET /v1/campaigns/{campaign_id}
Read one campaign
Parameters: campaign_id (path, required)
Response 200: {"$ref":"#/components/schemas/Campaign"}
## PATCH /v1/campaigns/{campaign_id}
Edit a campaign
Parameters: campaign_id (path, required)
Request schema: {"$ref":"#/components/schemas/CampaignUpdateRequest"}
Response 200: {"$ref":"#/components/schemas/Campaign"}
## POST /v1/campaigns/{campaign_id}/launch
Launch a campaign, pinning the agent version it dials under
Parameters: campaign_id (path, required), Idempotency-Key (header, required)
Response 200: {"$ref":"#/components/schemas/CampaignLaunchResponse"}
## POST /v1/campaigns/{campaign_id}/pause
Stop launching new dials
Parameters: campaign_id (path, required)
Response 200: {"$ref":"#/components/schemas/CampaignSummary"}
## POST /v1/campaigns/{campaign_id}/resume
Resume a paused campaign
Parameters: campaign_id (path, required)
Response 200: {"$ref":"#/components/schemas/CampaignSummary"}
## POST /v1/campaigns/{campaign_id}/cancel
Cancel a campaign for good
Parameters: campaign_id (path, required)
Response 200: {"$ref":"#/components/schemas/CampaignSummary"}
## GET /v1/campaigns/{campaign_id}/stats
Live campaign counters (polled at 5s; carries an ETag)
Parameters: campaign_id (path, required)
Response 200: {"$ref":"#/components/schemas/CampaignStats"}
## GET /v1/campaigns/{campaign_id}/attempts
Fill-status board: one row per attempt, most recently updated first
Parameters: campaign_id (path, required), limit (query), cursor (query), campaign_id (query), campaign_id.gte (query), campaign_id.lte (query), campaign_id.gt (query), campaign_id.lt (query), campaign_id.in (query), contact_id (query), contact_id.gte (query), contact_id.lte (query), contact_id.gt (query), contact_id.lt (query), contact_id.in (query), disposition (query), disposition.gte (query), disposition.lte (query), disposition.gt (query), disposition.lt (query), disposition.in (query), next_eligible_at (query), next_eligible_at.gte (query), next_eligible_at.lte (query), next_eligible_at.gt (query), next_eligible_at.lt (query), next_eligible_at.in (query), origin_kind (query), origin_kind.gte (query), origin_kind.lte (query), origin_kind.gt (query), origin_kind.lt (query), origin_kind.in (query), state (query), state.gte (query), state.lte (query), state.gt (query), state.lt (query), state.in (query), updated_at (query), updated_at.gte (query), updated_at.lte (query), updated_at.gt (query), updated_at.lt (query), updated_at.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_AttemptSummary_"}
## GET /v1/campaigns/{campaign_id}/outcomes
Per-contact reconciliation: the latest attempt and its structured outcome
Parameters: campaign_id (path, required), limit (query), cursor (query), disposition (query), disposition.gte (query), disposition.lte (query), disposition.gt (query), disposition.lt (query), disposition.in (query), has_outcome (query), has_outcome.gte (query), has_outcome.lte (query), has_outcome.gt (query), has_outcome.lt (query), has_outcome.in (query), sub_code (query), sub_code.gte (query), sub_code.lte (query), sub_code.gt (query), sub_code.lt (query), sub_code.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_CampaignOutcomeSummary_"}
## GET /v1/campaigns/{campaign_id}/survey-rollup
Survey rollup: response funnel, per-question distributions, CSAT/NPS aggregates
Parameters: campaign_id (path, required), created_at (query), created_at.gte (query), created_at.lte (query), created_at.gt (query), created_at.lt (query), created_at.in (query)
Response 200: {"$ref":"#/components/schemas/SurveyRollup"}
## GET /v1/dashboard-summary
Read the compact customer-portal Home summary
Return bounded carrier activity, current work, outcomes, and attention states.
Parameters: start (query), end (query)
Response 200: {"$ref":"#/components/schemas/DashboardSummary"}
## GET /v1/analytics/funnel
Dial → connect → conversation → outcome, with stage rates
Parameters: start (query), end (query), group_by (query), campaign_id (query), agent_version_id (query)
Response 200: {"$ref":"#/components/schemas/FunnelReport"}
## GET /v1/analytics/dispositions
Disposition mix, with each disposition's share of its group
Parameters: start (query), end (query), group_by (query), campaign_id (query), agent_version_id (query)
Response 200: {"$ref":"#/components/schemas/DispositionReport"}
## GET /v1/analytics/pathways
Business-pathway counters: leads, messages, tickets, callbacks, appointments
What the receptionist's conversations actually left behind (ADR-0037).
Counts durable rows only - never the outbox, which is a delivery artifact - so the
report and the resources it summarizes can never disagree.
Parameters: start (query), end (query), group_by (query)
Response 200: {"$ref":"#/components/schemas/PathwaysReport"}
## GET /v1/analytics/latency
Voice-to-voice and LLM-TTFT percentiles over the turn ledger
Parameters: start (query), end (query), group_by (query), campaign_id (query), agent_version_id (query)
Response 200: {"$ref":"#/components/schemas/LatencyReport"}
## POST /v1/scrub-runs
Queue a pre-campaign scrub of a contact list
Request schema: {"$ref":"#/components/schemas/ScrubRunCreateRequest"}
Response 202: {"$ref":"#/components/schemas/ScrubRun"}
## GET /v1/scrub-runs/{run_id}
Read a scrub report
Parameters: run_id (path, required)
Response 200: {"$ref":"#/components/schemas/ScrubRun"}
## GET /v1/scrub-runs/{run_id}/results
List a scrub run's per-contact rows
Parameters: run_id (path, required), limit (query), cursor (query), verdict (query), verdict.gte (query), verdict.lte (query), verdict.gt (query), verdict.lt (query), verdict.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_ScrubResultSummary_"}
## POST /v1/scrub-runs/{run_id}/acknowledge
Acknowledge a scrub report so it can authorize a launch
Parameters: run_id (path, required)
Request schema: {"$ref":"#/components/schemas/ScrubRunAcknowledgeRequest"}
Response 200: {"$ref":"#/components/schemas/ScrubRunAcknowledgement"}
## GET /v1/dnc-entries
List this tenant's internal DNC
Parameters: limit (query), cursor (query), phone_e164 (query), phone_e164.gte (query), phone_e164.lte (query), phone_e164.gt (query), phone_e164.lt (query), phone_e164.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_DncEntrySummary_"}
## POST /v1/dnc-entries
Record a revocation: suppress a number across every channel
Suppress a number on this tenant's internal DNC - and everything that implies.
A tenant writing this row is recording a revocation, so it takes the same path a
mid-call opt-out takes: the suppression, `optout.recorded`, and the withdrawal of the
consent standing behind the number, in one transaction. Writing the row alone would
leave the consent ledger saying this person still consents - the suppression blocks the
dial either way (internal DNC is an unwaivable floor, CANON fact 71), but the ledger is
what an evidence request is answered from, so it may not contradict the opt-out beside
it.
Which is why a revocation may not be time-limited here, and `expires_at` is refused by
name rather than accepted and quietly ignored - the same reason `scope` is expressible
at all (docs/07-compliance.md: internal DNC is a suppression record that persists, and
is carved out of erasure for exactly that reason).
Request schema: {"$ref":"#/components/schemas/DncEntryCreateRequest"}
Response 201: {"$ref":"#/components/schemas/DncEntrySummary"}
## GET /v1/consents
List this tenant's consent records
Parameters: limit (query), cursor (query), contact_id (query), contact_id.gte (query), contact_id.lte (query), contact_id.gt (query), contact_id.lt (query), contact_id.in (query), status (query), status.gte (query), status.lte (query), status.gt (query), status.lt (query), status.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_ConsentRecordSummary_"}
## POST /v1/consents
Append a consent record, or a revocation of one
Request schema: {"$ref":"#/components/schemas/ConsentCreateRequest"}
Response 201: {"$ref":"#/components/schemas/ConsentRecordSummary"}
## GET /v1/dnc-subscriptions
List this tenant's DNC registry subscriptions (SANs)
Response 200: {"items":{"$ref":"#/components/schemas/DncSubscriptionSummary"},"type":"array","title":"Response List Dnc Subscriptions V1 Dnc Subscriptions Get"}
## POST /v1/dnc-subscriptions
Register this tenant's federal SAN and the area codes it covers
Register (or refresh) this tenant's own FTC SAN - its federal DNC authorization.
This records authorization, not data. The federal registry is platform reference data
refreshed by ingestion and shared across tenants (the same boundary
`POST /dnc-entries` enforces when it refuses a federal scope); a tenant's subscription is
what gates that tenant's access to it. Re-registering the same SAN updates its coverage
rather than duplicating it. The credential is not accepted here - it is held in the
platform vault - so this endpoint records the account reference and coverage only.
Request schema: {"$ref":"#/components/schemas/DncSubscriptionRegisterRequest"}
Response 201: {"$ref":"#/components/schemas/DncSubscriptionSummary"}
## GET /v1/workflow-outcomes
List latest saved workforce and qualification responses
Parameters: fit_status (query), workflow (query), response (query), agent_id (query), campaign_id (query), start (query), end (query), limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_WorkflowOutcome_"}
## GET /v1/qualification-fit-metrics
Count current team decisions for a response cohort
Parameters: workflow (query), response (query), agent_id (query), campaign_id (query), start (query), end (query)
Response 200: {"$ref":"#/components/schemas/QualificationFitSummary"}
## GET /v1/workflow-outcomes/summary
Count current saved responses across a bounded window
Parameters: workflow (query), response (query), agent_id (query), campaign_id (query), start (query), end (query)
Response 200: {"$ref":"#/components/schemas/WorkflowOutcomeSummary"}
## GET /v1/workflow-outcomes/{action_id}/fit-reviews
Read the team review history for these exact answers
Parameters: action_id (path, required), limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_QualificationReview_"}
## POST /v1/workflow-outcomes/{action_id}/fit-reviews
Save a team decision against the current qualification answers
Parameters: action_id (path, required), Idempotency-Key (header, required)
Request schema: {"$ref":"#/components/schemas/QualificationReviewRequest"}
Response 201: {"$ref":"#/components/schemas/QualificationReview"}
## GET /v1/workflow-outcomes/{action_id}/contact-correction
Review a caller proposal against the current contact
Parameters: action_id (path, required)
Response 200: {"$ref":"#/components/schemas/ContactCorrectionReviewState"}
## POST /v1/workflow-outcomes/{action_id}/contact-correction-reviews
Apply or reject this caller correction with a final recorded decision
Parameters: action_id (path, required), Idempotency-Key (header, required)
Request schema: {"$ref":"#/components/schemas/ContactCorrectionReviewRequest"}
Response 201: {"$ref":"#/components/schemas/ContactCorrectionReview"}
## GET /v1/calls
List calls
Parameters: limit (query), cursor (query), agent_id (query), agent_id.gte (query), agent_id.lte (query), agent_id.gt (query), agent_id.lt (query), agent_id.in (query), agent_version_id (query), agent_version_id.gte (query), agent_version_id.lte (query), agent_version_id.gt (query), agent_version_id.lt (query), agent_version_id.in (query), campaign_id (query), campaign_id.gte (query), campaign_id.lte (query), campaign_id.gt (query), campaign_id.lt (query), campaign_id.in (query), contact_id (query), contact_id.gte (query), contact_id.lte (query), contact_id.gt (query), contact_id.lt (query), contact_id.in (query), created_at (query), created_at.gte (query), created_at.lte (query), created_at.gt (query), created_at.lt (query), created_at.in (query), direction (query), direction.gte (query), direction.lte (query), direction.gt (query), direction.lt (query), direction.in (query), disposition (query), disposition.gte (query), disposition.lte (query), disposition.gt (query), disposition.lt (query), disposition.in (query), has_campaign (query), has_campaign.gte (query), has_campaign.lte (query), has_campaign.gt (query), has_campaign.lt (query), has_campaign.in (query), started_at (query), started_at.gte (query), started_at.lte (query), started_at.gt (query), started_at.lt (query), started_at.in (query), state (query), state.gte (query), state.lte (query), state.gt (query), state.lt (query), state.in (query), sub_code (query), sub_code.gte (query), sub_code.lte (query), sub_code.gt (query), sub_code.lt (query), sub_code.in (query), use_case (query), use_case.gte (query), use_case.lte (query), use_case.gt (query), use_case.lt (query), use_case.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_CallSummary_"}
## POST /v1/calls
Queue one contact-bound campaign-less outbound call
Queue one compliant API-key-originated campaign-less outbound call.
Parameters: Idempotency-Key (header, required)
Request schema: {"$ref":"#/components/schemas/AdHocCallRequest"}
Response 202: {"$ref":"#/components/schemas/AdHocCall"}
## GET /v1/calls/inbound-voicemails
List inbound voicemail inbox receipts
List admitted inbound messages, including explicit import failures and absences.
This is a receipt inbox, not a second recordings API. When an item says ``available``,
its ``call_id`` can be read through the ordinary call detail and recording endpoints;
every other state is intentionally non-playable.
Parameters: limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_InboundVoicemailSummary_"}
## GET /v1/calls/inbound-voicemails/{voicemail_id}
Read one inbound voicemail receipt
Return receipt state and timing, never audio bytes or a carrier callback URL.
Parameters: voicemail_id (path, required)
Response 200: {"$ref":"#/components/schemas/InboundVoicemail"}
## GET /v1/calls/{call_id}
Read a call
The call detail, with its `analysis` object once post-call scoring has run.
`analysis` is null until the worker scores the call (docs/04-api.md Step 8a). It is
also served null - with a warning logged - when the stored verdict was written by a
results schema this deploy cannot read (a rollback across a schema bump): the call
detail is the primary resource and must not fail for a sidecar document, and the
verdict itself stays in the `eval_runs` ledger, readable again once the deploy
catches up.
Parameters: call_id (path, required)
Response 200: {"$ref":"#/components/schemas/Call"}
## GET /v1/calls/{call_id}/dispositions
A call's disposition history, oldest first
Every verdict ever written for this call, agent and human alike.
Not paginated: the list is one agent row plus however many times a person revisited
it, which is a handful, and a review UI needs all of them at once to show what changed.
Parameters: call_id (path, required)
Response 200: {"type":"array","items":{"$ref":"#/components/schemas/DispositionRecordSummary"},"title":"Response List Call Dispositions V1 Calls Call Id Dispositions Get"}
## POST /v1/calls/{call_id}/disposition
Record a human's verdict on a reviewed call
Append a `human_review` disposition and make it the call's current one.
An append, never an edit. The agent's original verdict stays exactly as written - the
`dispositions` table carries no `updated_at` for that reason - and this row becomes
what `GET /v1/calls/{id}` reports. `GET /calls/{id}/dispositions` shows both.
Reporting only. A disposition authorizes nothing and suppresses nothing: labelling a
call `opt_out` here does not put the number on the internal DNC list, and labelling it
`completed_goal` does not make the contact dialable. Suppression is
`POST /v1/dnc-entries`, which writes the entry, withdraws the consent standing behind
the number, and emits `optout.recorded`.
Parameters: call_id (path, required)
Request schema: {"$ref":"#/components/schemas/DispositionOverrideRequest"}
Response 201: {"$ref":"#/components/schemas/DispositionRecordSummary"}
## GET /v1/calls/{call_id}/turns
List a call's turns in order
Parameters: call_id (path, required), limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_CallTurn_"}
## GET /v1/calls/{call_id}/transcript
Read a call's transcript with its turns
Parameters: call_id (path, required)
Response 200: {"$ref":"#/components/schemas/CallTranscript"}
## GET /v1/calls/{call_id}/recording
Redirect to a short-lived signed URL for a call's recording
Parameters: call_id (path, required)
## GET /v1/appointments
List booked appointments
Parameters: limit (query), cursor (query), call_id (query), call_id.gte (query), call_id.lte (query), call_id.gt (query), call_id.lt (query), call_id.in (query), campaign_id (query), campaign_id.gte (query), campaign_id.lte (query), campaign_id.gt (query), campaign_id.lt (query), campaign_id.in (query), contact_id (query), contact_id.gte (query), contact_id.lte (query), contact_id.gt (query), contact_id.lt (query), contact_id.in (query), created_at (query), created_at.gte (query), created_at.lte (query), created_at.gt (query), created_at.lt (query), created_at.in (query), starts_at (query), starts_at.gte (query), starts_at.lte (query), starts_at.gt (query), starts_at.lt (query), starts_at.in (query), status (query), status.gte (query), status.lte (query), status.gt (query), status.lt (query), status.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_Appointment_"}
## GET /v1/survey-responses
List survey responses
Parameters: limit (query), cursor (query), agent_version_id (query), agent_version_id.gte (query), agent_version_id.lte (query), agent_version_id.gt (query), agent_version_id.lt (query), agent_version_id.in (query), call_id (query), call_id.gte (query), call_id.lte (query), call_id.gt (query), call_id.lt (query), call_id.in (query), campaign_id (query), campaign_id.gte (query), campaign_id.lte (query), campaign_id.gt (query), campaign_id.lt (query), campaign_id.in (query), completion_status (query), completion_status.gte (query), completion_status.lte (query), completion_status.gt (query), completion_status.lt (query), completion_status.in (query), created_at (query), created_at.gte (query), created_at.lte (query), created_at.gt (query), created_at.lt (query), created_at.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_SurveyResponse_"}
## GET /v1/calls/{call_id}/survey-response
Read the one survey response a call produced
The single-fetch convenience over the collection filter.
A call produces at most one response (`uq_survey_responses_call_id`), so the
integrator's common path - "the webhook named this call; give me its answers" - is
one GET rather than a filtered list walk. 404 when the call collected none.
Parameters: call_id (path, required)
Response 200: {"$ref":"#/components/schemas/SurveyResponse"}
## GET /v1/shift-confirmations
List shift confirmations
Parameters: limit (query), cursor (query), call_id (query), call_id.gte (query), call_id.lte (query), call_id.gt (query), call_id.lt (query), call_id.in (query), campaign_id (query), campaign_id.gte (query), campaign_id.lte (query), campaign_id.gt (query), campaign_id.lt (query), campaign_id.in (query), contact_id (query), contact_id.gte (query), contact_id.lte (query), contact_id.gt (query), contact_id.lt (query), contact_id.in (query), created_at (query), created_at.gte (query), created_at.lte (query), created_at.gt (query), created_at.lt (query), created_at.in (query), response (query), response.gte (query), response.lte (query), response.gt (query), response.lt (query), response.in (query), shift_ref (query), shift_ref.gte (query), shift_ref.lte (query), shift_ref.gt (query), shift_ref.lt (query), shift_ref.in (query), shift_starts_at (query), shift_starts_at.gte (query), shift_starts_at.lte (query), shift_starts_at.gt (query), shift_starts_at.lt (query), shift_starts_at.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_ShiftConfirmation_"}
## GET /v1/caller-request-metrics
Current resolution of messages and tickets received in a bounded period
Parameters: agent_id (query), campaign_id (query), start (query), end (query)
Response 200: {"$ref":"#/components/schemas/RequestResolutionSummary"}
## GET /v1/caller-requests
The caller-request queue: messages and tickets the AI took mid-call
Cursor + filters + a watermark ETag, exactly like the attempt feed it sits beside.
Parameters: limit (query), cursor (query), agent_version_id (query), agent_version_id.gte (query), agent_version_id.lte (query), agent_version_id.gt (query), agent_version_id.lt (query), agent_version_id.in (query), call_id (query), call_id.gte (query), call_id.lte (query), call_id.gt (query), call_id.lt (query), call_id.in (query), contact_id (query), contact_id.gte (query), contact_id.lte (query), contact_id.gt (query), contact_id.lt (query), contact_id.in (query), created_at (query), created_at.gte (query), created_at.lte (query), created_at.gt (query), created_at.lt (query), created_at.in (query), kind (query), kind.gte (query), kind.lte (query), kind.gt (query), kind.lt (query), kind.in (query), severity (query), severity.gte (query), severity.lte (query), severity.gt (query), severity.lt (query), severity.in (query), state (query), state.gte (query), state.lte (query), state.gt (query), state.lt (query), state.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_CallerRequestSummary_"}
## GET /v1/caller-requests/{request_id}
Read one caller request with its full event trail
Parameters: request_id (path, required)
Response 200: {"$ref":"#/components/schemas/CallerRequestResource"}
## PATCH /v1/caller-requests/{request_id}
Acknowledge, resolve, or assign a caller request
One optimistic-concurrency move: the caller names the version they read.
A version mismatch and an illegal transition are both 409s with distinct codes, so
the portal can tell "refresh and retry" from "someone already resolved this".
`ticket.resolved` is appended in this same transaction (`append_once` on the request
id - resolved is terminal, so once is all there ever is).
Parameters: request_id (path, required)
Request schema: {"$ref":"#/components/schemas/CallerRequestUpdateRequest"}
Response 200: {"$ref":"#/components/schemas/CallerRequestResource"}
## GET /v1/inbox
The merged inbox: everything a caller asked the business to do
One keyset timeline (created_at desc) over the five inbox kinds, caller included.
The per-kind resources remain authoritative for their own lifecycles - a client
navigates by the returned durable id, and this feed synthesizes no event table.
Without a `created_at.gte`, the feed defaults to the last 30 days
(`DEFAULT_INBOX_WINDOW`): five sources - one month-partitioned - must not make the
laziest request the most expensive one. An explicit `created_at.gte` overrides the
default entirely (docs/04-api.md §2).
Parameters: limit (query), cursor (query), agent_id (query), agent_id.gte (query), agent_id.lte (query), agent_id.gt (query), agent_id.lt (query), agent_id.in (query), agent_version_id (query), agent_version_id.gte (query), agent_version_id.lte (query), agent_version_id.gt (query), agent_version_id.lt (query), agent_version_id.in (query), campaign_id (query), campaign_id.gte (query), campaign_id.lte (query), campaign_id.gt (query), campaign_id.lt (query), campaign_id.in (query), created_at (query), created_at.gte (query), created_at.lte (query), created_at.gt (query), created_at.lt (query), created_at.in (query), kind (query), kind.gte (query), kind.lte (query), kind.gt (query), kind.lt (query), kind.in (query), phone_number_id (query), phone_number_id.gte (query), phone_number_id.lte (query), phone_number_id.gt (query), phone_number_id.lt (query), phone_number_id.in (query), state (query), state.gte (query), state.lte (query), state.gt (query), state.lt (query), state.in (query)
Response 200: {"$ref":"#/components/schemas/InboxPage"}
## GET /v1/human-reps
List human handoff destinations
Parameters: limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_HumanRep_"}
## POST /v1/human-reps
Create a tenant-owned human handoff destination
Parameters: Idempotency-Key (header)
Request schema: {"$ref":"#/components/schemas/HumanRepCreateRequest"}
Response 201: {"$ref":"#/components/schemas/HumanRep"}
## PATCH /v1/human-reps/{rep_id}
Update a human handoff destination
Parameters: rep_id (path, required)
Request schema: {"$ref":"#/components/schemas/HumanRepUpdateRequest"}
Response 200: {"$ref":"#/components/schemas/HumanRep"}
## GET /v1/rep-groups
List human-rep groups
Parameters: limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_RepGroup_"}
## POST /v1/rep-groups
Create a tenant-owned human-rep group
Parameters: Idempotency-Key (header)
Request schema: {"$ref":"#/components/schemas/RepGroupCreateRequest"}
Response 201: {"$ref":"#/components/schemas/RepGroup"}
## GET /v1/rep-groups/{rep_group_id}/members
List members of a human-rep group
Parameters: rep_group_id (path, required), limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_RepGroupMember_"}
## PUT /v1/rep-groups/{rep_group_id}/members/{rep_id}
Ensure a human rep belongs to a group
Parameters: rep_group_id (path, required), rep_id (path, required)
Response 200: {"$ref":"#/components/schemas/RepGroupMember"}
## PUT /v1/human-reps/{rep_id}/presence
Renew a short-lived manual human-rep availability lease
Parameters: rep_id (path, required)
Request schema: {"$ref":"#/components/schemas/RepPresenceHeartbeatRequest"}
Response 200: {"$ref":"#/components/schemas/RepPresence"}
## GET /v1/calls/{call_id}/handoff-readiness
Read truthful live handoff and supervision prerequisites for a call
Return a conservative capability answer; never imply carrier media exists.
It intentionally yields `200` with ``active: false`` for an absent/expired call rather
than using the permanent ``calls`` resource as a live-state oracle. A caller can poll
this during a conversation without learning whether an arbitrary historical call id
exists, and no follow-up mutation becomes possible from this response alone.
Parameters: call_id (path, required)
Response 200: {"$ref":"#/components/schemas/HandoffReadiness"}
## POST /v1/calls/{call_id}/live-listen-token
Mint a single-use short-lived listener capability for an eligible live call
Issue no token unless the external listener will have a real, lawful source.
The token goes in the first WSS frame, never a URL. Its opaque JTI is consumed by the
gateway's private attach request only after the socket is established; no bearer bytes
land in the database or audit log.
Parameters: call_id (path, required)
Response 200: {"$ref":"#/components/schemas/RealtimeToken"}
## GET /v1/calls/{call_id}/billing
Read the ledger-backed usage and current-plan projection for a call
One tenant-owned call's exact recorded usage and conservative cost visibility.
A live call receives ``completed: false``. Its ``live_projection`` is populated only
for a verified managed prepaid test-call receipt; every other active lane is explicitly
pending rather than a partial carrier cost. For a completed call, `meters` combines
direct runtime attribution with a managed carrier's attempt-linked minute row. That
carrier row may still be absent while the callback is in flight; an empty list means no
usage row has landed, not that a future invoice is $0.
Parameters: call_id (path, required)
Response 200: {"$ref":"#/components/schemas/CallBilling"}
## POST /v1/agents/{agent_id}/test-call
Hear your agent: one campaign-less call to a verified own number
Parameters: agent_id (path, required), Idempotency-Key (header)
Request schema: {"$ref":"#/components/schemas/TestCallRequest"}
Response 202: {"$ref":"#/components/schemas/TestCall"}
## POST /v1/agents/{agent_id}/voice-session
Open an in-browser voice session - the published version, or the draft when none is published (no phone call)
Mint one browser session against the agent's latest published version.
Everything that can refuse this session refuses it **here, in HTTP, with a body**. The
alternative is what the runtime would otherwise do: accept the socket, redeem the token,
fail a floor check, and close with a WSS 1008 and no payload - the worst diagnostic in
the product, because the operator learns only that "it didn't work". So the same four
checks the redemption runs (`modules/internal/bootstrap_checks.py`, imported rather than
reimplemented) run against the same pinned version before a token is minted at all:
`voice_profile_missing` and `disclosure_opener_missing` as 409s the tenant can fix,
`llm_chain_unresolvable` and `runtime_plane_unavailable` as 503s only a deployment can.
Nothing is written. A browser session's durable state begins at teardown, which is where
its `calls` row and its compliance evidence land (migration 0031) - so a session the
operator never connects leaves no attempt, no call and no meter behind it.
## The admission gates, and why they run where they do
The budget gates (`.admission`) run **first**, before any read: a caller who has spent
their address's hourly budget or their account's daily one must not cost us four queries
to find that out, and both refusals are the same whatever agent was named.
The lane reservation runs **last**, immediately before the mint. It is the one gate that
*holds* something, so it is taken only once every reason to refuse this session has been
ruled out - a reservation abandoned behind a `voice_profile_missing` would block that
account's next attempt for the reservation's whole life. If minting fails after it is
taken, it is given back in the same request.
Parameters: agent_id (path, required)
Response 201: {"$ref":"#/components/schemas/VoiceSession"}
## POST /v1/agents/{agent_id}/chat
Chat with this agent draft in text - no publish, no dial, no recording
Parameters: agent_id (path, required)
Request schema: {"$ref":"#/components/schemas/AgentChatRequest"}
Response 200: {}
## POST /v1/agents/{agent_id}/setup-chat
Assisted setup: interview + reviewable draft-patch proposals, never writes
Parameters: agent_id (path, required)
Request schema: {"$ref":"#/components/schemas/AgentChatRequest"}
Response 200: {}
## GET /v1/telephony-accounts
List this tenant's telephony accounts
Parameters: limit (query), cursor (query), mode (query), mode.gte (query), mode.lte (query), mode.gt (query), mode.lt (query), mode.in (query), provider (query), provider.gte (query), provider.lte (query), provider.gt (query), provider.lt (query), provider.in (query), verification_status (query), verification_status.gte (query), verification_status.lte (query), verification_status.gt (query), verification_status.lt (query), verification_status.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_TelephonyAccountSummary_"}
## POST /v1/telephony-accounts
Connect a BYO subaccount or elect the platform account
Request schema: {"$ref":"#/components/schemas/TelephonyAccountCreateRequest"}
Response 201: {"$ref":"#/components/schemas/TelephonyAccountSummary"}
## GET /v1/telephony-accounts/{account_id}
Read one telephony account
Parameters: account_id (path, required)
Response 200: {"$ref":"#/components/schemas/TelephonyAccountSummary"}
## PATCH /v1/telephony-accounts/{account_id}
Update an account, or rotate its carrier credentials
Parameters: account_id (path, required)
Request schema: {"$ref":"#/components/schemas/TelephonyAccountUpdateRequest"}
Response 200: {"$ref":"#/components/schemas/TelephonyAccountSummary"}
## PUT /v1/telephony-accounts/{account_id}/default
Make a verified telephony account the default for future account-omitted calls
Move only the tenant fallback; existing campaigns keep their elected account.
This is intentionally explicit rather than a side effect of connecting a later BYO
account. It lets a sandbox customer use the managed verified-number test lane first,
then choose verified BYO Twilio as the production fallback without silently rerouting
any campaign that already named an account.
Parameters: account_id (path, required)
Response 200: {"$ref":"#/components/schemas/TelephonyAccountSummary"}
## GET /v1/telephony-accounts/{account_id}/inbound-sms-webhook
Get the signed Twilio inbound-SMS webhook URL for a verified BYO account
Return the one account-level URL a customer copies into Twilio's Messaging config.
The route is visible only after the BYO account is verified. It does not try to make an
unverified credential or a platform-managed account look messaging-ready, and it never
serializes the credential reference or either credential field.
Parameters: account_id (path, required)
Response 200: {"$ref":"#/components/schemas/InboundSmsWebhookSummary"}
## GET /v1/telephony-accounts/{account_id}/inbound-voice-webhook
Get the signed Twilio inbound-Voice webhook URL for a verified BYO account
Return the account-level Voice URL a customer copies into Twilio's number config.
The URL remains useless without Twilio's account-specific request signature, and every
accepted callback re-resolves its signed destination against an active carrier-owned
voice number with a selected published agent. Managed or unverified accounts cannot
publish an inbound route, so they never become a back door around the BYO verification
boundary.
Parameters: account_id (path, required)
Response 200: {"$ref":"#/components/schemas/InboundVoiceWebhookSummary"}
## POST /v1/telephony-accounts/{account_id}/verify
Prove the account's credentials and read its CPS ceiling back
Parameters: account_id (path, required), Idempotency-Key (header)
Response 200: {"$ref":"#/components/schemas/TelephonyAccountSummary"}
## GET /v1/phone-numbers
List numbers and inventory
This tenant's numbers plus the unassigned platform inventory it may claim.
Parameters: limit (query), cursor (query), reputation_state (query), reputation_state.gte (query), reputation_state.lte (query), reputation_state.gt (query), reputation_state.lt (query), reputation_state.in (query), status (query), status.gte (query), status.lte (query), status.gt (query), status.lt (query), status.in (query), telephony_account_id (query), telephony_account_id.gte (query), telephony_account_id.lte (query), telephony_account_id.gt (query), telephony_account_id.lt (query), telephony_account_id.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_PhoneNumberSummary_"}
## POST /v1/phone-numbers
Buy or connect a number on one of this tenant's telephony accounts
Parameters: Idempotency-Key (header)
Request schema: {"$ref":"#/components/schemas/PhoneNumberCreateRequest"}
Response 201: {"$ref":"#/components/schemas/PhoneNumberSummary"}
## POST /v1/phone-numbers/{number_id}/assign
Claim a number for this tenant and put it into service
Parameters: number_id (path, required), Idempotency-Key (header)
Response 200: {"$ref":"#/components/schemas/PhoneNumberSummary"}
## POST /v1/phone-numbers/{number_id}/release
Take a number out of service
Parameters: number_id (path, required)
Response 200: {"$ref":"#/components/schemas/PhoneNumberSummary"}
## PATCH /v1/phone-numbers/{number_id}/inbound-voice-routing
Choose the live-agent or consent-gated voicemail route for a verified BYO number
Set or clear a number's inbound receiving route without widening carrier ingress.
Only an active number this tenant owns, whose carrier `PN…` identity was confirmed on a
verified BYO Twilio account, can carry a non-null route. Setting an agent additionally proves
the tenant has already published an immutable version; the carrier callback freezes that
version before returning TwiML, so this mutable setting is never read by a live stream.
Clearing remains allowed after an account is unverified or a number is released, because
the safe way to turn a stale route off must never depend on a carrier.
Parameters: number_id (path, required)
Request schema: {"$ref":"#/components/schemas/InboundVoiceRoutingUpdateRequest"}
Response 200: {"$ref":"#/components/schemas/PhoneNumberSummary"}
## GET /v1/phone-numbers/{number_id}/answering-policy
Read the answering policy one number answers by
The stored document, re-validated against the contract on the way out.
Readable without any carrier eligibility: a customer whose account has since been
unverified must still be able to see what their number is configured to do, exactly as
clearing a stale route never depends on a carrier.
Parameters: number_id (path, required)
Response 200: {"$ref":"#/components/schemas/AnsweringPolicy"}
## PUT /v1/phone-numbers/{number_id}/answering-policy
Write the answering policy a verified BYO number answers by
Replace one number's whole answering document and put it on the `policy` route.
The same eligibility a live-agent route requires, for the same reason: a policy arms
carrier ingress just as an agent does. What it additionally proves is that every
reference inside the document resolves *now* - each `ai` step's agent has a published
immutable version, and every `ring_humans` rep is an active transfer-enabled rep of this
tenant. Those are the two ways a policy could otherwise be written that answers a real
caller with a dead end, and a carrier callback is the wrong place to discover either.
The document is written before the route is moved, so a number is never in `policy` mode
with nothing to execute; both statements share the request transaction, so a failure at
either point leaves the number on the route it already had.
Parameters: number_id (path, required)
Request schema: {"$ref":"#/components/schemas/AnsweringPolicyUpsertRequest"}
Response 200: {"$ref":"#/components/schemas/AnsweringPolicy"}
## DELETE /v1/phone-numbers/{number_id}/answering-policy
Remove a number's answering policy and return it to the agent route
Delete the document and restore the `agent` route the number had before.
Deliberately not gated on carrier eligibility or on a live key. This is the only way to
disarm a policy route, and the safe way to turn a route off must never depend on the
carrier account still verifying - the same rule the legacy clear already follows.
The route is only moved when the number is actually on `policy`: a number a later
`inbound-voice-routing` PATCH already returned to an agent keeps that agent instead of
having it silently cleared by this call.
Parameters: number_id (path, required)
Response 204: empty
## GET /v1/number-pools
List caller-ID pools
Parameters: limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_NumberPoolSummary_"}
## POST /v1/number-pools
Create a caller-ID pool
Request schema: {"$ref":"#/components/schemas/NumberPoolCreateRequest"}
Response 201: {"$ref":"#/components/schemas/NumberPoolSummary"}
## POST /v1/number-pools/{pool_id}/members
Add a phone number to a caller-ID pool
Parameters: pool_id (path, required)
Request schema: {"$ref":"#/components/schemas/NumberPoolMemberCreateRequest"}
Response 201: {"$ref":"#/components/schemas/NumberPoolMemberSummary"}
## GET /v1/verified-numbers
List this tenant's verified-number registrations
Parameters: limit (query), cursor (query), status (query), status.gte (query), status.lte (query), status.gt (query), status.lt (query), status.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_VerifiedNumberSummary_"}
## POST /v1/verified-numbers
Register a destination and deliver its one-time code
Parameters: Idempotency-Key (header)
Request schema: {"$ref":"#/components/schemas/VerifiedNumberCreateRequest"}
Response 201: {"$ref":"#/components/schemas/VerifiedNumberCreated"}
## POST /v1/verified-numbers/{verified_number_id}/check
Confirm the one-time code delivered to the number
Parameters: verified_number_id (path, required)
Request schema: {"$ref":"#/components/schemas/VerifiedNumberCheckRequest"}
Response 200: {"$ref":"#/components/schemas/VerifiedNumberSummary"}
## GET /v1/integration-connections
List this tenant's integration connections
Parameters: limit (query), cursor (query), provider (query), provider.gte (query), provider.lte (query), provider.gt (query), provider.lt (query), provider.in (query), status (query), status.gte (query), status.lte (query), status.gt (query), status.lt (query), status.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_IntegrationConnectionSummary_"}
## POST /v1/integration-connections
Start a calendar OAuth consent
Mint the tenant-bound consent state and hand back the provider's consent URL.
**No row is written here.** `integration_connections.status` has no `pending` value
(docs/03-domain-model.md) - a connection exists only once consent has completed at the
callback, so an abandoned consent leaves nothing behind to expire or clean up.
Request schema: {"$ref":"#/components/schemas/IntegrationConnectionCreateRequest"}
Response 201: {"$ref":"#/components/schemas/IntegrationAuthorization"}
## GET /v1/integration-connections/callback
Complete a calendar OAuth consent
Unauthenticated by design: the customer's admin lands here from the provider's
consent screen carrying no API key, so the signed, tenant-bound, 10-minute state **is**
the admission control (docs/04-api.md §7).
Parameters: state (query, required), code (query), error (query)
Response 200: {"$ref":"#/components/schemas/IntegrationConnectionSummary"}
## POST /v1/calendar/availability
Free slots on the connected calendar (check_calendar's backing operation)
Request schema: {"$ref":"#/components/schemas/CalendarAvailabilityRequest"}
Response 200: {"$ref":"#/components/schemas/CalendarAvailability"}
## POST /v1/calendar/events
Book a meeting (book_meeting's backing operation)
Replay-safe without idempotency middleware.
The runtime derives its tool idempotency key from the same arguments, so a replayed
`book_meeting` turn re-presents the same `(call_id, start_time)` pair:
`find_for_call` returns the existing appointment **before any provider call**, and
`uq_appointments_integration_connection_id_external_event_id` closes the race two
concurrent replays could still slip through.
Request schema: {"$ref":"#/components/schemas/CalendarEventCreateRequest"}
Response 201: {"$ref":"#/components/schemas/Appointment"}
## GET /v1/usage/summary
One billing month across all nine meters
Parameters: period (query)
Response 200: {"$ref":"#/components/schemas/UsageSummary"}
## GET /v1/usage/events
Page through the usage-event ledger
Parameters: limit (query), cursor (query), call_id (query), call_id.gte (query), call_id.lte (query), call_id.gt (query), call_id.lt (query), call_id.in (query), campaign_id (query), campaign_id.gte (query), campaign_id.lte (query), campaign_id.gt (query), campaign_id.lt (query), campaign_id.in (query), meter (query), meter.gte (query), meter.lte (query), meter.gt (query), meter.lt (query), meter.in (query), occurred_at (query), occurred_at.gte (query), occurred_at.lte (query), occurred_at.gt (query), occurred_at.lt (query), occurred_at.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_UsageEventSummary_"}
## GET /v1/invoices
List invoices
Parameters: limit (query), cursor (query), created_at (query), created_at.gte (query), created_at.lte (query), created_at.gt (query), created_at.lt (query), created_at.in (query), period_start (query), period_start.gte (query), period_start.lte (query), period_start.gt (query), period_start.lt (query), period_start.in (query), status (query), status.gte (query), status.lte (query), status.gt (query), status.lt (query), status.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_Invoice_"}
## GET /v1/subscription
Read this tenant's subscription
Response 200: {"$ref":"#/components/schemas/Subscription"}
## POST /v1/subscription
Start this tenant's production subscription
Subscribe this tenant without ever retrying an ambiguous Stripe create blindly.
The request has three independently committed phases: a tenant-unique operation before
Stripe I/O, the provider correlation after Stripe answers, then the local subscription
mirror. A crash after any phase leaves a durable recovery point. In particular, a
retry that finds ``pending`` does **not** make another Stripe POST: its first outcome is
unknown and a signed ``customer.subscription.created`` webhook must reconcile it.
Request schema: {"$ref":"#/components/schemas/SubscriptionRequest"}
Response 201: {"$ref":"#/components/schemas/SubscriptionCreated"}
## GET /v1/subscription/commitment
Read verified original commitment dates
Read the original 36-calendar-month term without changing billing or service.
Dates require verified checkout and agreement provenance and survive paid plan upgrades.
Monthly terms return `not_committed`; unverified histories return `review_required`.
`term_elapsed` reports the anniversary only, not a renewal or cancellation decision.
Response 200: {"$ref":"#/components/schemas/SubscriptionCommitment"}
## GET /v1/billing/budget
Read this tenant's budget
Response 200: {"$ref":"#/components/schemas/Budget"}
## PUT /v1/billing/budget
Set this tenant's budget
Set the spend cap and the low-balance floor. This is the cap the pacer enforces.
No audit action id and no step-up: this is the *tenant* setting its own budget on its
own money, not staff moving someone else's (that path is
`PATCH /admin/tenants/{id}/subscription`, which is `critical` and step-up-gated for
exactly that reason - docs/19 §6.1).
Request schema: {"$ref":"#/components/schemas/BudgetRequest"}
Response 200: {"$ref":"#/components/schemas/Budget"}
## PUT /v1/subscription/extra-sessions
Set this tenant's purchased extra concurrent sessions
Set purchased outbound capacity using a durable provider operation.
Read live items before claiming the frozen mutation. A committed claim owns the
source across provider I/O; exact provider correlation and local completion are
required before returning success. A retry can recover an earlier request before
applying a different absolute target. Stripe's default proration is retained.
Request schema: {"$ref":"#/components/schemas/ExtraSessionsRequest"}
Response 200: {"$ref":"#/components/schemas/Subscription"}
## PUT /v1/subscription/receptionist-connections
Set this tenant's purchased receptionist connections
Converge the receptionist connection count at Stripe and locally (ADR-0025).
**An added connection is paid for before it is reserved, and it is paid for a full
month** (the owner's decision of 2026-08-18). A receptionist connection is not a line
on a bill - it is a GPU slot withheld from every other claim on this platform for as
long as the tenant holds it, so granting one on the strength of an invoice that
renders up to a month later hands out physical capacity on credit. So an INCREASE
collects `250·Δbase + 150·Δadditional` immediately
(`_receptionist_month_due_minor`), and only a collected charge reaches
`set_receptionist_connections` and the capacity refresh below. The recurring items are
moved with `proration_behavior="none"` because Stripe would otherwise ALSO invoice a
prorated stub for the same days at the next renewal - the customer would pay the
remainder of this month twice - and the recurring price then simply starts at the next
renewal.
A DECREASE is gated on no payment and on no billing state a customer can be trapped
in: giving capacity back costs this platform nothing to accept, and refusing it would
be refusing to stop charging someone. That last clause used to be a claim the code did
not keep - the `subscription_inactive` guard ran before the direction was known and
refused BOTH directions, so a `past_due` tenant could not lower the bill it was
already behind on, which is the one tenant most likely to be asking. The guard is now
split by direction: only an INCREASE is held to `_RECEPTIONIST_PURCHASE_STATUSES`,
while a decrease is accepted wherever the Stripe subscription still has items to
change (`_RECEPTIONIST_RELEASE_STATUSES`). It refunds nothing either - the month was
prepaid, which is the honest consequence of the decision above and which the portal
states before the customer confirms rather than this endpoint discovering for them.
The extra-sessions endpoint's discipline over TWO items instead of one. Absolute
semantics are what make it safely retryable without a durable provisioning operation:
an ambiguous outcome cannot double a fee, because setting N twice is setting N. A
definite Stripe success updates the local row; an ambiguous outcome leaves it untouched
and answers 502 `receptionist_connections_update_unconfirmed`, with the signed
`customer.subscription.updated` webhook as the reconciler.
The charge is under that same definite-versus-ambiguous split and carries codes of its
own, because "your card said no" and "the item change failed" send the customer to
different places: `402 receptionist_connections_charge_declined` is a clean refusal
that changed nothing at all, and `502 receptionist_connections_charge_unconfirmed`
says the money may have moved while no subscription item did - the local count is
untouched, no webhook is coming for it, and the resolution is re-sending the same
absolute target, which replays the same charge instead of collecting a second month
(`_receptionist_charge_idempotency_key`).
A third, `402 receptionist_connections_charge_authentication_required`, makes the same
statement about the money as the decline and the opposite statement about the cure: the
card works and the issuer wants the customer present to authenticate, so the invoice
the attempt left open is where the payment is completed and the card must NOT be
replaced. It is a separate code because the code is the only thing a client can branch
on, and while it shared the decline's every portal pinned to that code told the
customer to change a payment method that was never refused.
Both 402s name the invoice their attempt left behind rather than pretending it is not
there. A failed `pay` leaves a FINALIZED, OPEN invoice that the tenant's own
`GET /v1/invoices` mirror shows, voiding it would break the "send the same number
again" cure that replays onto it, and a refusal that told a customer to ignore an
invoice they can see is how a support ticket becomes a chargeback.
That promise needs one thing absolute semantics cannot supply on their own, and it is
the same thing in every part of the request: what Stripe actually holds. Every
ambiguous outcome here leaves the local row behind on purpose, so the row lags a change
that landed - in its item ids, which would make a retried 0 -> N first purchase ADD a
second item and invoice $250 or $150 twice, AND in its count, which would make the
next genuinely different target collect a month for connections Stripe already bills.
So the request reads the live item set once (`_held_licensed_items`) and that one read
decides all four of whether there is anything to do, the mutation, the amount, and the
charge's idempotency key.
**Whether there is anything to do is the one that is not obvious, and it is not a
question the local row may answer.** A target equal to the count the row shows looks
like a request with nothing to do - and that is precisely the shape of the customer's
natural "give the connection back" after an ambiguous 1 -> 2 landed at Stripe and not
here: it answered 200, sent Stripe nothing, and left the $150 item billing for as long
as the subscription lived. The undocumented cure was to set a HIGHER number first and
then the lower one, which no customer discovers. So a request that would change
Stripe's items now makes that change whatever the local row says, and only a
subscription Stripe already holds at the target quantities is answered without a
mutation. The money rules are unmoved by it: a decrease still collects nothing, and an
increase is still priced from what Stripe holds, so converging a row onto items the
customer already paid for is free.
Two items rather than one because the first connection is $250 and the rest are $150
each, and which of them a subscription holds depends on its plan: a standalone
receptionist plan already includes the first, an outbound plan attaching a receptionist
buys it. `rate_card.receptionist_line_items` owns that split; this route only converges
whatever it returns, and it moves both items in ONE Stripe request so no invoice can
show the additional connections billed while the first one is not.
**A comped or internal tenant skips the prepay entirely**
(`_PREPAY_EXEMPT_COMMERCIAL_STATUSES`): the ADR-0026 d3 hardening removed their
default payment method on purpose, so collecting would 402 on every increase forever.
The items move, the local count is written, no invoice is raised and no card exists to
charge - the recurring price simply lands on the next `send_invoice` invoice at full
value, where the credit note offsets it.
Request schema: {"$ref":"#/components/schemas/ReceptionistConnectionsRequest"}
Response 200: {"$ref":"#/components/schemas/Subscription"}
## GET /v1/subscription/expansions
Active Expansion
Recover the in-progress purchase when the customer returns on another device.
Response 200: {"anyOf":[{"$ref":"#/components/schemas/ExpansionQuoteResponse"},{"type":"null"}],"title":"Response Active Expansion V1 Subscription Expansions Get"}
## POST /v1/subscription/expansions
Quote Expansion
Request schema: {"$ref":"#/components/schemas/ExpansionQuoteRequest"}
Response 200: {"$ref":"#/components/schemas/ExpansionQuoteResponse"}
## GET /v1/subscription/expansions/{intent_id}
Get Expansion
Parameters: intent_id (path, required)
Response 200: {"$ref":"#/components/schemas/ExpansionQuoteResponse"}
## DELETE /v1/subscription/expansions/{intent_id}
Cancel Expansion
Parameters: intent_id (path, required)
Response 200: {"$ref":"#/components/schemas/ExpansionQuoteResponse"}
## POST /v1/subscription/expansions/{intent_id}/checkout
Checkout Expansion
Confirm a reviewed price and open its original hosted payment page.
A 202 response retains the same purchase for recovery; it is not proof of payment.
Parameters: intent_id (path, required)
Request schema: {"$ref":"#/components/schemas/ExpansionCheckoutRequest"}
Response 200: {"$ref":"#/components/schemas/ExpansionQuoteResponse"}
Response 202: {"$ref":"#/components/schemas/ExpansionQuoteResponse"}
## POST /v1/subscription/expansions/{intent_id}/recover
Recover Expansion
Recover the original payment and activation; never create another Checkout.
Recovery remains available when new purchases are disabled. A verified payment
can finish its existing plan change and refresh purchased capacity.
Parameters: intent_id (path, required)
Response 200: {"$ref":"#/components/schemas/ExpansionQuoteResponse"}
Response 202: {"$ref":"#/components/schemas/ExpansionQuoteResponse"}
## POST /v1/subscription/receptionist-connections/quote
Quote Receptionist Connections
Request schema: {"$ref":"#/components/schemas/ReceptionistQuoteRequest"}
Response 200: {"$ref":"#/components/schemas/ReceptionistConnectionQuote"}
## GET /v1/subscription/receptionist-purchases
Active Receptionist Purchase
Parameters: request_key (query)
Response 200: {"anyOf":[{"$ref":"#/components/schemas/ReceptionistPurchaseResponse"},{"type":"null"}],"title":"Response Active Receptionist Purchase V1 Subscription Receptionist Purchases Get"}
## POST /v1/subscription/receptionist-purchases
Purchase Receptionist Connections
Request schema: {"$ref":"#/components/schemas/ReceptionistPurchaseInput"}
Response 200: {"$ref":"#/components/schemas/ReceptionistPurchaseResponse"}
Response 202: {"$ref":"#/components/schemas/ReceptionistPurchaseResponse"}
## GET /v1/subscription/receptionist-purchases/{purchase_id}
Get Receptionist Purchase
Parameters: purchase_id (path, required)
Response 200: {"$ref":"#/components/schemas/ReceptionistPurchaseResponse"}
## POST /v1/subscription/receptionist-purchases/{purchase_id}/recover
Recover Receptionist Purchase
Parameters: purchase_id (path, required)
Response 200: {"$ref":"#/components/schemas/ReceptionistPurchaseResponse"}
Response 202: {"$ref":"#/components/schemas/ReceptionistPurchaseResponse"}
## GET /v1/billing/prepaid
Read prepaid managed-call credit
Response 200: {"$ref":"#/components/schemas/PrepaidWallet"}
## POST /v1/billing/prepaid/funding-intents
Open Stripe Checkout for manual prepaid managed-call credit
Create/recover one funding intent, then ask Stripe for its hosted Checkout URL.
A transport timeout deliberately returns ``202`` *without* rolling the new intent
back. Stripe may have accepted the request, so retrying the same key reuses the local
intent id and Stripe idempotency key instead of risking a second charge.
Request schema: {"$ref":"#/components/schemas/PrepaidFundingRequest"}
Response 201: {"$ref":"#/components/schemas/PrepaidFundingIntent"}
## GET /v1/billing/prepaid/auto-fund
Read automatic prepaid managed-test-call funding configuration
Response 200: {"$ref":"#/components/schemas/AutoFundResponse"}
## PUT /v1/billing/prepaid/auto-fund
Enable, change, or disable automatic prepaid managed-test-call funding
Request schema: {"$ref":"#/components/schemas/AutoFundConfigRequest"}
Response 200: {"$ref":"#/components/schemas/AutoFundResponse"}
## POST /v1/billing/customer-portal
Open Stripe-hosted payment, invoice, and subscription management
Mint a customer-scoped Stripe portal session for the authenticated tenant.
We resolve the customer and subscription from the tenant database rather than accepting
either ID from the browser. Otherwise a tenant that guessed another ``cus_`` or ``sub_``
identifier could be handed a management link for someone else's account.
Request schema: {"$ref":"#/components/schemas/CustomerPortalRequest"}
Response 200: {"$ref":"#/components/schemas/CustomerPortalSession"}
## POST /v1/exports
Submit an async CSV export
Queue one extraction and answer 202 with the poll contract on `GET /exports/{id}`.
Parameters: Idempotency-Key (header)
Request schema: {"$ref":"#/components/schemas/ExportCreateRequest"}
Response 202: {"$ref":"#/components/schemas/ExportJob"}
## GET /v1/exports/{export_id}
Read an export job
Parameters: export_id (path, required)
Response 200: {"$ref":"#/components/schemas/ExportJob"}
## GET /v1/webhook-endpoint
Read the tenant's webhook endpoint
Response 200: {"$ref":"#/components/schemas/WebhookEndpointSummary"}
## PUT /v1/webhook-endpoint
Create or replace the tenant's webhook endpoint
Upsert the tenant's single endpoint.
Creating mints a signing secret and returns it - the only time it is ever readable.
Replacing keeps the stored secret (``secret: null``) and re-activates an endpoint
that was `disabled_by_failures`: fixing the URL and re-`PUT`ting is the documented
way back after an auto-disable.
Request schema: {"$ref":"#/components/schemas/WebhookEndpointRequest"}
Response 200: {"$ref":"#/components/schemas/WebhookEndpointSecretView"}
## POST /v1/webhook-endpoint/rotate-secret
Rotate the endpoint's signing secret
Mint a new signing secret, keeping the superseded one valid for 24 hours.
During the overlap every delivery carries two `v1=` signatures - one per secret - so
a receiver can be redeployed with the new key at any point inside the window
(docs/04-api.md §5 signing). `previous_secret_expires_at` on the response is when
the old key stops verifying.
An `Idempotency-Key` is honored so a retried rotate replays the first rotation's minted
secret rather than rotating a second time - a double rotate would collapse the previous
secret's 24-hour overlap before a receiver could pick up the new one. The claim shares
this request's transaction with the rotation, so a failure frees the key.
Parameters: Idempotency-Key (header)
Response 200: {"$ref":"#/components/schemas/WebhookEndpointSecretView"}
## GET /v1/webhook-deliveries
List webhook deliveries
The inspectable delivery ledger, newest first.
Filterable on `event_id`, `endpoint_id`, `status`, and `created_at` ranges - the
documented recovery path is `?event_id=…` plus re-fetching the underlying resource.
Parameters: limit (query), cursor (query), created_at (query), created_at.gte (query), created_at.lte (query), created_at.gt (query), created_at.lt (query), created_at.in (query), endpoint_id (query), endpoint_id.gte (query), endpoint_id.lte (query), endpoint_id.gt (query), endpoint_id.lt (query), endpoint_id.in (query), event_id (query), event_id.gte (query), event_id.lte (query), event_id.gt (query), event_id.lt (query), event_id.in (query), status (query), status.gte (query), status.lte (query), status.gt (query), status.lt (query), status.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_WebhookDelivery_"}
## POST /v1/webhook-endpoint/test
Send one signed sample event to the endpoint, synchronously
Prove the receiver end to end before any real call depends on it.
The sample rides the exact production mechanics - same envelope keys, same compact
key-ordered serialization, same `Vocapable-Signature` scheme signed with the same
secrets (both, during a rotation overlap), same 10-second success rule - so a
receiver that verifies this ping verifies real traffic. `data.test: true` is the
marker; nothing durable is written and no outbox row is minted.
Response 200: {"$ref":"#/components/schemas/WebhookTestResult"}
## POST /v1/webhook-deliveries/{delivery_id}/redrive
Put one exhausted delivery back on the retry ladder
Self-service recovery after an outage outlasted the 24-hour retry window.
Only an `exhausted` delivery moves: `pending`/`failed` are already on the ladder,
and a `delivered` one was received - re-sending it would deliver the same event
twice on a human's timescale. The requeued delivery gets exactly one more POST
(the enqueue window is never reopened); a receiver that is still down returns it
to `exhausted`, one press at a time.
Parameters: delivery_id (path, required)
Response 200: {"$ref":"#/components/schemas/WebhookDelivery"}
## GET /v1/receptionist-action-targets
List bounded receptionist outcome delivery targets
The control-center view of native outcome routing and its latest evidence.
The catalog is fixed in ``ReceptionistActionOutcome``; this route never renders a
tenant-defined executable tool, schema, command, or signing secret. Delivery counts
are an observation of the durable ledger, not a promise that an external receiver
completed business work.
Response 200: {"items":{"$ref":"#/components/schemas/ReceptionistActionTargetSummary"},"type":"array","title":"Response List Receptionist Action Targets V1 Receptionist Action Targets Get"}
## PUT /v1/receptionist-action-targets/{outcome_type}
Bind one predefined receptionist outcome to the signed endpoint
Upsert one closed-catalog action binding.
The endpoint must already exist through ``PUT /webhook-endpoint``. That endpoint owns
the one-time HMAC secret and the durable retry worker; this resource only decides which
native outcome it may receive after its Inbox/resource transaction has committed.
Parameters: outcome_type (path, required)
Request schema: {"$ref":"#/components/schemas/ReceptionistActionTargetRequest"}
Response 200: {"$ref":"#/components/schemas/ReceptionistActionTargetSummary"}
## DELETE /v1/receptionist-action-targets/{outcome_type}
Remove one receptionist outcome delivery binding
Idempotently stop future external delivery for one outcome.
Existing Inbox/resource rows and any already-enqueued delivery ledger rows remain
durable/auditable. Removing a binding cannot erase a caller's request or falsely turn a
delivered/failed historical outcome into a different state.
Parameters: outcome_type (path, required)
Response 204: empty
## GET /v1/receptionist-action-targets/{outcome_type}/deliveries
List durable deliveries for one receptionist outcome
The exact retry/delivery history behind one bound native outcome.
Parameters: outcome_type (path, required), limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_WebhookDelivery_"}
## POST /v1/receptionist-action-targets/{outcome_type}/test
Send a signed setup-check for one receptionist outcome target
Prove the exact action envelope/signature without creating a caller outcome.
This carries the real outcome event type but ``data.test: true`` and writes no Inbox,
resource, event, or delivery ledger row. Its result is persisted on the binding so the
portal can show the last setup check separately from the durable delivery ledger.
Parameters: outcome_type (path, required)
Response 200: {"$ref":"#/components/schemas/WebhookTestResult"}
## GET /v1/webhook-targets
List every webhook target, active and disabled
Parameters: limit (query), cursor (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_WebhookTargetSummary_"}
## POST /v1/webhook-targets
Register a custom_webhook tool target
Register one target, minting its signing secret - the only time it is readable.
Everything bind time would refuse is refused here, where it costs one API request
instead of one tool on a live call: a reserved name, a schema the runtime's validator
cannot enforce, one over the prompt budget, and a URL outside the delivery policy.
Request schema: {"$ref":"#/components/schemas/WebhookTargetCreateRequest"}
Response 201: {"$ref":"#/components/schemas/WebhookTargetSecretView"}
## GET /v1/webhook-targets/{target_id}
Read one webhook target
Parameters: target_id (path, required)
Response 200: {"$ref":"#/components/schemas/WebhookTargetSummary"}
## PATCH /v1/webhook-targets/{target_id}
Update a webhook target
Apply the supplied fields, under the same boundary checks as registration.
An edit takes effect for calls redeemed after it commits; a call already running keeps
the configuration its session was bound with.
Parameters: target_id (path, required)
Request schema: {"$ref":"#/components/schemas/WebhookTargetUpdateRequest"}
Response 200: {"$ref":"#/components/schemas/WebhookTargetSummary"}
## DELETE /v1/webhook-targets/{target_id}
Disable a webhook target
Disable-not-delete: the id stays attributable in transcripts and eval history.
Idempotent - deleting an already-disabled target changes nothing and answers 204,
so a retried DELETE cannot fail its caller. Re-enabling is `PATCH {status: active}`.
Parameters: target_id (path, required)
Response 204: empty
## POST /v1/webhook-targets/{target_id}/rotate-secret
Rotate the target's signing secret
Mint a new signing secret, keeping the superseded one valid for 24 hours.
During the overlap every dispatch carries two `v1=` signatures - one per secret - so
the receiver can be redeployed with the new key at any point inside the window,
exactly like the endpoint rotation above (docs/04-api.md §5 signing).
An `Idempotency-Key` is honored so a retried rotate replays the first rotation's minted
secret rather than rotating twice and collapsing the previous secret's overlap. The
claim shares this request's transaction with the rotation.
Parameters: target_id (path, required), Idempotency-Key (header)
Response 200: {"$ref":"#/components/schemas/WebhookTargetSecretView"}
## GET /v1/messages
List this tenant's messages
Parameters: limit (query), cursor (query), contact_id (query), contact_id.gte (query), contact_id.lte (query), contact_id.gt (query), contact_id.lt (query), contact_id.in (query), created_at (query), created_at.gte (query), created_at.lte (query), created_at.gt (query), created_at.lt (query), created_at.in (query), direction (query), direction.gte (query), direction.lte (query), direction.gt (query), direction.lt (query), direction.in (query), inbound_intent (query), inbound_intent.gte (query), inbound_intent.lte (query), inbound_intent.gt (query), inbound_intent.lt (query), inbound_intent.in (query), origin_call_id (query), origin_call_id.gte (query), origin_call_id.lte (query), origin_call_id.gt (query), origin_call_id.lt (query), origin_call_id.in (query), status (query), status.gte (query), status.lte (query), status.gt (query), status.lt (query), status.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_MessageSummary_"}
## POST /v1/messages
Send an SMS from a number this tenant holds
Parameters: Idempotency-Key (header)
Request schema: {"$ref":"#/components/schemas/SmsSendRequest"}
Response 201: {"$ref":"#/components/schemas/MessageSummary"}
## GET /v1/message-templates
List this tenant's message templates
Parameters: limit (query), cursor (query), locale (query), locale.gte (query), locale.lte (query), locale.gt (query), locale.lt (query), locale.in (query), status (query), status.gte (query), status.lte (query), status.gt (query), status.lt (query), status.in (query)
Response 200: {"$ref":"#/components/schemas/CursorPage_MessageTemplateSummary_"}
## POST /v1/message-templates
Create a message template
Request schema: {"$ref":"#/components/schemas/MessageTemplateCreateRequest"}
Response 201: {"$ref":"#/components/schemas/MessageTemplateSummary"}
## PATCH /v1/message-templates/{template_id}
Archive or reactivate a message template
The one legal template mutation: the status flip (see the request model).
Parameters: template_id (path, required)
Request schema: {"$ref":"#/components/schemas/MessageTemplateUpdateRequest"}
Response 200: {"$ref":"#/components/schemas/MessageTemplateSummary"}
## POST /v1/messages/inbound
Record an inbound SMS delivered by the provider
Request schema: {"$ref":"#/components/schemas/InboundMessageRequest"}
Response 201: {"$ref":"#/components/schemas/InboundMessageAck"}
## GET /v1/workspace-capabilities
Read the deployment and tenant capability map for this workspace
Return one fresh readiness map without mutating a workspace or reserving capacity.
Response 200: {"$ref":"#/components/schemas/WorkspaceCapabilities"}
## POST /v1/workspace-telemetry
Record one privacy-safe customer portal funnel event
Write only finite, non-identifying portal dimensions to structured monitoring.
This endpoint deliberately owns no database model, progression record, counter, or
resource join. It is a best-effort operational signal; the browser never waits for
it to decide whether a customer can keep setting up their workspace.
Request schema: {"$ref":"#/components/schemas/PortalTelemetryEventRequest"}
Response 200: {"$ref":"#/components/schemas/PortalTelemetryReceipt"}
# Schemas
- Accepted: {"properties":{"message":{"type":"string","title":"Message"},"action_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Action Url"}},"type":"object","required":["message"],"title":"Accepted"}
- AccountDeletionBlocker: {"properties":{"code":{"type":"string","title":"Code"},"title":{"type":"string","title":"Title"},"detail":{"type":"string","title":"Detail"},"next_action":{"type":"string","title":"Next Action"}},"type":"object","required":["code","title","detail","next_action"],"title":"AccountDeletionBlocker"}
- AccountDeletionImpact: {"properties":{"workspaces":{"items":{"$ref":"#/components/schemas/WorkspaceImpact"},"type":"array","title":"Workspaces"},"active_membership_count":{"type":"integer","title":"Active Membership Count"},"legacy_user_count":{"type":"integer","title":"Legacy User Count"},"legacy_owner_workspace_count":{"type":"integer","title":"Legacy Owner Workspace Count"},"last_owner_workspace_count":{"type":"integer","title":"Last Owner Workspace Count"},"active_owner_subscription_workspace_count":{"type":"integer","title":"Active Owner Subscription Workspace Count"},"staff_identity_count":{"type":"integer","title":"Staff Identity Count"},"onboarding_application_count":{"type":"integer","title":"Onboarding Application Count"},"invitation_reference_count":{"type":"integer","title":"Invitation Reference Count"}},"type":"object","required":["workspaces","active_membership_count","legacy_user_count","legacy_owner_workspace_count","last_owner_workspace_count","active_owner_subscription_workspace_count","staff_identity_count","onboarding_application_count","invitation_reference_count"],"title":"AccountDeletionImpact"}
- AccountDeletionOverview: {"properties":{"fresh_auth_method":{"type":"string","enum":["password","email_link"],"title":"Fresh Auth Method"},"impact":{"$ref":"#/components/schemas/AccountDeletionImpact"},"blockers":{"items":{"$ref":"#/components/schemas/AccountDeletionBlocker"},"type":"array","title":"Blockers"},"request":{"anyOf":[{"$ref":"#/components/schemas/AccountDeletionRequestView"},{"type":"null"}]},"can_confirm_or_reconfirm":{"type":"boolean","title":"Can Confirm Or Reconfirm"},"retention_notices":{"items":{"$ref":"#/components/schemas/RetentionNotice"},"type":"array","title":"Retention Notices"}},"type":"object","required":["fresh_auth_method","impact","blockers","request","can_confirm_or_reconfirm","retention_notices"],"title":"AccountDeletionOverview"}
- AccountDeletionRequestView: {"properties":{"id":{"type":"string","title":"Id"},"status":{"type":"string","enum":["pending","awaiting_owner_transfer","awaiting_workspace_termination","awaiting_staff_offboarding","awaiting_retention_review","processing","completed","refused"],"title":"Status"},"verification_method":{"type":"string","enum":["password","email_link"],"title":"Verification Method"},"requested_at":{"type":"string","format":"date-time","title":"Requested At"},"confirmed_at":{"type":"string","format":"date-time","title":"Confirmed At"},"response_due_at":{"type":"string","format":"date-time","title":"Response Due At"},"processing_started_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Processing Started At"},"completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Completed At"},"refused_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Refused At"}},"type":"object","required":["id","status","verification_method","requested_at","confirmed_at","response_due_at","processing_started_at","completed_at","refused_at"],"title":"AccountDeletionRequestView"}
- AccountSummary: {"properties":{"id":{"type":"string","title":"Id"},"email":{"type":"string","title":"Email"},"email_verified":{"type":"boolean","title":"Email Verified"}},"type":"object","required":["id","email","email_verified"],"title":"AccountSummary"}
- ActionConfirmationProof: {"properties":{"kind":{"type":"string","enum":["shift_response","qualification"],"title":"Kind"},"subject_ref":{"type":"string","maxLength":64,"minLength":1,"title":"Subject Ref"},"response":{"type":"string","enum":["accepted","declined","tentative","interested","not_interested","needs_follow_up","unknown"],"title":"Response"},"policy_version":{"type":"string","const":"exact_action_v1","title":"Policy Version","default":"exact_action_v1"},"confirming_turn":{"$ref":"#/components/schemas/ConversationEvidence"},"method":{"type":"string","enum":["explicit_statement","heard_readback"],"title":"Method"},"readback":{"anyOf":[{"$ref":"#/components/schemas/HeardReadbackEvidence"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["kind","subject_ref","response","confirming_turn","method"],"title":"ActionConfirmationProof","description":"Runtime-created attestation, never a model-authored tool argument.\n\nThis attests the trusted runtime's ledger, not independently verified audio.\nA fresh native tool call is still required before dispatch."}
- ActionTokenRequest: {"properties":{"token":{"type":"string","maxLength":512,"minLength":32,"title":"Token"}},"type":"object","required":["token"],"title":"ActionTokenRequest"}
- AdHocCall: {"properties":{"id":{"type":"string","title":"Id"},"agent_id":{"type":"string","title":"Agent Id"},"agent_version_id":{"type":"string","title":"Agent Version Id"},"contact_id":{"type":"string","title":"Contact Id"},"telephony":{"type":"string","enum":["mock","byo"],"title":"Telephony"},"state":{"type":"string","title":"State"},"disposition":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Disposition"},"call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","agent_id","agent_version_id","contact_id","telephony","state","created_at"],"title":"AdHocCall","description":"The queued campaign-less ContactAttempt returned by `POST /v1/calls`."}
- AdHocCallRequest: {"properties":{"agent_id":{"type":"string","maxLength":64,"minLength":1,"title":"Agent Id"},"contact_id":{"type":"string","maxLength":64,"minLength":1,"title":"Contact Id"},"questions":{"items":{"$ref":"#/components/schemas/CallQuestion"},"type":"array","maxItems":10,"title":"Questions"},"context":{"additionalProperties":{"type":"string"},"type":"object","title":"Context"}},"additionalProperties":false,"type":"object","required":["agent_id","contact_id"],"title":"AdHocCallRequest","description":"One contact-bound campaign-less outbound attempt (`POST /v1/calls`).\n\nThe deliberately small shape is the safety boundary: callers name durable tenant\nresources only. There is no raw E.164 destination, caller-id override, or carrier\nroute that could turn the endpoint into an ungoverned dialer. `questions` and\n`context` are bounded CONVERSATION inputs (migration 0070): additive survey\nquestions the pinned version's objectives do not carry, and key: value facts the\nagent may use - never dial-routing inputs, never platform-key shadows, and capped\nhard enough that the call context stays a context, not a document."}
- AgentChatMessage: {"properties":{"role":{"type":"string","enum":["user","assistant"],"title":"Role"},"content":{"type":"string","maxLength":4000,"minLength":1,"title":"Content"}},"additionalProperties":false,"type":"object","required":["role","content"],"title":"AgentChatMessage"}
- AgentChatRequest: {"properties":{"messages":{"items":{"$ref":"#/components/schemas/AgentChatMessage"},"type":"array","maxItems":40,"minItems":1,"title":"Messages"}},"additionalProperties":false,"type":"object","required":["messages"],"title":"AgentChatRequest","description":"The full visible transcript, oldest first, ending with the user's new message."}
- AgentCreateRequest: {"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"},"use_case":{"type":"string","enum":["sdr","staffing","survey","reminder","custom","receptionist","recruiting"],"title":"Use Case"},"conversation_settings":{"$ref":"#/components/schemas/ConversationSettings"},"persona":{"additionalProperties":true,"type":"object","title":"Persona"},"objectives":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Objectives"},"guardrails":{"$ref":"#/components/schemas/Guardrails"},"tools":{"items":{"type":"string","enum":["check_calendar","book_meeting","send_sms","send_verification_code","verify_identity","record_sms_consent","take_message","open_ticket","update_lead","record_survey_answer","confirm_shift","record_shift_response","record_qualification","schedule_callback","transfer_to_human","record_optout","end_call","custom_webhook"]},"type":"array","title":"Tools"},"sms_template_ids":{"items":{"type":"string"},"type":"array","title":"Sms Template Ids"},"handoff_target_rep_ids":{"items":{"type":"string"},"type":"array","title":"Handoff Target Rep Ids"},"llm_route":{"anyOf":[{"$ref":"#/components/schemas/LlmRoute"},{"type":"null"}]},"stock_voice_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stock Voice Id"},"voice_profile_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Voice Profile Id"},"knowledge_pack_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Knowledge Pack Id"},"knowledge_pack_version":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"title":"Knowledge Pack Version"},"disclosure_config":{"additionalProperties":{"type":"string"},"type":"object","minProperties":1,"title":"Disclosure Config"},"voicemail_config":{"$ref":"#/components/schemas/VoicemailConfig"},"compliance_profile_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compliance Profile Id"}},"additionalProperties":false,"type":"object","required":["name","use_case","disclosure_config"],"title":"AgentCreateRequest","description":"`POST /v1/agents`. Extra fields are refused, which is what rejects `stt_route`."}
- AgentResource: {"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"use_case":{"type":"string","enum":["sdr","staffing","survey","reminder","custom","receptionist","recruiting"],"title":"Use Case"},"status":{"type":"string","enum":["draft","active","archived"],"title":"Status"},"latest_version":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Latest Version"},"stock_voice_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stock Voice Id"},"voice_profile_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Voice Profile Id"},"knowledge_pack_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Knowledge Pack Id"},"seeded_from_template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Seeded From Template Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"persona":{"additionalProperties":true,"type":"object","title":"Persona"},"conversation_settings":{"$ref":"#/components/schemas/ConversationSettings"},"objectives":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Objectives"},"guardrails":{"$ref":"#/components/schemas/Guardrails"},"tools":{"items":{"type":"string","enum":["check_calendar","book_meeting","send_sms","send_verification_code","verify_identity","record_sms_consent","take_message","open_ticket","update_lead","record_survey_answer","confirm_shift","record_shift_response","record_qualification","schedule_callback","transfer_to_human","record_optout","end_call","custom_webhook"]},"type":"array","title":"Tools"},"sms_template_ids":{"items":{"type":"string"},"type":"array","title":"Sms Template Ids"},"handoff_targets":{"items":{"$ref":"#/components/schemas/DirectRepHandoffTarget"},"type":"array","title":"Handoff Targets"},"llm_route":{"anyOf":[{"$ref":"#/components/schemas/LlmRoute"},{"type":"null"}]},"knowledge_pack_version":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Knowledge Pack Version"},"disclosure_config":{"additionalProperties":{"type":"string"},"type":"object","title":"Disclosure Config"},"voicemail_config":{"$ref":"#/components/schemas/VoicemailConfig"},"compliance_profile_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compliance Profile Id"},"seeded_from_template_version":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Seeded From Template Version"}},"type":"object","required":["id","name","use_case","status","created_at","updated_at","persona","objectives","guardrails","tools","disclosure_config"],"title":"AgentResource","description":"A definition in full."}
- AgentSummary: {"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"use_case":{"type":"string","enum":["sdr","staffing","survey","reminder","custom","receptionist","recruiting"],"title":"Use Case"},"status":{"type":"string","enum":["draft","active","archived"],"title":"Status"},"latest_version":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Latest Version"},"stock_voice_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stock Voice Id"},"voice_profile_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Voice Profile Id"},"knowledge_pack_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Knowledge Pack Id"},"seeded_from_template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Seeded From Template Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","name","use_case","status","created_at","updated_at"],"title":"AgentSummary","description":"A definition as a list row."}
- AgentUpdateRequest: {"properties":{"name":{"anyOf":[{"type":"string","maxLength":200,"minLength":1},{"type":"null"}],"title":"Name"},"use_case":{"anyOf":[{"type":"string","enum":["sdr","staffing","survey","reminder","custom","receptionist","recruiting"]},{"type":"null"}],"title":"Use Case"},"conversation_settings":{"anyOf":[{"$ref":"#/components/schemas/ConversationSettings"},{"type":"null"}]},"persona":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Persona"},"objectives":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Objectives"},"guardrails":{"anyOf":[{"$ref":"#/components/schemas/Guardrails"},{"type":"null"}]},"tools":{"anyOf":[{"items":{"type":"string","enum":["check_calendar","book_meeting","send_sms","send_verification_code","verify_identity","record_sms_consent","take_message","open_ticket","update_lead","record_survey_answer","confirm_shift","record_shift_response","record_qualification","schedule_callback","transfer_to_human","record_optout","end_call","custom_webhook"]},"type":"array"},{"type":"null"}],"title":"Tools"},"sms_template_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Sms Template Ids"},"handoff_target_rep_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Handoff Target Rep Ids"},"llm_route":{"anyOf":[{"$ref":"#/components/schemas/LlmRoute"},{"type":"null"}]},"stock_voice_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stock Voice Id"},"voice_profile_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Voice Profile Id"},"knowledge_pack_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Knowledge Pack Id"},"knowledge_pack_version":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"title":"Knowledge Pack Version"},"disclosure_config":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object","minProperties":1},{"type":"null"}],"title":"Disclosure Config"},"voicemail_config":{"anyOf":[{"$ref":"#/components/schemas/VoicemailConfig"},{"type":"null"}]},"compliance_profile_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compliance Profile Id"},"status":{"anyOf":[{"type":"string","enum":["draft","archived"]},{"type":"null"}],"title":"Status"}},"additionalProperties":false,"type":"object","title":"AgentUpdateRequest","description":"`PATCH /v1/agents/{id}`. Only the fields present in the body are applied."}
- AgentVersionResource: {"properties":{"id":{"type":"string","title":"Id"},"agent_definition_id":{"type":"string","title":"Agent Definition Id"},"version":{"type":"integer","title":"Version"},"content_hash":{"type":"string","title":"Content Hash"},"action_confirmation_policy":{"type":"string","enum":["legacy_v0","exact_action_v1","exact_action_v2"],"title":"Action Confirmation Policy","default":"legacy_v0"},"max_call_seconds":{"type":"integer","title":"Max Call Seconds"},"stock_voice_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stock Voice Id"},"voice_profile_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Voice Profile Id"},"knowledge_pack_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Knowledge Pack Version Id"},"receptionist_intelligence_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Receptionist Intelligence Version Id"},"receptionist_release_status":{"type":"string","enum":["not_applicable","unqualified","qualified","draining"],"title":"Receptionist Release Status","default":"not_applicable"},"receptionist_capability_manifest_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Receptionist Capability Manifest Hash"},"receptionist_rendered_token_budget":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Receptionist Rendered Token Budget"},"receptionist_live_preflight_eval_run_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Receptionist Live Preflight Eval Run Id"},"receptionist_canary_route_fingerprint":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Receptionist Canary Route Fingerprint"},"receptionist_qualified_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Receptionist Qualified At"},"compliance_profile_id":{"type":"string","title":"Compliance Profile Id"},"gate_eval_run_id":{"type":"string","title":"Gate Eval Run Id"},"published_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Published By"},"published_at":{"type":"string","format":"date-time","title":"Published At"},"persona":{"additionalProperties":true,"type":"object","title":"Persona"},"use_case":{"type":"string","enum":["sdr","staffing","survey","reminder","custom","receptionist","recruiting"],"title":"Use Case","default":"custom"},"conversation_settings":{"$ref":"#/components/schemas/ConversationSettings"},"objectives":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Objectives"},"guardrails":{"$ref":"#/components/schemas/Guardrails"},"tools":{"items":{"type":"string","enum":["check_calendar","book_meeting","send_sms","send_verification_code","verify_identity","record_sms_consent","take_message","open_ticket","update_lead","record_survey_answer","confirm_shift","record_shift_response","record_qualification","schedule_callback","transfer_to_human","record_optout","end_call","custom_webhook"]},"type":"array","title":"Tools"},"sms_template_ids":{"items":{"type":"string"},"type":"array","title":"Sms Template Ids"},"handoff_targets":{"items":{"$ref":"#/components/schemas/DirectRepHandoffTarget"},"type":"array","title":"Handoff Targets"},"llm_route":{"anyOf":[{"$ref":"#/components/schemas/LlmRoute"},{"type":"null"}]},"disclosure_config":{"additionalProperties":{"type":"string"},"type":"object","title":"Disclosure Config"},"voicemail_config":{"$ref":"#/components/schemas/VoicemailConfig"}},"type":"object","required":["id","agent_definition_id","version","content_hash","max_call_seconds","compliance_profile_id","gate_eval_run_id","published_at","persona","objectives","guardrails","tools","disclosure_config"],"title":"AgentVersionResource","description":"The fully resolved configuration a call executes."}
- AgentVersionSummary: {"properties":{"id":{"type":"string","title":"Id"},"agent_definition_id":{"type":"string","title":"Agent Definition Id"},"version":{"type":"integer","title":"Version"},"content_hash":{"type":"string","title":"Content Hash"},"action_confirmation_policy":{"type":"string","enum":["legacy_v0","exact_action_v1","exact_action_v2"],"title":"Action Confirmation Policy","default":"legacy_v0"},"max_call_seconds":{"type":"integer","title":"Max Call Seconds"},"stock_voice_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stock Voice Id"},"voice_profile_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Voice Profile Id"},"knowledge_pack_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Knowledge Pack Version Id"},"receptionist_intelligence_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Receptionist Intelligence Version Id"},"receptionist_release_status":{"type":"string","enum":["not_applicable","unqualified","qualified","draining"],"title":"Receptionist Release Status","default":"not_applicable"},"receptionist_capability_manifest_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Receptionist Capability Manifest Hash"},"receptionist_rendered_token_budget":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Receptionist Rendered Token Budget"},"receptionist_live_preflight_eval_run_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Receptionist Live Preflight Eval Run Id"},"receptionist_canary_route_fingerprint":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Receptionist Canary Route Fingerprint"},"receptionist_qualified_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Receptionist Qualified At"},"compliance_profile_id":{"type":"string","title":"Compliance Profile Id"},"gate_eval_run_id":{"type":"string","title":"Gate Eval Run Id"},"published_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Published By"},"published_at":{"type":"string","format":"date-time","title":"Published At"}},"type":"object","required":["id","agent_definition_id","version","content_hash","max_call_seconds","compliance_profile_id","gate_eval_run_id","published_at"],"title":"AgentVersionSummary","description":"An immutable published snapshot, as a list row."}
- AgreementAcceptRequest: {"properties":{"agreement_kind":{"type":"string","enum":["msa","telecom_addendum","test_calls_addendum","aup","dpa","production_services_order","scrub_attestation","api_source_attestation","three_year_commitment_addendum","cold_outreach_waiver"],"title":"Agreement Kind"},"version":{"type":"string","maxLength":100,"minLength":1,"title":"Version"},"content_sha256":{"type":"string","pattern":"^[0-9a-f]{64}$","title":"Content Sha256"},"signer_name":{"type":"string","maxLength":200,"minLength":1,"title":"Signer Name"},"signer_title":{"type":"string","maxLength":200,"minLength":1,"title":"Signer Title"},"signer_email":{"type":"string","maxLength":320,"minLength":3,"pattern":"^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$","title":"Signer Email"}},"additionalProperties":false,"type":"object","required":["agreement_kind","version","content_sha256","signer_name","signer_title","signer_email"],"title":"AgreementAcceptRequest","description":"One click-through acceptance. The caller echoes the version and hash it was served;\n`signer_ip` and `accepted_at` are stamped by the API, never client input - the same\nposture as `ConsentAttestation`."}
- AgreementAcceptance: {"properties":{"id":{"type":"string","title":"Id"},"agreement_kind":{"type":"string","enum":["msa","telecom_addendum","test_calls_addendum","aup","dpa","production_services_order","scrub_attestation","api_source_attestation","three_year_commitment_addendum","cold_outreach_waiver"],"title":"Agreement Kind"},"version":{"type":"string","title":"Version"},"content_sha256":{"type":"string","title":"Content Sha256"},"signer_name":{"type":"string","title":"Signer Name"},"signer_title":{"type":"string","title":"Signer Title"},"signer_email":{"type":"string","title":"Signer Email"},"signer_ip":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Signer Ip"},"accepted_at":{"type":"string","format":"date-time","title":"Accepted At"}},"type":"object","required":["id","agreement_kind","version","content_sha256","signer_name","signer_title","signer_email","accepted_at"],"title":"AgreementAcceptance","description":"A recorded acceptance, as the append-only ledger holds it."}
- AgreementDocument: {"properties":{"kind":{"type":"string","enum":["msa","telecom_addendum","test_calls_addendum","aup","dpa","production_services_order","scrub_attestation","api_source_attestation","three_year_commitment_addendum","cold_outreach_waiver"],"title":"Kind"},"version":{"type":"string","title":"Version"},"content_sha256":{"type":"string","title":"Content Sha256"},"markdown":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Markdown"},"gate_scope":{"type":"string","enum":["account","production","headless","commitment","cold_outreach"],"title":"Gate Scope"},"draft":{"type":"boolean","title":"Draft"},"accepted":{"type":"boolean","title":"Accepted"},"accepted_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Accepted Version"},"accepted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Accepted At"}},"type":"object","required":["kind","version","content_sha256","gate_scope","draft","accepted"],"title":"AgreementDocument","description":"One gated document as currently served, with the caller-tenant's acceptance state."}
- AgreementList: {"properties":{"documents":{"items":{"$ref":"#/components/schemas/AgreementDocument"},"type":"array","title":"Documents"}},"type":"object","required":["documents"],"title":"AgreementList"}
- AiStep: {"properties":{"type":{"type":"string","const":"ai","title":"Type","default":"ai"},"agent_id":{"type":"string","maxLength":128,"minLength":1,"title":"Agent Id"},"location_ref":{"anyOf":[{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},{"type":"null"}],"title":"Location Ref"}},"additionalProperties":false,"type":"object","required":["agent_id"],"title":"AiStep","description":"Hand the caller to one of this tenant's published agents.\n\nThe agent is named per step rather than by ``phone_numbers.inbound_agent_id`` so a\nsingle number can route sales and support to different agents without widening a\ncolumn three call paths already read as \"the one inbound agent\"."}
- AnalysisDimensionResult: {"properties":{"result":{"type":"string","enum":["pass","fail","not_applicable","unscored"],"title":"Result"},"basis":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Basis"},"score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Score"},"claims_checked":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Claims Checked"}},"type":"object","required":["result"],"title":"AnalysisDimensionResult","description":"One scored dimension inside `analysis.success_evaluation` (docs/04-api.md Step 8a).\n\n`not_applicable` means the call gave the dimension nothing to judge (no opt-out was\nsignalled, no pack is pinned); `unscored` means it could not be evaluated - reachable\nonly for a judge dimension, never for the compliance floor. `claims_checked` is\npopulated on `pack_grounded` alone."}
- AnalysisDimensions: {"properties":{"goal_completion":{"$ref":"#/components/schemas/AnalysisDimensionResult"},"disclosure_given":{"$ref":"#/components/schemas/AnalysisDimensionResult"},"optout_honored":{"$ref":"#/components/schemas/AnalysisDimensionResult"},"pack_grounded":{"$ref":"#/components/schemas/AnalysisDimensionResult"},"latency":{"$ref":"#/components/schemas/AnalysisLatency"}},"type":"object","required":["goal_completion","disclosure_given","optout_honored","pack_grounded","latency"],"title":"AnalysisDimensions","description":"The per-dimension P1 verdicts (docs/08-ai-quality.md §Production auto-scoring).\n\nThe floor dimensions - `disclosure_given` and `optout_honored` - appear on every call\nand are never waivable; no field here is optional because the worker scores all of\nthem or writes nothing."}
- AnalysisLatency: {"properties":{"turns_measured":{"type":"integer","title":"Turns Measured","default":0},"voice_to_voice_p50_ms":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Voice To Voice P50 Ms"},"voice_to_voice_p95_ms":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Voice To Voice P95 Ms"},"llm_ttft_p50_ms":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Llm Ttft P50 Ms"},"llm_ttft_p95_ms":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Llm Ttft P95 Ms"}},"type":"object","title":"AnalysisLatency","description":"Per-call latency against the voice SLO, from the persisted turn ledger."}
- AnsweringPolicy: {"properties":{"id":{"type":"string","title":"Id"},"phone_number_id":{"type":"string","title":"Phone Number Id"},"timezone":{"type":"string","title":"Timezone"},"business_hours":{"additionalProperties":{"items":{"$ref":"#/components/schemas/CallingWindow"},"type":"array"},"propertyNames":{"enum":["mon","tue","wed","thu","fri","sat","sun"]},"type":"object","title":"Business Hours"},"holiday_dates":{"items":{"type":"string","format":"date"},"type":"array","title":"Holiday Dates"},"open_steps":{"items":{"oneOf":[{"$ref":"#/components/schemas/MenuStep-Output"},{"$ref":"#/components/schemas/RingHumansStep"},{"$ref":"#/components/schemas/AiStep"},{"$ref":"#/components/schemas/VoicemailStep"},{"$ref":"#/components/schemas/SayStep"},{"$ref":"#/components/schemas/ForwardStep"}],"discriminator":{"propertyName":"type","mapping":{"ai":"#/components/schemas/AiStep","forward":"#/components/schemas/ForwardStep","menu":"#/components/schemas/MenuStep-Output","ring_humans":"#/components/schemas/RingHumansStep","say":"#/components/schemas/SayStep","voicemail":"#/components/schemas/VoicemailStep"}}},"type":"array","title":"Open Steps"},"closed_steps":{"items":{"oneOf":[{"$ref":"#/components/schemas/MenuStep-Output"},{"$ref":"#/components/schemas/RingHumansStep"},{"$ref":"#/components/schemas/AiStep"},{"$ref":"#/components/schemas/VoicemailStep"},{"$ref":"#/components/schemas/SayStep"},{"$ref":"#/components/schemas/ForwardStep"}],"discriminator":{"propertyName":"type","mapping":{"ai":"#/components/schemas/AiStep","forward":"#/components/schemas/ForwardStep","menu":"#/components/schemas/MenuStep-Output","ring_humans":"#/components/schemas/RingHumansStep","say":"#/components/schemas/SayStep","voicemail":"#/components/schemas/VoicemailStep"}}},"type":"array","title":"Closed Steps"},"version":{"type":"integer","title":"Version"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","phone_number_id","timezone","open_steps","version","created_at","updated_at"],"title":"AnsweringPolicy","description":"One number's stored answering document, as its tenant reads it back.\n\n``version`` is server-owned and bumped on every write. It is not an optimistic-locking\ntoken for this endpoint: it exists so a signed callback URL can carry the exact document\nrevision it was minted against, and a caller walking a policy that was edited mid-call\nis detected rather than silently re-routed."}
- AnsweringPolicySchedule: {"properties":{"policy_id":{"type":"string","title":"Policy Id"},"phone_number_id":{"type":"string","title":"Phone Number Id"},"timezone":{"type":"string","title":"Timezone"},"business_hours":{"additionalProperties":{"items":{"$ref":"#/components/schemas/CallingWindow"},"type":"array"},"propertyNames":{"enum":["mon","tue","wed","thu","fri","sat","sun"]},"type":"object","title":"Business Hours"},"holiday_dates":{"items":{"type":"string","format":"date"},"type":"array","title":"Holiday Dates"},"version":{"type":"integer","title":"Version"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["policy_id","phone_number_id","timezone","version","updated_at"],"title":"AnsweringPolicySchedule","description":"The independently editable calendar portion of an answering policy."}
- AnsweringPolicySchedulePatchRequest: {"properties":{"expected_version":{"type":"integer","minimum":1,"title":"Expected Version"},"business_hours":{"anyOf":[{"additionalProperties":{"items":{"$ref":"#/components/schemas/CallingWindow"},"type":"array","maxItems":6},"propertyNames":{"enum":["mon","tue","wed","thu","fri","sat","sun"]},"type":"object"},{"type":"null"}],"title":"Business Hours"},"holiday_dates":{"anyOf":[{"items":{"type":"string","format":"date"},"type":"array","maxItems":60},{"type":"null"}],"title":"Holiday Dates"}},"additionalProperties":false,"type":"object","required":["expected_version"],"title":"AnsweringPolicySchedulePatchRequest","description":"Compare-and-swap patch of only a policy's business calendar.\n\nThe policy's route tree is deliberately absent. A mobile calendar edit cannot race a\nweb route-tree edit into a half-valid document because the request carries the version\nit read and the repository updates only when that exact version is still current."}
- AnsweringPolicyUpsertRequest: {"properties":{"timezone":{"type":"string","maxLength":64,"minLength":1,"title":"Timezone"},"business_hours":{"additionalProperties":{"items":{"$ref":"#/components/schemas/CallingWindow"},"type":"array","maxItems":6},"propertyNames":{"enum":["mon","tue","wed","thu","fri","sat","sun"]},"type":"object","title":"Business Hours"},"holiday_dates":{"items":{"type":"string","format":"date"},"type":"array","maxItems":60,"title":"Holiday Dates"},"open_steps":{"items":{"oneOf":[{"$ref":"#/components/schemas/MenuStep-Input"},{"$ref":"#/components/schemas/RingHumansStep"},{"$ref":"#/components/schemas/AiStep"},{"$ref":"#/components/schemas/VoicemailStep"},{"$ref":"#/components/schemas/SayStep"},{"$ref":"#/components/schemas/ForwardStep"}],"discriminator":{"propertyName":"type","mapping":{"ai":"#/components/schemas/AiStep","forward":"#/components/schemas/ForwardStep","menu":"#/components/schemas/MenuStep-Input","ring_humans":"#/components/schemas/RingHumansStep","say":"#/components/schemas/SayStep","voicemail":"#/components/schemas/VoicemailStep"}}},"type":"array","maxItems":60,"minItems":1,"title":"Open Steps"},"closed_steps":{"items":{"oneOf":[{"$ref":"#/components/schemas/MenuStep-Input"},{"$ref":"#/components/schemas/RingHumansStep"},{"$ref":"#/components/schemas/AiStep"},{"$ref":"#/components/schemas/VoicemailStep"},{"$ref":"#/components/schemas/SayStep"},{"$ref":"#/components/schemas/ForwardStep"}],"discriminator":{"propertyName":"type","mapping":{"ai":"#/components/schemas/AiStep","forward":"#/components/schemas/ForwardStep","menu":"#/components/schemas/MenuStep-Input","ring_humans":"#/components/schemas/RingHumansStep","say":"#/components/schemas/SayStep","voicemail":"#/components/schemas/VoicemailStep"}}},"type":"array","maxItems":60,"title":"Closed Steps"}},"additionalProperties":false,"type":"object","required":["timezone","open_steps"],"title":"AnsweringPolicyUpsertRequest","description":"The whole answering document for one number, written in a single call.\n\nWhole-document rather than patch: the steps form a tree whose branches reference each\nother by position, and a partial write is the shape in which a menu keeps pointing at a\nbranch the same request deleted.\n\n``business_hours`` is the tenant's own calendar in the tenant's own zone, and it shares\nonly the *vocabulary* of `campaign_schedules.calling_windows`, never its meaning: the\ncompliance layer's quiet hours are keyed to the callee's jurisdiction and are untouched\nhere. An empty map means the number is answered by ``open_steps`` at every hour; naming\nany day at all makes every unnamed day closed, which is the same load-bearing\nempty-versus-absent distinction `CallingSchedule.windows_for` carries."}
- ApiContactConsentEvidence: {"properties":{"artifact_type":{"type":"string","maxLength":100,"minLength":1,"title":"Artifact Type"},"artifact_ref":{"type":"string","maxLength":2000,"minLength":1,"title":"Artifact Ref"},"content_hash":{"type":"string","pattern":"^[0-9a-f]{64}$","title":"Content Hash"}},"additionalProperties":false,"type":"object","required":["artifact_type","artifact_ref","content_hash"],"title":"ApiContactConsentEvidence","description":"The immutable customer-side artifact behind one API-created contact's consent.\n\nA standing API-source attestation says the tenant may create contacts through this\nsurface; it does not replace evidence for an individual person. The server links the\nresulting ConsentRecord to that standing attestation itself, so callers cannot name or\nsubstitute an attestation id here."}
- ApiContactConsentInput: {"properties":{"consent_basis":{"$ref":"#/components/schemas/ConsentBasis"},"captured_at":{"type":"string","format":"date-time","title":"Captured At"},"scope":{"additionalProperties":true,"type":"object","minProperties":1,"title":"Scope"},"evidence":{"$ref":"#/components/schemas/ApiContactConsentEvidence"},"disclosure_text_shown":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Disclosure Text Shown"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"}},"additionalProperties":false,"type":"object","required":["consent_basis","captured_at","scope","evidence"],"title":"ApiContactConsentInput","description":"Consent that must land atomically with a headless contact create.\n\n``scope`` is deliberately explicit rather than defaulting to ``{}``: an empty scope\nwould make a contact look broadly consented while saying nothing about which calls or\nchannels the customer captured permission for. The policy engine remains the\nauthority for whether that stored scope covers a later call purpose."}
- ApiKeyCreateRequest: {"properties":{"mode":{"type":"string","enum":["live","test"],"title":"Mode","default":"test"},"scopes":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Scopes"},"name":{"anyOf":[{"type":"string","maxLength":120,"minLength":1},{"type":"null"}],"title":"Name"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"}},"type":"object","required":["scopes"],"title":"ApiKeyCreateRequest"}
- ApiKeyCreateResponse: {"properties":{"id":{"type":"string","title":"Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"display_prefix":{"type":"string","title":"Display Prefix"},"mode":{"type":"string","enum":["live","test"],"title":"Mode"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"revoked_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Revoked At"},"secret":{"type":"string","title":"Secret"}},"type":"object","required":["id","display_prefix","mode","scopes","created_at","secret"],"title":"ApiKeyCreateResponse","description":"Returned once at creation - `secret` is unrecoverable afterwards."}
- ApiKeyRotateRequest: {"properties":{"name":{"anyOf":[{"type":"string","maxLength":120,"minLength":1},{"type":"null"}],"title":"Name"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"}},"type":"object","title":"ApiKeyRotateRequest","description":"Replace one key with another carrying the same authority.\n\nMode and scopes are deliberately **not** settable here: rotation replaces a\ncredential, it does not re-grant one. Widening on rotate would route around the\nscope-escalation refusal `POST /v1/api-keys` makes."}
- ApiKeyRotateResponse: {"properties":{"id":{"type":"string","title":"Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"display_prefix":{"type":"string","title":"Display Prefix"},"mode":{"type":"string","enum":["live","test"],"title":"Mode"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"revoked_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Revoked At"},"secret":{"type":"string","title":"Secret"},"replaced_api_key_id":{"type":"string","title":"Replaced Api Key Id"}},"type":"object","required":["id","display_prefix","mode","scopes","created_at","secret","replaced_api_key_id"],"title":"ApiKeyRotateResponse","description":"The replacement key, plus the id of the credential it retired.\n\nBoth halves in one body because they happen in one transaction: a client that\nstored the new secret and never learned which key stopped working would have to\ndiff a list to find out."}
- ApiKeySummary: {"properties":{"id":{"type":"string","title":"Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"display_prefix":{"type":"string","title":"Display Prefix"},"mode":{"type":"string","enum":["live","test"],"title":"Mode"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"revoked_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Revoked At"}},"type":"object","required":["id","display_prefix","mode","scopes","created_at"],"title":"ApiKeySummary","description":"A key as its owning tenant sees it. The secret is never present."}
- Appointment: {"properties":{"id":{"type":"string","title":"Id"},"call_id":{"type":"string","title":"Call Id"},"contact_id":{"type":"string","title":"Contact Id"},"integration_connection_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Integration Connection Id"},"external_event_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Event Id"},"starts_at":{"type":"string","format":"date-time","title":"Starts At"},"ends_at":{"type":"string","format":"date-time","title":"Ends At"},"contact_timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Timezone"},"attendees":{"items":{},"type":"array","title":"Attendees"},"status":{"type":"string","enum":["booked","rescheduled","cancelled","completed","no_show"],"title":"Status"},"invite_sent_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Invite Sent At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","call_id","contact_id","starts_at","ends_at","status","created_at","updated_at"],"title":"Appointment"}
- AppointmentOutcome: {"properties":{"kind":{"type":"string","const":"appointment","title":"Kind","default":"appointment"},"id":{"type":"string","title":"Id"},"status":{"type":"string","enum":["booked","rescheduled","cancelled","completed","no_show"],"title":"Status"},"starts_at":{"type":"string","format":"date-time","title":"Starts At"},"ends_at":{"type":"string","format":"date-time","title":"Ends At"},"contact_timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Timezone"}},"type":"object","required":["id","status","starts_at","ends_at"],"title":"AppointmentOutcome"}
- AttemptState: {"type":"string","enum":["pending","scrubbed","scheduled","dialing","answered","in_conversation","completed","machine","no_answer","busy","failed","disposed"],"title":"AttemptState"}
- AttemptSummary: {"properties":{"id":{"type":"string","title":"Id"},"contact_id":{"type":"string","title":"Contact Id"},"contact_phone":{"type":"string","title":"Contact Phone"},"attempt_number":{"type":"integer","title":"Attempt Number"},"state":{"$ref":"#/components/schemas/AttemptState"},"disposition":{"anyOf":[{"$ref":"#/components/schemas/Disposition"},{"type":"null"}]},"sub_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sub Code"},"failure_reason":{"anyOf":[{"$ref":"#/components/schemas/FailureReason"},{"type":"null"}]},"call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Id"},"scheduled_for":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Scheduled For"},"dialed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Dialed At"},"ended_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ended At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","contact_id","contact_phone","attempt_number","state","updated_at"],"title":"AttemptSummary","description":"One `ContactAttempt` row of the fill-status board."}
- AttributionPayload: {"properties":{"utm_source":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Utm Source"},"utm_medium":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Utm Medium"},"utm_campaign":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Utm Campaign"},"utm_content":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Utm Content"},"utm_term":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Utm Term"},"gclid":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Gclid"},"msclkid":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Msclkid"},"ref":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Ref"},"landing_path":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Landing Path"},"referrer_host":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Referrer Host"},"first_seen_at":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"First Seen At"}},"additionalProperties":false,"type":"object","title":"AttributionPayload","description":"The published shape of an attribution blob.\n\nSpelled out field by field rather than as a ``dict[str, str]`` for one reason: this\nmodel is reachable from the consumer OpenAPI document through the onboarding step\nrequest, and a published ``additionalProperties`` map would tell an integrator that\nany key is accepted, which is exactly what the allow-list refuses. ``extra=\"forbid\"``\nmakes an off-list key a 422 on that route - the marketing endpoints, which take the\nsame values from a browser we do not control, use :func:`normalize_attribution`\ninstead and drop rather than refuse."}
- AuthCredentials: {"properties":{"email":{"type":"string","maxLength":254,"minLength":3,"title":"Email"},"password":{"type":"string","maxLength":1024,"minLength":8,"title":"Password"}},"type":"object","required":["email","password"],"title":"AuthCredentials"}
- AuthProviders: {"properties":{"google_client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Google Client Id"}},"type":"object","title":"AuthProviders","description":"Which optional sign-in providers this deployment has wired.\n\nPublic identifiers only - the Google client id travels in every GIS widget the\nbrowser renders, so exposing it here leaks nothing and lets the portal discover\navailability at runtime instead of baking it into a build."}
- AutoFundActiveOperation: {"properties":{"id":{"type":"string","title":"Id"},"status":{"type":"string","title":"Status"},"amount_minor":{"type":"integer","title":"Amount Minor"},"provider_requested_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Provider Requested At"},"payment_intent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payment Intent Id"},"failure_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failure Code"}},"type":"object","required":["id","status","amount_minor","provider_requested_at","payment_intent_id","failure_code"],"title":"AutoFundActiveOperation"}
- AutoFundCadenceOption: {"properties":{"cadence":{"type":"string","enum":["as_needed","at_most_once_per_24h","at_most_once_per_7d"],"title":"Cadence"},"minimum_charge_interval_seconds":{"type":"integer","minimum":0,"title":"Minimum Charge Interval Seconds"}},"type":"object","required":["cadence","minimum_charge_interval_seconds"],"title":"AutoFundCadenceOption","description":"One server-supported automatic-funding cadence choice.\n\n``minimum_charge_interval_seconds`` is an elapsed UTC duration measured from the prior\nsubmitted provider request. It is not a scheduled future charge time."}
- AutoFundConfigRequest: {"properties":{"enabled":{"type":"boolean","title":"Enabled"},"threshold_minor":{"anyOf":[{"type":"integer","minimum":0},{"type":"null"}],"title":"Threshold Minor"},"amount_minor":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"title":"Amount Minor"},"period_cap_minor":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"title":"Period Cap Minor"},"period":{"type":"string","const":"calendar_month","title":"Period","default":"calendar_month"},"cadence":{"type":"string","enum":["as_needed","at_most_once_per_24h","at_most_once_per_7d"],"title":"Cadence","default":"as_needed"},"mandate_accepted":{"anyOf":[{"type":"boolean","const":true},{"type":"null"}],"title":"Mandate Accepted"},"idempotency_key":{"anyOf":[{"type":"string","maxLength":160,"minLength":8},{"type":"null"}],"title":"Idempotency Key"}},"type":"object","required":["enabled"],"title":"AutoFundConfigRequest"}
- AutoFundResponse: {"properties":{"available":{"type":"boolean","title":"Available"},"funding_min_minor":{"type":"integer","minimum":1,"title":"Funding Min Minor"},"funding_max_minor":{"type":"integer","minimum":1,"title":"Funding Max Minor"},"period_cap_max_minor":{"type":"integer","minimum":1,"title":"Period Cap Max Minor"},"enabled":{"type":"boolean","title":"Enabled"},"status":{"type":"string","title":"Status"},"currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Currency"},"threshold_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Threshold Minor"},"amount_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Amount Minor"},"period_cap_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Period Cap Minor"},"period":{"type":"string","const":"calendar_month","title":"Period","default":"calendar_month"},"period_used_minor":{"type":"integer","title":"Period Used Minor","default":0},"period_pending_minor":{"type":"integer","title":"Period Pending Minor","default":0},"period_remaining_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Period Remaining Minor"},"setup":{"anyOf":[{"$ref":"#/components/schemas/AutoFundSetup"},{"type":"null"}]},"active_operation":{"anyOf":[{"$ref":"#/components/schemas/AutoFundActiveOperation"},{"type":"null"}]},"last_failure_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Failure Code"},"applies_to":{"type":"string","const":"managed_verified_number_test_calls_only","title":"Applies To","default":"managed_verified_number_test_calls_only"},"byo_campaigns_chargeable":{"type":"boolean","const":false,"title":"Byo Campaigns Chargeable","default":false},"schedule":{"type":"string","const":"server_threshold_polling","title":"Schedule","default":"server_threshold_polling"},"cadence":{"type":"string","enum":["as_needed","at_most_once_per_24h","at_most_once_per_7d"],"title":"Cadence","default":"as_needed"},"minimum_charge_interval_seconds":{"type":"integer","minimum":0,"title":"Minimum Charge Interval Seconds","default":0},"cadence_options":{"items":{"$ref":"#/components/schemas/AutoFundCadenceOption"},"type":"array","title":"Cadence Options"},"cadence_hold_until":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Cadence Hold Until"},"accepted_mandate_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Accepted Mandate Version"},"accepted_mandate_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Accepted Mandate Text"},"mandate_version":{"type":"string","title":"Mandate Version","default":"2026-08-12-cadence-v2"},"mandate_text":{"type":"string","title":"Mandate Text","default":"I authorize Vocapable to charge the payment method I set up for future automatic prepaid managed verified-number test-call credit when my eligible prepaid balance is at or below my selected threshold, for my selected amount, up to my selected calendar-month cap and no more often than my selected cadence measured from the prior submitted charge attempt. Vocapable does not promise a clock time or catch up a missed attempt. I can disable this at any time before a charge is submitted."}},"type":"object","required":["available","funding_min_minor","funding_max_minor","period_cap_max_minor","enabled","status","currency","threshold_minor","amount_minor","period_cap_minor","period_remaining_minor"],"title":"AutoFundResponse"}
- AutoFundSetup: {"properties":{"id":{"type":"string","title":"Id"},"status":{"type":"string","title":"Status"},"checkout_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Checkout Url"},"pending":{"type":"boolean","title":"Pending","default":false}},"type":"object","required":["id","status"],"title":"AutoFundSetup"}
- BillingPeriodEstimate: {"properties":{"period_start":{"type":"string","format":"date-time","title":"Period Start"},"period_end":{"type":"string","format":"date-time","title":"Period End"},"base_price_minor":{"type":"integer","title":"Base Price Minor"},"usage_amount_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Usage Amount Minor"},"total_amount_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Amount Minor"},"minutes_amount_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Minutes Amount Minor"},"call_fees_amount_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Call Fees Amount Minor"},"volume_tier":{"anyOf":[{"$ref":"#/components/schemas/VolumeTierStatus"},{"type":"null"}]},"unpriced_meters":{"items":{"type":"string","enum":["telephony_minutes","stt_audio_seconds","tts_characters","llm_prompt_tokens","llm_cached_prompt_tokens","llm_completion_tokens","gpu_seconds","sms_segments","lookup_requests"]},"type":"array","title":"Unpriced Meters"},"excluded_usage_types":{"items":{"type":"string","enum":["byo_carrier_usage","separately_settled_prepaid_test_calls"]},"type":"array","title":"Excluded Usage Types"}},"type":"object","required":["period_start","period_end","base_price_minor"],"title":"BillingPeriodEstimate","description":"A live, read-only estimate for the subscription's actual billing period.\n\nStripe remains the source of truth for the final invoice. This value is calculated only\nfrom the current plan and usage ledger; it excludes tax, credits, discounts, proration,\nand any Stripe-side adjustment."}
- Budget: {"properties":{"currency":{"type":"string","title":"Currency"},"balance_minor":{"type":"integer","title":"Balance Minor"},"spend_cap_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Spend Cap Minor"},"spend_cap_period":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Spend Cap Period"},"low_balance_threshold_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Low Balance Threshold Minor"},"period_spend_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Period Spend Minor"},"unpriced_meters":{"items":{"type":"string"},"type":"array","title":"Unpriced Meters"}},"type":"object","required":["currency","balance_minor","spend_cap_minor","spend_cap_period","low_balance_threshold_minor","period_spend_minor","unpriced_meters"],"title":"Budget","description":"The tenant's budget as stored, plus what it has spent against it so far."}
- BudgetRequest: {"properties":{"spend_cap_minor":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"title":"Spend Cap Minor"},"spend_cap_period":{"anyOf":[{"type":"string","pattern":"^(day|month)$"},{"type":"null"}],"title":"Spend Cap Period"},"low_balance_threshold_minor":{"anyOf":[{"type":"integer","minimum":0},{"type":"null"}],"title":"Low Balance Threshold Minor"}},"type":"object","title":"BudgetRequest","description":"`PUT /v1/billing/budget`. Absent or `null` clears the field it names."}
- BusinessAnswers: {"properties":{"legal_name":{"type":"string","maxLength":200,"minLength":1,"title":"Legal Name"},"website":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Website"},"business_phone":{"type":"string","maxLength":20,"minLength":7,"title":"Business Phone"},"signer_relationship":{"type":"string","enum":["owner","officer","employee_authorized","employee_unauthorized","agency","other"],"title":"Signer Relationship"},"signer_title":{"type":"string","maxLength":120,"minLength":1,"title":"Signer Title"},"business_size":{"type":"string","enum":["1","2-10","11-50","51-200","201-1000","1000+"],"title":"Business Size"}},"additionalProperties":false,"type":"object","required":["legal_name","website","business_phone","signer_relationship","signer_title","business_size"],"title":"BusinessAnswers","description":"Step 2. Everything here is contact data as much as it is qualification data - it is\nwhat an abandoned-signup follow-up has to work with."}
- CalendarAvailability: {"properties":{"connection_id":{"type":"string","title":"Connection Id"},"timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timezone"},"slots":{"items":{"$ref":"#/components/schemas/CalendarSlot"},"type":"array","title":"Slots"}},"type":"object","required":["connection_id"],"title":"CalendarAvailability"}
- CalendarAvailabilityRequest: {"properties":{"start_date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","title":"Start Date"},"end_date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","title":"End Date"},"duration_minutes":{"type":"integer","maximum":240,"minimum":5,"title":"Duration Minutes"},"timezone":{"anyOf":[{"type":"string","maxLength":64,"minLength":1},{"type":"null"}],"title":"Timezone"}},"additionalProperties":false,"type":"object","required":["start_date","end_date","duration_minutes"],"title":"CalendarAvailabilityRequest","description":"`check_calendar`'s backing request - the tool's argument schema, verbatim."}
- CalendarEventCreateRequest: {"properties":{"call_id":{"type":"string","title":"Call Id"},"contact_id":{"type":"string","title":"Contact Id"},"start_time":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(:\\d{2})?(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$","title":"Start Time"},"duration_minutes":{"type":"integer","maximum":240,"minimum":5,"title":"Duration Minutes"},"attendee_email":{"type":"string","maxLength":320,"pattern":"^[^@\\s]+@[^@\\s.]+\\.[^@\\s]+$","title":"Attendee Email"},"attendee_name":{"anyOf":[{"type":"string","maxLength":200,"minLength":1},{"type":"null"}],"title":"Attendee Name"},"notes":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Notes"},"contact_timezone":{"anyOf":[{"type":"string","maxLength":64,"minLength":1},{"type":"null"}],"title":"Contact Timezone"}},"additionalProperties":false,"type":"object","required":["call_id","contact_id","start_time","duration_minutes","attendee_email"],"title":"CalendarEventCreateRequest","description":"`book_meeting`'s backing request: the tool arguments plus their call context.\n\n`call_id` and `contact_id` are supplied by the control-plane tool-execution path, never\nby the model; the remaining fields carry exactly the frozen tool schema's constraints."}
- CalendarSlot: {"properties":{"starts_at":{"type":"string","format":"date-time","title":"Starts At"},"ends_at":{"type":"string","format":"date-time","title":"Ends At"}},"type":"object","required":["starts_at","ends_at"],"title":"CalendarSlot"}
- Call: {"properties":{"id":{"type":"string","title":"Id"},"direction":{"$ref":"#/components/schemas/CallDirection"},"state":{"$ref":"#/components/schemas/CallState"},"campaign_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Id"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"contact_first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact First Name"},"contact_last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Last Name"},"contact_company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Company"},"counterparty_e164":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Counterparty E164"},"dialed_e164":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dialed E164"},"contact_attempt_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Attempt Id"},"agent_version_id":{"type":"string","title":"Agent Version Id"},"agent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Id"},"agent_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Name"},"use_case":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Use Case"},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"},"captured_fields":{"additionalProperties":true,"type":"object","title":"Captured Fields"},"recorded_responses":{"items":{"$ref":"#/components/schemas/RecordedCallResponse"},"type":"array","title":"Recorded Responses"},"recorded_response_count":{"type":"integer","title":"Recorded Response Count","default":0},"started_at":{"type":"string","format":"date-time","title":"Started At"},"ended_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ended At"},"duration_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration Ms"},"end_reason":{"anyOf":[{"$ref":"#/components/schemas/CallEndReason"},{"type":"null"}]},"disposition":{"anyOf":[{"$ref":"#/components/schemas/Disposition"},{"type":"null"}]},"sub_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sub Code"},"recording_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recording Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"gpu_node_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gpu Node Id"},"llm_provider_used":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Llm Provider Used"},"cost":{"additionalProperties":true,"type":"object","title":"Cost"},"disposition_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Disposition Id"},"disposition_notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Disposition Notes"},"disposition_set_by":{"anyOf":[{"type":"string","enum":["agent_llm","human_review"]},{"type":"null"}],"title":"Disposition Set By"},"structured_outcome":{"additionalProperties":true,"type":"object","title":"Structured Outcome"},"conversation_actions":{"items":{"$ref":"#/components/schemas/ConversationAction"},"type":"array","title":"Conversation Actions"},"qualification_reviews":{"additionalProperties":{"$ref":"#/components/schemas/QualificationReviewDetails"},"type":"object","title":"Qualification Reviews"},"analysis":{"anyOf":[{"$ref":"#/components/schemas/CallAnalysis"},{"type":"null"}]},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","direction","state","agent_version_id","started_at","created_at","updated_at"],"title":"Call"}
- CallAnalysis: {"properties":{"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"},"success_evaluation":{"$ref":"#/components/schemas/SuccessEvaluation"},"structured_data":{"additionalProperties":true,"type":"object","title":"Structured Data"}},"type":"object","required":["success_evaluation"],"title":"CallAnalysis","description":"The customer-facing post-call analysis on the call detail (docs/04-api.md Step 8a)."}
- CallBilling: {"properties":{"call_id":{"type":"string","title":"Call Id"},"completed":{"type":"boolean","title":"Completed"},"meters":{"items":{"$ref":"#/components/schemas/CallBillingMeter"},"type":"array","title":"Meters"},"rating_status":{"type":"string","enum":["no_usage_recorded","unrated","partially_rated","rated","prepaid_settled"],"title":"Rating Status"},"rated_usage_amount_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rated Usage Amount Minor"},"currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Currency"},"billing_period_start":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Billing Period Start"},"billing_period_end":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Billing Period End"},"projection_status":{"type":"string","enum":["not_available","unpriced","estimated"],"title":"Projection Status"},"projected_marginal_usage_amount_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Projected Marginal Usage Amount Minor"},"projection_unpriced_meters":{"items":{"type":"string","enum":["telephony_minutes","stt_audio_seconds","tts_characters","llm_prompt_tokens","llm_cached_prompt_tokens","llm_completion_tokens","gpu_seconds","sms_segments","lookup_requests"]},"type":"array","title":"Projection Unpriced Meters"},"prepaid_authorization_status":{"type":"string","enum":["not_applicable","reserved","captured","released"],"title":"Prepaid Authorization Status","default":"not_applicable"},"prepaid_reserved_amount_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Prepaid Reserved Amount Minor"},"prepaid_captured_amount_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Prepaid Captured Amount Minor"},"live_projection":{"anyOf":[{"$ref":"#/components/schemas/LiveCallBillingProjection"},{"type":"null"}]}},"type":"object","required":["call_id","completed","rating_status","projection_status"],"title":"CallBilling","description":"Ledger-backed usage and a deliberately limited estimate for one call.\n\n``rated_usage_amount_minor`` is never an inferred cost: it stays null until every\nlinked ledger event has a final Stripe allocation. The projection is instead the\n*current-period marginal* plan/ledger estimate - the difference between the current\nbilling-period estimate with this call's rows and without them. It excludes the base\nsubscription fee, tax, credits, discounts, proration, and Stripe-side adjustments; the\nStripe invoice remains authoritative."}
- CallBillingMeter: {"properties":{"meter":{"type":"string","enum":["telephony_minutes","stt_audio_seconds","tts_characters","llm_prompt_tokens","llm_cached_prompt_tokens","llm_completion_tokens","gpu_seconds","sms_segments","lookup_requests"],"title":"Meter"},"quantity":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Quantity"},"event_count":{"type":"integer","title":"Event Count"},"rated_amount_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rated Amount Minor"},"unrated_event_count":{"type":"integer","title":"Unrated Event Count"},"prepaid_event_count":{"type":"integer","title":"Prepaid Event Count","default":0}},"type":"object","required":["meter","quantity","event_count","unrated_event_count"],"title":"CallBillingMeter","description":"The exact ledger quantity currently attributable to one finished call.\n\nThe list is intentionally sparse. An absent ``telephony_minutes`` row does not mean\nthe carrier charged zero: carrier usage arrives through an asynchronous status callback\nand can appear after the call teardown. ``rated_amount_minor`` is non-null only once\nevery event for this meter has a final Stripe per-call allocation."}
- CallDirection: {"type":"string","enum":["outbound","inbound","browser"],"title":"CallDirection"}
- CallEndReason: {"type":"string","enum":["agent_completed","contact_hangup","opt_out","error","max_duration"],"title":"CallEndReason"}
- CallQuestion: {"properties":{"id":{"type":"string","maxLength":64,"minLength":1,"pattern":"^[A-Za-z0-9_.:\\-]+$","title":"Id"},"prompt":{"type":"string","maxLength":500,"minLength":1,"title":"Prompt"},"scoring":{"anyOf":[{"$ref":"#/components/schemas/ScoringBlock"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["id","prompt"],"title":"CallQuestion","description":"One per-call question - the strict profile of a version objective (docs/04 §3).\n\nUnlike `Objective` (open - goal graphs are legal on an agent), a per-call question\nis closed and fully required: it exists to be asked and recorded on THIS call, so\nan id and question text are not optional and unknown keys are typos, not shapes."}
- CallState: {"type":"string","enum":["initiated","ringing","in_progress","completed","no_answer","busy","failed"],"title":"CallState"}
- CallSummary: {"properties":{"id":{"type":"string","title":"Id"},"direction":{"$ref":"#/components/schemas/CallDirection"},"state":{"$ref":"#/components/schemas/CallState"},"campaign_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Id"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"contact_first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact First Name"},"contact_last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Last Name"},"contact_company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Company"},"counterparty_e164":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Counterparty E164"},"dialed_e164":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dialed E164"},"contact_attempt_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Attempt Id"},"agent_version_id":{"type":"string","title":"Agent Version Id"},"agent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Id"},"agent_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Name"},"use_case":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Use Case"},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"},"captured_fields":{"additionalProperties":true,"type":"object","title":"Captured Fields"},"recorded_responses":{"items":{"$ref":"#/components/schemas/RecordedCallResponse"},"type":"array","title":"Recorded Responses"},"recorded_response_count":{"type":"integer","title":"Recorded Response Count","default":0},"started_at":{"type":"string","format":"date-time","title":"Started At"},"ended_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ended At"},"duration_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration Ms"},"end_reason":{"anyOf":[{"$ref":"#/components/schemas/CallEndReason"},{"type":"null"}]},"disposition":{"anyOf":[{"$ref":"#/components/schemas/Disposition"},{"type":"null"}]},"sub_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sub Code"},"recording_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recording Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","direction","state","agent_version_id","started_at","created_at"],"title":"CallSummary","description":"A call as a list endpoint returns it, with its current disposition flattened on."}
- CallTranscript: {"properties":{"id":{"type":"string","title":"Id"},"call_id":{"type":"string","title":"Call Id"},"full_text":{"type":"string","title":"Full Text"},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"},"turns":{"items":{"$ref":"#/components/schemas/CallTurn"},"type":"array","title":"Turns"},"turns_truncated":{"type":"boolean","title":"Turns Truncated","default":false},"extracted_entities":{"additionalProperties":true,"type":"object","title":"Extracted Entities"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"},"redaction_status":{"type":"string","enum":["raw","pii_redacted"],"title":"Redaction Status"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","call_id","full_text","redaction_status","created_at","updated_at"],"title":"CallTranscript","description":"The materialized read model over a call's turns."}
- CallTurn: {"properties":{"id":{"type":"string","title":"Id"},"seq":{"type":"integer","title":"Seq"},"speaker":{"type":"string","enum":["agent","contact"],"title":"Speaker"},"text":{"type":"string","title":"Text"},"started_at_ms":{"type":"integer","title":"Started At Ms"},"ended_at_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ended At Ms"},"interrupted":{"type":"boolean","title":"Interrupted","default":false},"revision":{"type":"integer","title":"Revision","default":0},"llm_ttft_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Llm Ttft Ms"},"voice_to_voice_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Voice To Voice Ms"},"tool_calls":{"items":{},"type":"array","title":"Tool Calls"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","seq","speaker","text","started_at_ms","created_at"],"title":"CallTurn","description":"One utterance. Offsets are call-relative milliseconds, never wall clock."}
- CallerRequestEventOut: {"properties":{"id":{"type":"string","title":"Id"},"action":{"type":"string","enum":["opened","acknowledged","resolved","assigned"],"title":"Action"},"actor_ref":{"type":"string","title":"Actor Ref"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","action","actor_ref","created_at"],"title":"CallerRequestEventOut","description":"One append-only transition on a request's trail."}
- CallerRequestResource: {"properties":{"id":{"type":"string","title":"Id"},"kind":{"type":"string","enum":["message","support_ticket"],"title":"Kind"},"state":{"type":"string","enum":["open","acknowledged","resolved"],"title":"State"},"severity":{"anyOf":[{"type":"string","enum":["critical","high","normal","low"]},{"type":"null"}],"title":"Severity"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Id"},"agent_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Version Id"},"phone_number_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number Id"},"caller_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Caller Name"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"body":{"type":"string","title":"Body"},"assignee_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Assignee User Id"},"acknowledged_by_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Acknowledged By Ref"},"acknowledged_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Acknowledged At"},"resolved_by_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolved By Ref"},"resolved_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Resolved At"},"version":{"type":"integer","title":"Version"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"events":{"items":{"$ref":"#/components/schemas/CallerRequestEventOut"},"type":"array","title":"Events"}},"type":"object","required":["id","kind","state","body","version","created_at","updated_at"],"title":"CallerRequestResource","description":"The single-request read, with its full event trail embedded oldest-first."}
- CallerRequestSummary: {"properties":{"id":{"type":"string","title":"Id"},"kind":{"type":"string","enum":["message","support_ticket"],"title":"Kind"},"state":{"type":"string","enum":["open","acknowledged","resolved"],"title":"State"},"severity":{"anyOf":[{"type":"string","enum":["critical","high","normal","low"]},{"type":"null"}],"title":"Severity"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Id"},"agent_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Version Id"},"phone_number_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number Id"},"caller_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Caller Name"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"body":{"type":"string","title":"Body"},"assignee_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Assignee User Id"},"acknowledged_by_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Acknowledged By Ref"},"acknowledged_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Acknowledged At"},"resolved_by_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolved By Ref"},"resolved_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Resolved At"},"version":{"type":"integer","title":"Version"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","kind","state","body","version","created_at","updated_at"],"title":"CallerRequestSummary","description":"One caller request as the list endpoint renders it."}
- CallerRequestUpdateRequest: {"properties":{"version":{"type":"integer","minimum":1,"title":"Version"},"state":{"anyOf":[{"type":"string","enum":["acknowledged","resolved"]},{"type":"null"}],"title":"State"},"assignee_user_id":{"anyOf":[{"type":"string","maxLength":128,"minLength":1},{"type":"null"}],"title":"Assignee User Id"},"note":{"anyOf":[{"type":"string","maxLength":1000,"minLength":1},{"type":"null"}],"title":"Note"}},"additionalProperties":false,"type":"object","required":["version"],"title":"CallerRequestUpdateRequest","description":"`PATCH /v1/caller-requests/{id}` - acknowledge, resolve, or (re)assign.\n\n``version`` is mandatory: the caller names the revision they read, and a mismatch is a\n409 rather than a silent last-write-wins. ``state`` may only move forward -\nopen → acknowledged → resolved, or open → resolved directly; resolved is terminal (no\nreopen in v1, ADR-0037 records the deferral). ``assignee_user_id`` uses PATCH\npresence semantics: absent means no assignment edit, an explicit ``null`` clears it."}
- CallingWindow: {"properties":{"start":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","title":"Start"},"end":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","title":"End"}},"additionalProperties":false,"type":"object","required":["start","end"],"title":"CallingWindow","description":"One local-time window on one weekday."}
- Campaign: {"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"status":{"$ref":"#/components/schemas/CampaignStatus"},"agent_definition_id":{"type":"string","title":"Agent Definition Id"},"agent_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Version Id"},"target":{"$ref":"#/components/schemas/CampaignTarget"},"number_pool_id":{"type":"string","title":"Number Pool Id"},"telephony_account_id":{"type":"string","title":"Telephony Account Id"},"compliance_profile_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compliance Profile Id"},"scrub_run_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scrub Run Id"},"max_attempts_per_contact":{"type":"integer","title":"Max Attempts Per Contact"},"concurrency_cap":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Concurrency Cap"},"contacts_total":{"type":"integer","title":"Contacts Total"},"dialed":{"type":"integer","title":"Dialed"},"connected":{"type":"integer","title":"Connected"},"completed_goal":{"type":"integer","title":"Completed Goal"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"retry_policy":{"$ref":"#/components/schemas/RetryPolicy"},"voicemail_drop":{"$ref":"#/components/schemas/VoicemailDropPolicy"},"goal":{"additionalProperties":true,"type":"object","title":"Goal"},"schedule":{"anyOf":[{"$ref":"#/components/schemas/CampaignSchedule"},{"type":"null"}]}},"type":"object","required":["id","name","status","agent_definition_id","target","number_pool_id","telephony_account_id","max_attempts_per_contact","contacts_total","dialed","connected","completed_goal","created_at","updated_at","retry_policy"],"title":"Campaign","description":"One campaign in full, schedule included."}
- CampaignActivity: {"properties":{"campaign_id":{"type":"string","title":"Campaign Id"},"name":{"type":"string","title":"Name"},"status":{"$ref":"#/components/schemas/CampaignStatus"},"workflows":{"items":{"type":"string"},"type":"array","title":"Workflows"},"calls_started":{"type":"integer","minimum":0,"title":"Calls Started"},"last_call_at":{"type":"string","format":"date-time","title":"Last Call At"}},"type":"object","required":["campaign_id","name","status","workflows","calls_started","last_call_at"],"title":"CampaignActivity"}
- CampaignCreateRequest: {"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"},"agent_definition_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Definition Id"},"agent_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Version Id"},"target":{"$ref":"#/components/schemas/CampaignTarget"},"number_pool_id":{"type":"string","title":"Number Pool Id"},"telephony_account_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telephony Account Id"},"compliance_profile_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compliance Profile Id"},"scrub_run_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scrub Run Id"},"schedule":{"anyOf":[{"$ref":"#/components/schemas/CampaignSchedule"},{"type":"null"}]},"max_attempts_per_contact":{"type":"integer","maximum":10,"minimum":1,"title":"Max Attempts Per Contact","default":3},"retry_policy":{"$ref":"#/components/schemas/RetryPolicy"},"voicemail_drop":{"$ref":"#/components/schemas/VoicemailDropPolicy"},"concurrency_cap":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"title":"Concurrency Cap"},"goal":{"additionalProperties":true,"type":"object","title":"Goal"}},"additionalProperties":false,"type":"object","required":["name","target","number_pool_id"],"title":"CampaignCreateRequest","description":"Create a campaign in `draft`. The agent version is pinned at launch, not here."}
- CampaignLaunchResponse: {"properties":{"status":{"$ref":"#/components/schemas/CampaignStatus"},"dialable_contacts":{"type":"integer","title":"Dialable Contacts"}},"type":"object","required":["status","dialable_contacts"],"title":"CampaignLaunchResponse","description":"What a launch accepted, and how many contacts it may dial."}
- CampaignOutcomeSummary: {"properties":{"contact_id":{"type":"string","title":"Contact Id"},"contact_phone":{"type":"string","title":"Contact Phone"},"attempts":{"type":"integer","title":"Attempts"},"latest_attempt_id":{"type":"string","title":"Latest Attempt Id"},"latest_attempt_state":{"$ref":"#/components/schemas/AttemptState"},"disposition":{"anyOf":[{"$ref":"#/components/schemas/Disposition"},{"type":"null"}]},"sub_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sub Code"},"call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Id"},"outcome":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/ShiftConfirmationOutcome"},{"$ref":"#/components/schemas/AppointmentOutcome"},{"$ref":"#/components/schemas/SurveyResponseOutcome"}],"discriminator":{"propertyName":"kind","mapping":{"appointment":"#/components/schemas/AppointmentOutcome","shift_confirmation":"#/components/schemas/ShiftConfirmationOutcome","survey_response":"#/components/schemas/SurveyResponseOutcome"}}},{"type":"null"}],"title":"Outcome"},"last_activity_at":{"type":"string","format":"date-time","title":"Last Activity At"}},"type":"object","required":["contact_id","contact_phone","attempts","latest_attempt_id","latest_attempt_state","last_activity_at"],"title":"CampaignOutcomeSummary","description":"One row per targeted contact: the latest attempt plus its structured outcome.\n\n`outcome` is null wherever no structured outcome was recorded - no answer, machine,\nopt-out, or a conversation that ended short of the objective."}
- CampaignOverview: {"properties":{"start":{"type":"string","format":"date-time","title":"Start"},"end":{"type":"string","format":"date-time","title":"End"},"as_of":{"type":"string","format":"date-time","title":"As Of"},"total_campaigns":{"type":"integer","minimum":0,"title":"Total Campaigns"},"status_counts":{"items":{"$ref":"#/components/schemas/CampaignStatusCount"},"type":"array","title":"Status Counts"},"calls_started":{"type":"integer","minimum":0,"title":"Calls Started"},"campaigns_with_calls":{"type":"integer","minimum":0,"title":"Campaigns With Calls"},"most_active":{"items":{"$ref":"#/components/schemas/CampaignActivity"},"type":"array","title":"Most Active"},"most_active_limit":{"type":"integer","minimum":1,"title":"Most Active Limit"}},"type":"object","required":["start","end","as_of","total_campaigns","status_counts","calls_started","campaigns_with_calls","most_active","most_active_limit"],"title":"CampaignOverview","description":"Current statuses plus outbound campaign call records started in [start, end)."}
- CampaignSchedule: {"properties":{"calling_windows":{"additionalProperties":{"items":{"$ref":"#/components/schemas/CallingWindow"},"type":"array"},"propertyNames":{"enum":["mon","tue","wed","thu","fri","sat","sun"]},"type":"object","title":"Calling Windows"},"start_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Start Date"},"end_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"End Date"},"paused_dates":{"items":{"type":"string","format":"date"},"type":"array","title":"Paused Dates"}},"additionalProperties":false,"type":"object","title":"CampaignSchedule","description":"Per-weekday calling windows plus the campaign's calendar bounds.\n\nDates, not instants: they bound the campaign in the callee's local day, and the policy\nlayer intersects the windows with jurisdictional quiet hours - the stricter wins."}
- CampaignStats: {"properties":{"dials":{"type":"integer","title":"Dials"},"connects":{"type":"integer","title":"Connects"},"connect_rate":{"type":"number","title":"Connect Rate"},"live_calls_now":{"type":"integer","title":"Live Calls Now"},"goal_completions":{"type":"integer","title":"Goal Completions"},"dispositions":{"additionalProperties":{"type":"integer"},"type":"object","title":"Dispositions"},"sub_codes":{"additionalProperties":{"type":"integer"},"type":"object","title":"Sub Codes"},"avg_call_duration_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Avg Call Duration Ms"},"spend_to_date":{"$ref":"#/components/schemas/Money"}},"type":"object","required":["dials","connects","connect_rate","live_calls_now","goal_completions","spend_to_date"],"title":"CampaignStats","description":"The 5-second monitor payload (docs/04-api.md §2 step 7).\n\nCounted from the attempt ledger and the billing ledger rather than from the campaign's\ndenormalized counters, so a stat is never stale relative to the rows behind it."}
- CampaignStatus: {"type":"string","enum":["draft","scrubbing","ready","running","paused","completed","cancelled"],"title":"CampaignStatus"}
- CampaignStatusCount: {"properties":{"status":{"$ref":"#/components/schemas/CampaignStatus"},"count":{"type":"integer","minimum":0,"title":"Count"}},"type":"object","required":["status","count"],"title":"CampaignStatusCount"}
- CampaignSummary: {"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"status":{"$ref":"#/components/schemas/CampaignStatus"},"agent_definition_id":{"type":"string","title":"Agent Definition Id"},"agent_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Version Id"},"target":{"$ref":"#/components/schemas/CampaignTarget"},"number_pool_id":{"type":"string","title":"Number Pool Id"},"telephony_account_id":{"type":"string","title":"Telephony Account Id"},"compliance_profile_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compliance Profile Id"},"scrub_run_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scrub Run Id"},"max_attempts_per_contact":{"type":"integer","title":"Max Attempts Per Contact"},"concurrency_cap":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Concurrency Cap"},"contacts_total":{"type":"integer","title":"Contacts Total"},"dialed":{"type":"integer","title":"Dialed"},"connected":{"type":"integer","title":"Connected"},"completed_goal":{"type":"integer","title":"Completed Goal"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","name","status","agent_definition_id","target","number_pool_id","telephony_account_id","max_attempts_per_contact","contacts_total","dialed","connected","completed_goal","created_at","updated_at"],"title":"CampaignSummary","description":"A campaign as a list endpoint returns it, counters included."}
- CampaignTarget: {"properties":{"contact_list_id":{"type":"string","title":"Contact List Id"}},"additionalProperties":false,"type":"object","required":["contact_list_id"],"title":"CampaignTarget","description":"v1 campaigns target a contact list; a `segment_id` target joins at P2."}
- CampaignUpdateRequest: {"properties":{"name":{"anyOf":[{"type":"string","maxLength":200,"minLength":1},{"type":"null"}],"title":"Name"},"agent_definition_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Definition Id"},"agent_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Version Id"},"target":{"anyOf":[{"$ref":"#/components/schemas/CampaignTarget"},{"type":"null"}]},"number_pool_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Number Pool Id"},"telephony_account_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telephony Account Id"},"compliance_profile_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compliance Profile Id"},"scrub_run_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scrub Run Id"},"schedule":{"anyOf":[{"$ref":"#/components/schemas/CampaignSchedule"},{"type":"null"}]},"max_attempts_per_contact":{"anyOf":[{"type":"integer","maximum":10,"minimum":1},{"type":"null"}],"title":"Max Attempts Per Contact"},"retry_policy":{"anyOf":[{"$ref":"#/components/schemas/RetryPolicy"},{"type":"null"}]},"voicemail_drop":{"anyOf":[{"$ref":"#/components/schemas/VoicemailDropPolicy"},{"type":"null"}]},"concurrency_cap":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"title":"Concurrency Cap"},"goal":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Goal"}},"additionalProperties":false,"type":"object","title":"CampaignUpdateRequest","description":"PATCH: only the fields present are written."}
- ComplianceAnswers: {"properties":{"contact_source":{"type":"string","enum":["own_signup","existing_customers","own_employees","own_crm","purchased","rented","scraped","appended","unsure"],"title":"Contact Source"},"call_purpose":{"type":"string","enum":["csat_survey","appointment_reminder","order_transactional","staffing_shifts","internal_team","sales_outreach","other"],"title":"Call Purpose"},"consent_basis":{"type":"string","enum":["pewc","pec","ebr","transactional","none"],"title":"Consent Basis"},"calls_mobile_numbers":{"type":"boolean","title":"Calls Mobile Numbers"},"target_states":{"items":{"type":"string"},"type":"array","maxItems":51,"minItems":1,"title":"Target States"},"intends_voicemail_drop":{"type":"boolean","title":"Intends Voicemail Drop"},"accepts_ai_disclosure":{"type":"boolean","title":"Accepts Ai Disclosure"},"accepts_own_carrier":{"type":"boolean","title":"Accepts Own Carrier"},"accepts_cold_outreach_waiver":{"type":"boolean","title":"Accepts Cold Outreach Waiver"}},"additionalProperties":false,"type":"object","required":["contact_source","call_purpose","consent_basis","calls_mobile_numbers","target_states","intends_voicemail_drop","accepts_ai_disclosure","accepts_own_carrier","accepts_cold_outreach_waiver"],"title":"ComplianceAnswers","description":"Step 3. Mirrors `vocapable_compliance.screening.ScreeningAnswers` field for field -\nevery question is required, because a screen with a hole in it is not a screen."}
- ConsentAttestation: {"properties":{"signer_name":{"type":"string","maxLength":200,"minLength":1,"title":"Signer Name"},"signer_title":{"type":"string","maxLength":200,"minLength":1,"title":"Signer Title"},"affirmation":{"type":"string","minLength":1,"title":"Affirmation"},"consent_basis":{"$ref":"#/components/schemas/ConsentBasis"},"signer_ip":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Signer Ip"},"signed_at":{"type":"string","format":"date-time","title":"Signed At"}},"type":"object","required":["signer_name","signer_title","affirmation","consent_basis","signed_at"],"title":"ConsentAttestation","description":"The stored attestation. `signer_ip` and `signed_at` are stamped by the API - a\nclient-supplied timestamp is a claim, not evidence."}
- ConsentAttestationInput: {"properties":{"signer_name":{"type":"string","maxLength":200,"minLength":1,"title":"Signer Name"},"signer_title":{"type":"string","maxLength":200,"minLength":1,"title":"Signer Title"},"affirmation":{"type":"string","minLength":1,"title":"Affirmation"},"consent_basis":{"$ref":"#/components/schemas/ConsentBasis"}},"type":"object","required":["signer_name","signer_title","affirmation","consent_basis"],"title":"ConsentAttestationInput","description":"What the tenant affirms about the list they are uploading.\n\nRequired on every import: the affirmation is the evidence linking each later dial to a\nnamed human who declared the consent basis (docs/03-domain-model.md §Audience)."}
- ConsentBasis: {"type":"string","enum":["pewc","pec","ebr","transactional"],"title":"ConsentBasis","description":"CANON fact 18 - the four consent classes (docs/07-compliance.md §consent)."}
- ConsentCreateRequest: {"properties":{"contact_id":{"type":"string","title":"Contact Id"},"consent_class":{"$ref":"#/components/schemas/ConsentBasis"},"source":{"type":"string","enum":["web_form","ivr","recorded_call","paper","import_attestation"],"title":"Source"},"captured_at":{"type":"string","format":"date-time","title":"Captured At"},"scope":{"additionalProperties":true,"type":"object","title":"Scope"},"proof":{"additionalProperties":true,"type":"object","title":"Proof"},"disclosure_text_shown":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Disclosure Text Shown"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"revokes_consent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Revokes Consent Id"}},"type":"object","required":["contact_id","consent_class","source","captured_at"],"title":"ConsentCreateRequest","description":"Append a consent record, or - with `revokes_consent_id` - a revocation of one."}
- ConsentRecordSummary: {"properties":{"id":{"type":"string","title":"Id"},"contact_id":{"type":"string","title":"Contact Id"},"consent_class":{"$ref":"#/components/schemas/ConsentBasis"},"scope":{"additionalProperties":true,"type":"object","title":"Scope"},"source":{"type":"string","enum":["web_form","ivr","recorded_call","paper","import_attestation","api_attestation"],"title":"Source"},"proof":{"additionalProperties":true,"type":"object","title":"Proof"},"disclosure_text_shown":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Disclosure Text Shown"},"captured_at":{"type":"string","format":"date-time","title":"Captured At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"status":{"type":"string","enum":["active","expired","revoked","suspect"],"title":"Status"},"revokes_consent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Revokes Consent Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","contact_id","consent_class","source","captured_at","status","created_at"],"title":"ConsentRecordSummary"}
- Contact: {"properties":{"id":{"type":"string","title":"Id"},"phone_e164":{"type":"string","title":"Phone E164"},"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timezone"},"best_consent_class":{"anyOf":[{"$ref":"#/components/schemas/ConsentBasis"},{"type":"null"}]},"do_not_call":{"type":"boolean","title":"Do Not Call","default":false},"lead_status":{"anyOf":[{"$ref":"#/components/schemas/LeadStatus"},{"type":"null"}]},"owner_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner User Id"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"jurisdictions":{"items":{"type":"string"},"type":"array","title":"Jurisdictions"},"attributes":{"additionalProperties":true,"type":"object","title":"Attributes"}},"type":"object","required":["id","phone_e164","created_at","updated_at"],"title":"Contact"}
- ContactAttemptSummary: {"properties":{"id":{"type":"string","title":"Id"},"contact_id":{"type":"string","title":"Contact Id"},"contact_phone":{"type":"string","title":"Contact Phone"},"attempt_number":{"type":"integer","title":"Attempt Number"},"state":{"$ref":"#/components/schemas/AttemptState"},"disposition":{"anyOf":[{"$ref":"#/components/schemas/Disposition"},{"type":"null"}]},"sub_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sub Code"},"failure_reason":{"anyOf":[{"$ref":"#/components/schemas/FailureReason"},{"type":"null"}]},"call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Id"},"scheduled_for":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Scheduled For"},"dialed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Dialed At"},"ended_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ended At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"campaign_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Id"},"contact_first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact First Name"},"contact_last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Last Name"},"origin_kind":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin Kind"},"callback_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Callback Reason"}},"type":"object","required":["id","contact_id","contact_phone","attempt_number","state","updated_at"],"title":"ContactAttemptSummary","description":"One attempt on the tenant-wide feed (`GET /v1/contact-attempts`).\n\nThe follow-up board. `scheduled_for` is the attempt's `next_eligible_at` - the\nearliest instant every window layer allows, which is what makes \"who is due today\" a\nserver-side range filter rather than a client-side scan.\n\nIt states scheduling facts and nothing more. An attempt appearing here is not a\npromise that the dial will be placed: every attempt is re-gated at dial time, and one\nlisted as due can still be refused then (docs/07-compliance.md)."}
- ContactBulkRequest: {"properties":{"contact_ids":{"items":{"type":"string"},"type":"array","maxItems":1000,"minItems":1,"title":"Contact Ids"},"action":{"type":"string","enum":["add_to_list","remove_from_list","set_lead_status","suppress","delete"],"title":"Action"},"contact_list_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact List Id"},"lead_status":{"anyOf":[{"$ref":"#/components/schemas/LeadStatus"},{"type":"null"}]}},"type":"object","required":["contact_ids","action"],"title":"ContactBulkRequest","description":"One action over up to 1000 contacts.\n\nThe same cap as `POST /contact-lists/{id}/members`, for the same reason: the whole\naction runs in one request and one transaction."}
- ContactBulkResponse: {"properties":{"action":{"type":"string","enum":["add_to_list","remove_from_list","set_lead_status","suppress","delete"],"title":"Action"},"requested":{"type":"integer","title":"Requested"},"matched":{"type":"integer","title":"Matched"},"changed":{"type":"integer","title":"Changed"},"unknown_contact_ids":{"items":{"type":"string"},"type":"array","title":"Unknown Contact Ids"}},"type":"object","required":["action","requested","matched","changed"],"title":"ContactBulkResponse","description":"What the action did, per outcome rather than per id.\n\n`unknown_contact_ids` is enumerated because a caller pasting stale ids has to find\nout; `matched` and `changed` differ whenever an action was already applied - bulk\nwrites are idempotent, so re-running one reports `changed = 0` rather than failing."}
- ContactCorrectionReview: {"properties":{"id":{"type":"string","title":"Id"},"action_id":{"type":"string","title":"Action Id"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"field":{"type":"string","enum":["first_name","last_name","company","title","email"],"title":"Field"},"decision":{"type":"string","enum":["applied","rejected"],"title":"Decision"},"reason":{"type":"string","title":"Reason"},"reviewer_ref":{"type":"string","title":"Reviewer Ref"},"before_value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Before Value"},"after_value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"After Value"},"proposed_value":{"type":"string","title":"Proposed Value"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","action_id","contact_id","field","decision","reason","reviewer_ref","before_value","after_value","proposed_value","created_at"],"title":"ContactCorrectionReview"}
- ContactCorrectionReviewRequest: {"properties":{"decision":{"type":"string","enum":["apply","reject"],"title":"Decision"},"reason":{"type":"string","maxLength":2000,"minLength":1,"title":"Reason"},"expected_contact_updated_at":{"type":"string","format":"date-time","title":"Expected Contact Updated At"},"expected_value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expected Value"}},"additionalProperties":false,"type":"object","required":["decision","reason","expected_contact_updated_at","expected_value"],"title":"ContactCorrectionReviewRequest"}
- ContactCorrectionReviewState: {"properties":{"can_decide":{"type":"boolean","title":"Can Decide","default":false},"status":{"type":"string","enum":["unreviewed","reviewed","superseded","contact_changed","unavailable"],"title":"Status"},"current_action_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Action Id"},"current_value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Value"},"contact_updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Contact Updated At"},"review":{"anyOf":[{"$ref":"#/components/schemas/ContactCorrectionReview"},{"type":"null"}]}},"type":"object","required":["status"],"title":"ContactCorrectionReviewState"}
- ContactCreateRequest: {"properties":{"phone_e164":{"type":"string","pattern":"^\\+[1-9]\\d{6,14}$","title":"Phone E164"},"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timezone"},"jurisdictions":{"items":{"type":"string"},"type":"array","title":"Jurisdictions"},"attributes":{"additionalProperties":true,"type":"object","title":"Attributes"},"lead_status":{"anyOf":[{"$ref":"#/components/schemas/LeadStatus"},{"type":"null"}]},"owner_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner User Id"},"tags":{"items":{"type":"string","maxLength":64,"minLength":1},"type":"array","maxItems":32,"title":"Tags"},"consent":{"$ref":"#/components/schemas/ApiContactConsentInput"}},"additionalProperties":false,"type":"object","required":["phone_e164","consent"],"title":"ContactCreateRequest","description":"Create one API contact and its initial lawful-consent record together.\n\nThis is intentionally stricter than the operator fields below: a programmatic create\nis headless contact intake, so it cannot first land a dialable-looking contact and ask\nthe customer to append consent later. The matching tenant-level API-source\nattestation is resolved only on the server and is written into the ConsentRecord proof."}
- ContactDialability: {"properties":{"contact_id":{"type":"string","title":"Contact Id"},"phone_e164":{"type":"string","title":"Phone E164"},"purpose":{"type":"string","enum":["marketing","informational","transactional"],"title":"Purpose"},"dialable":{"type":"boolean","title":"Dialable"},"blocked_reasons":{"items":{"type":"string"},"type":"array","title":"Blocked Reasons"},"policy_refusals":{"items":{"type":"string"},"type":"array","title":"Policy Refusals"},"risk_flags":{"items":{"type":"string"},"type":"array","title":"Risk Flags"},"jurisdictions":{"items":{"type":"string"},"type":"array","title":"Jurisdictions"},"timezone":{"type":"string","title":"Timezone"},"evaluated_at":{"type":"string","format":"date-time","title":"Evaluated At"},"policy_snapshot":{"additionalProperties":true,"type":"object","title":"Policy Snapshot"}},"type":"object","required":["contact_id","phone_e164","purpose","dialable","timezone","evaluated_at"],"title":"ContactDialability","description":"The pre-dial verdict for one contact.\n\nEvaluated over the contact's own record - the denormalized DNC flag, the consent class,\nthe jurisdictions, and the callee-local clock. Bureau scrub stages are not run here:\nthey are billed per query and belong to a ScrubRun, so `policy_snapshot.stages` is\nempty and a dial still passes the full gate."}
- ContactImportRequest: {"properties":{"file_name":{"type":"string","maxLength":255,"minLength":1,"title":"File Name"},"content":{"type":"string","minLength":1,"title":"Content"},"column_mapping":{"additionalProperties":{"type":"string","enum":["phone_e164","first_name","last_name","company","title","email","timezone"]},"type":"object","title":"Column Mapping"},"consent_attestation":{"$ref":"#/components/schemas/ConsentAttestationInput"},"target_list_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target List Id"},"list_name":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"List Name"},"on_duplicate":{"type":"string","enum":["reject","skip","update"],"title":"On Duplicate","default":"reject"}},"type":"object","required":["file_name","content","column_mapping","consent_attestation"],"title":"ContactImportRequest","description":"An import submission.\n\nThe CSV arrives as a text field rather than `multipart/form-data`: multipart needs\n`python-multipart`, which is not a workspace dependency. The parser takes the file's\ntext either way, so the wire shape can change without touching it."}
- ContactListCreateRequest: {"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"},"source":{"type":"string","enum":["upload","api"],"title":"Source","default":"api"},"consent_basis":{"anyOf":[{"$ref":"#/components/schemas/ConsentBasis"},{"type":"null"}]},"attestation_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attestation Ref"}},"type":"object","required":["name"],"title":"ContactListCreateRequest"}
- ContactListMembersRemovedResponse: {"properties":{"contact_list_id":{"type":"string","title":"Contact List Id"},"removed":{"type":"integer","title":"Removed"},"not_present":{"type":"integer","title":"Not Present"},"contact_count":{"type":"integer","title":"Contact Count"}},"type":"object","required":["contact_list_id","removed","not_present","contact_count"],"title":"ContactListMembersRemovedResponse","description":"The mirror of :class:`ContactListMembersResponse` for a removal.\n\n`not_present` rather than `already_present`, and no `unknown_contact_ids`: a removal\ncannot distinguish an id that names no contact from one that names a contact not on\nthis list, and reporting a guess would be worse than reporting the count."}
- ContactListMembersRequest: {"properties":{"contact_ids":{"items":{"type":"string"},"type":"array","maxItems":1000,"minItems":1,"title":"Contact Ids"}},"type":"object","required":["contact_ids"],"title":"ContactListMembersRequest"}
- ContactListMembersResponse: {"properties":{"contact_list_id":{"type":"string","title":"Contact List Id"},"added":{"type":"integer","title":"Added"},"already_present":{"type":"integer","title":"Already Present"},"unknown_contact_ids":{"items":{"type":"string"},"type":"array","title":"Unknown Contact Ids"},"contact_count":{"type":"integer","title":"Contact Count"}},"type":"object","required":["contact_list_id","added","already_present","contact_count"],"title":"ContactListMembersResponse"}
- ContactListSummary: {"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"source":{"type":"string","enum":["upload","api"],"title":"Source"},"consent_basis":{"anyOf":[{"$ref":"#/components/schemas/ConsentBasis"},{"type":"null"}]},"attestation_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attestation Ref"},"contact_count":{"type":"integer","title":"Contact Count"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","name","source","contact_count","created_at","updated_at"],"title":"ContactListSummary"}
- ContactListUpdateRequest: {"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"}},"type":"object","required":["name"],"title":"ContactListUpdateRequest","description":"PATCH on a list: the name, and nothing else.\n\n`consent_basis` and `attestation_ref` are deliberately not writable. They record what\na named human affirmed about the rows on this list at the moment it was created, and\nevery dial made against it cites them - an attestation that can be edited afterwards\nis not evidence of anything (docs/03-domain-model.md §Audience)."}
- ContactNote: {"properties":{"id":{"type":"string","title":"Id"},"contact_id":{"type":"string","title":"Contact Id"},"call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Id"},"author_ref":{"type":"string","title":"Author Ref"},"body":{"type":"string","title":"Body"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","contact_id","author_ref","body","created_at","updated_at"],"title":"ContactNote","description":"One working note a rep left on a contact.\n\n`author_ref` is whichever credential wrote it - an API key id, or a user id when a\ndashboard session did. It is not a person's name, and nothing here resolves it to one."}
- ContactNoteCreateRequest: {"properties":{"body":{"type":"string","maxLength":10000,"minLength":1,"title":"Body"},"call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Id"}},"type":"object","required":["body"],"title":"ContactNoteCreateRequest"}
- ContactSummary: {"properties":{"id":{"type":"string","title":"Id"},"phone_e164":{"type":"string","title":"Phone E164"},"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timezone"},"best_consent_class":{"anyOf":[{"$ref":"#/components/schemas/ConsentBasis"},{"type":"null"}]},"do_not_call":{"type":"boolean","title":"Do Not Call","default":false},"lead_status":{"anyOf":[{"$ref":"#/components/schemas/LeadStatus"},{"type":"null"}]},"owner_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner User Id"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","phone_e164","created_at","updated_at"],"title":"ContactSummary","description":"A contact as a list endpoint returns it."}
- ContactUpdateRequest: {"properties":{"phone_e164":{"anyOf":[{"type":"string","pattern":"^\\+[1-9]\\d{6,14}$"},{"type":"null"}],"title":"Phone E164"},"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name"},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name"},"company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timezone"},"jurisdictions":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Jurisdictions"},"attributes":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Attributes"},"lead_status":{"anyOf":[{"$ref":"#/components/schemas/LeadStatus"},{"type":"null"}]},"owner_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner User Id"},"tags":{"anyOf":[{"items":{"type":"string","maxLength":64,"minLength":1},"type":"array","maxItems":32},{"type":"null"}],"title":"Tags"}},"type":"object","title":"ContactUpdateRequest","description":"PATCH: only the fields present are written, and an explicit null clears one."}
- ConversationAction: {"properties":{"id":{"type":"string","title":"Id"},"call_id":{"type":"string","title":"Call Id"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"kind":{"type":"string","enum":["shift_response","qualification","identity_capture","identity_correction"],"title":"Kind"},"subject_ref":{"type":"string","title":"Subject Ref"},"revision":{"type":"integer","minimum":1,"title":"Revision"},"response":{"type":"string","title":"Response"},"payload":{"additionalProperties":true,"type":"object","title":"Payload"},"source_turns":{"items":{"$ref":"#/components/schemas/ConversationEvidence"},"type":"array","title":"Source Turns"},"action_confirmation":{"anyOf":[{"$ref":"#/components/schemas/ActionConfirmationProof"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","call_id","contact_id","kind","subject_ref","revision","response","payload","created_at"],"title":"ConversationAction","description":"One immutable receipt; latest caller evidence per kind/subject is current.\n\nRevisions record database arrival order and break ties within the same caller turn.\nEvidence-free legacy receipts rank below receipts with validated caller evidence."}
- ConversationEvidence: {"properties":{"turn_number":{"type":"integer","minimum":1,"title":"Turn Number"},"text":{"type":"string","maxLength":6000,"minLength":1,"title":"Text"}},"additionalProperties":false,"type":"object","required":["turn_number","text"],"title":"ConversationEvidence"}
- ConversationSettings: {"properties":{"default_language":{"type":"string","enum":["en","es"],"title":"Default Language","default":"en"},"supported_languages":{"items":{"type":"string","enum":["en","es"]},"type":"array","title":"Supported Languages"},"allow_language_switch":{"type":"boolean","title":"Allow Language Switch","default":true}},"additionalProperties":false,"type":"object","title":"ConversationSettings","description":"Versioned dialogue policy; language availability is qualified by the speech service."}
- CurrentPrincipalResource: {"properties":{"tenant_id":{"type":"string","title":"Tenant Id"},"api_key_id":{"type":"string","title":"Api Key Id"},"mode":{"type":"string","enum":["live","test"],"title":"Mode"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes"},"user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Id"}},"type":"object","required":["tenant_id","api_key_id","mode","scopes"],"title":"CurrentPrincipalResource","description":"`GET /v1/me` - the principal, plus the human behind it where one is resolvable.\n\n``user_id`` is the ``usr_`` id of the tenant user whose first-party dashboard session\naccompanied the request (the browser attaches the session cookie alongside the console\nkey). It is what lets the portal offer \"assign to me\" against surfaces keyed on\n`users.id`. Null - and typed that way - for a key-only caller: an API key names an\nintegration, not a person, and inventing one would be worse than answering none."}
- CursorPage_AgentSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/AgentSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[AgentSummary]"}
- CursorPage_AgentVersionSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/AgentVersionSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[AgentVersionSummary]"}
- CursorPage_ApiKeySummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/ApiKeySummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[ApiKeySummary]"}
- CursorPage_Appointment_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/Appointment"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[Appointment]"}
- CursorPage_AttemptSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/AttemptSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[AttemptSummary]"}
- CursorPage_CallSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/CallSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[CallSummary]"}
- CursorPage_CallTurn_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/CallTurn"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[CallTurn]"}
- CursorPage_CallerRequestSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/CallerRequestSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[CallerRequestSummary]"}
- CursorPage_CampaignOutcomeSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/CampaignOutcomeSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[CampaignOutcomeSummary]"}
- CursorPage_CampaignSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/CampaignSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[CampaignSummary]"}
- CursorPage_ConsentRecordSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/ConsentRecordSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[ConsentRecordSummary]"}
- CursorPage_ContactAttemptSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/ContactAttemptSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[ContactAttemptSummary]"}
- CursorPage_ContactListSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/ContactListSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[ContactListSummary]"}
- CursorPage_ContactNote_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/ContactNote"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[ContactNote]"}
- CursorPage_ContactSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/ContactSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[ContactSummary]"}
- CursorPage_DncEntrySummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/DncEntrySummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[DncEntrySummary]"}
- CursorPage_HumanRep_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/HumanRep"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[HumanRep]"}
- CursorPage_ImportJob_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/ImportJob"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[ImportJob]"}
- CursorPage_ImportRowError_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/ImportRowError"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[ImportRowError]"}
- CursorPage_InboundVoicemailSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/InboundVoicemailSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[InboundVoicemailSummary]"}
- CursorPage_IntegrationConnectionSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/IntegrationConnectionSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[IntegrationConnectionSummary]"}
- CursorPage_Invoice_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/Invoice"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[Invoice]"}
- CursorPage_KnowledgePackSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/KnowledgePackSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[KnowledgePackSummary]"}
- CursorPage_MessageSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/MessageSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[MessageSummary]"}
- CursorPage_MessageTemplateSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/MessageTemplateSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[MessageTemplateSummary]"}
- CursorPage_MobileCallAiContact_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/MobileCallAiContact"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[MobileCallAiContact]"}
- CursorPage_MobileReceptionistActivity_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/MobileReceptionistActivity"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[MobileReceptionistActivity]"}
- CursorPage_MobileTransferHistoryEntry_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/MobileTransferHistoryEntry"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[MobileTransferHistoryEntry]"}
- CursorPage_NumberPoolSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/NumberPoolSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[NumberPoolSummary]"}
- CursorPage_PhoneNumberSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/PhoneNumberSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[PhoneNumberSummary]"}
- CursorPage_QualificationReview_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/QualificationReview"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[QualificationReview]"}
- CursorPage_ReleaseGateEvaluation_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/ReleaseGateEvaluation"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[ReleaseGateEvaluation]"}
- CursorPage_RepGroupMember_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/RepGroupMember"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[RepGroupMember]"}
- CursorPage_RepGroup_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/RepGroup"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[RepGroup]"}
- CursorPage_ScrubResultSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/ScrubResultSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[ScrubResultSummary]"}
- CursorPage_ShiftConfirmation_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/ShiftConfirmation"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[ShiftConfirmation]"}
- CursorPage_SurveyResponse_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/SurveyResponse"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[SurveyResponse]"}
- CursorPage_TelephonyAccountSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/TelephonyAccountSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[TelephonyAccountSummary]"}
- CursorPage_TemplateSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/TemplateSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[TemplateSummary]"}
- CursorPage_TenantUserSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/TenantUserSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[TenantUserSummary]"}
- CursorPage_UsageEventSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/UsageEventSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[UsageEventSummary]"}
- CursorPage_VerifiedNumberSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/VerifiedNumberSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[VerifiedNumberSummary]"}
- CursorPage_VoiceProfileSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/VoiceProfileSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[VoiceProfileSummary]"}
- CursorPage_VoiceSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/VoiceSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[VoiceSummary]"}
- CursorPage_WebhookDelivery_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/WebhookDelivery"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[WebhookDelivery]"}
- CursorPage_WebhookTargetSummary_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/WebhookTargetSummary"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[WebhookTargetSummary]"}
- CursorPage_WorkflowOutcome_: {"properties":{"data":{"items":{"$ref":"#/components/schemas/WorkflowOutcome"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","required":["data"],"title":"CursorPage[WorkflowOutcome]"}
- CustomerPortalRequest: {"properties":{"flow":{"type":"string","enum":["manage","payment_method","subscription_update","subscription_cancel"],"title":"Flow","default":"manage"}},"type":"object","title":"CustomerPortalRequest","description":"The one action the customer wants Stripe to foreground."}
- CustomerPortalSession: {"properties":{"url":{"type":"string","title":"Url"},"flow":{"type":"string","enum":["manage","payment_method","subscription_update","subscription_cancel"],"title":"Flow"}},"type":"object","required":["url","flow"],"title":"CustomerPortalSession","description":"A one-use-at-a-time redirect target, not a persisted customer credential."}
- DashboardAgentHeadline: {"properties":{"id":{"type":"string","maxLength":128,"minLength":1,"title":"Id"},"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"},"use_case":{"type":"string","enum":["sdr","staffing","survey","reminder","custom","receptionist","recruiting"],"title":"Use Case"},"status":{"type":"string","enum":["draft","active","archived"],"title":"Status"},"health":{"type":"string","enum":["live","setup_needed","attention","archived"],"title":"Health"},"latest_version":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"title":"Latest Version"},"calls":{"$ref":"#/components/schemas/DashboardCallTotals"},"call_records":{"$ref":"#/components/schemas/DashboardCallRecords"}},"additionalProperties":false,"type":"object","required":["id","name","use_case","status","health","calls","call_records"],"title":"DashboardAgentHeadline","description":"One configured agent's compact health and activity headline."}
- DashboardAttention: {"properties":{"key":{"type":"string","enum":["caller_work","receptionist_health","agent_setup"],"title":"Key"},"state":{"type":"string","enum":["ready","attention"],"title":"State"},"count":{"type":"integer","minimum":0,"title":"Count"}},"additionalProperties":false,"type":"object","required":["key","state","count"],"title":"DashboardAttention","description":"A small server-derived work signal; zero means the item is ready."}
- DashboardCallRecords: {"properties":{"inbound_calls":{"type":"integer","minimum":0,"title":"Inbound Calls"},"outbound_calls":{"type":"integer","minimum":0,"title":"Outbound Calls"},"browser_calls":{"type":"integer","minimum":0,"title":"Browser Calls"}},"additionalProperties":false,"type":"object","required":["inbound_calls","outbound_calls","browser_calls"],"title":"DashboardCallRecords","description":"Durable calls by direction whose started_at is inside the response window.\n\nThese match /v1/calls with direction and started_at >= start, < end filters.\nCalls persisted after a read can appear on a subsequent read of the same window."}
- DashboardCallTotals: {"properties":{"inbound_calls":{"type":"integer","minimum":0,"title":"Inbound Calls"},"outbound_dials":{"type":"integer","minimum":0,"title":"Outbound Dials"}},"additionalProperties":false,"type":"object","required":["inbound_calls","outbound_dials"],"title":"DashboardCallTotals","description":"Legacy ingress/attempt activity by creation time; not call-log counts."}
- DashboardOpenCallerWork: {"properties":{"total":{"type":"integer","minimum":0,"title":"Total"},"messages":{"type":"integer","minimum":0,"title":"Messages"},"support_tickets":{"type":"integer","minimum":0,"title":"Support Tickets"},"high_priority_tickets":{"type":"integer","minimum":0,"title":"High Priority Tickets"}},"additionalProperties":false,"type":"object","required":["total","messages","support_tickets","high_priority_tickets"],"title":"DashboardOpenCallerWork","description":"Current unresolved caller requests, regardless of when they were opened."}
- DashboardOutcomes: {"properties":{"leads_captured":{"type":"integer","minimum":0,"title":"Leads Captured"},"messages_taken":{"type":"integer","minimum":0,"title":"Messages Taken"},"tickets_opened":{"type":"integer","minimum":0,"title":"Tickets Opened"},"callbacks_scheduled":{"type":"integer","minimum":0,"title":"Callbacks Scheduled"},"appointments_created":{"type":"integer","minimum":0,"title":"Appointments Created"},"shifts_confirmed":{"type":"integer","minimum":0,"title":"Shifts Confirmed"},"survey_responses":{"type":"integer","minimum":0,"title":"Survey Responses"}},"additionalProperties":false,"type":"object","required":["leads_captured","messages_taken","tickets_opened","callbacks_scheduled","appointments_created","shifts_confirmed","survey_responses"],"title":"DashboardOutcomes","description":"Durable business results created inside the requested historical window."}
- DashboardSummary: {"properties":{"window":{"$ref":"#/components/schemas/DashboardWindow"},"as_of":{"type":"string","format":"date-time","title":"As Of"},"call_totals":{"$ref":"#/components/schemas/DashboardCallTotals"},"call_records":{"$ref":"#/components/schemas/DashboardCallRecords"},"agents":{"items":{"$ref":"#/components/schemas/DashboardAgentHeadline"},"type":"array","title":"Agents"},"open_caller_work":{"$ref":"#/components/schemas/DashboardOpenCallerWork"},"outcomes":{"$ref":"#/components/schemas/DashboardOutcomes"},"attention":{"items":{"$ref":"#/components/schemas/DashboardAttention"},"type":"array","title":"Attention"}},"additionalProperties":false,"type":"object","required":["window","as_of","call_totals","call_records","open_caller_work","outcomes"],"title":"DashboardSummary","description":"`GET /v1/dashboard-summary` response for the portal Home screen."}
- DashboardWindow: {"properties":{"start":{"type":"string","format":"date-time","title":"Start"},"end":{"type":"string","format":"date-time","title":"End"}},"additionalProperties":false,"type":"object","required":["start","end"],"title":"DashboardWindow","description":"The exact half-open UTC interval the historical counts cover."}
- DeleteAccountRequest: {"properties":{"confirmation":{"type":"string","const":"DELETE","title":"Confirmation"},"password":{"type":"string","maxLength":1024,"minLength":8,"title":"Password"}},"type":"object","required":["confirmation","password"],"title":"DeleteAccountRequest","description":"Password-account's final, fresh confirmation.\n\nA typed phrase makes the last step unambiguous. The server still recomputes impact\nand verifies the password; this model is never treated as authority by itself."}
- DeviceAuthorizationApprovalRequest: {"properties":{"user_code":{"type":"string","maxLength":32,"minLength":8,"title":"User Code"}},"additionalProperties":false,"type":"object","required":["user_code"],"title":"DeviceAuthorizationApprovalRequest","description":"A human code copied from a local CLI into the signed-in portal.\n\nThe code is deliberately the only customer-controlled field: a browser owner cannot\nchoose a tenant, scopes, API-key mode, or a target device identity. Those facts come\nfrom the active dashboard session and the unread provisioning key respectively."}
- DeviceAuthorizationApprovalResponse: {"properties":{"status":{"type":"string","const":"approved","title":"Status"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"}},"type":"object","required":["status","expires_at"],"title":"DeviceAuthorizationApprovalResponse","description":"A receipt for a CSRF-protected owner approval; it contains no bearer grant."}
- DeviceAuthorizationFirstKeyClaimRequest: {"properties":{"grant":{"type":"string","maxLength":256,"minLength":32,"title":"Grant"}},"additionalProperties":false,"type":"object","required":["grant"],"title":"DeviceAuthorizationFirstKeyClaimRequest","description":"The opaque grant accepted only by the first-key claim endpoint."}
- DeviceAuthorizationGrantRequest: {"properties":{"device_code":{"type":"string","maxLength":256,"minLength":32,"title":"Device Code"}},"additionalProperties":false,"type":"object","required":["device_code"],"title":"DeviceAuthorizationGrantRequest","description":"The high-entropy device credential used only to poll its own pairing request."}
- DeviceAuthorizationGrantResponse: {"properties":{"grant":{"type":"string","title":"Grant"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"}},"type":"object","required":["grant","expires_at"],"title":"DeviceAuthorizationGrantResponse","description":"A route-limited, short-lived bridge to one first-key claim."}
- DeviceAuthorizationPendingResponse: {"properties":{"status":{"type":"string","const":"pending","title":"Status"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"interval_seconds":{"type":"integer","title":"Interval Seconds"}},"type":"object","required":["status","expires_at","interval_seconds"],"title":"DeviceAuthorizationPendingResponse","description":"A poll-safe response: approval has not happened, and no grant exists yet."}
- DeviceAuthorizationStartResponse: {"properties":{"device_code":{"type":"string","title":"Device Code"},"user_code":{"type":"string","title":"User Code"},"verification_uri":{"type":"string","title":"Verification Uri"},"verification_uri_complete":{"type":"string","title":"Verification Uri Complete"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"interval_seconds":{"type":"integer","title":"Interval Seconds"}},"type":"object","required":["device_code","user_code","verification_uri","verification_uri_complete","expires_at","interval_seconds"],"title":"DeviceAuthorizationStartResponse","description":"What an unauthenticated local device receives before owner approval."}
- DirectRepHandoffTarget: {"properties":{"rep_id":{"type":"string","maxLength":128,"minLength":1,"title":"Rep Id"},"display_name":{"type":"string","maxLength":200,"minLength":1,"title":"Display Name"}},"additionalProperties":false,"type":"object","required":["rep_id","display_name"],"title":"DirectRepHandoffTarget","description":"One server-resolved, immutable direct-rep target in an agent snapshot.\n\nA write request names only ``rep_id`` values. Control-api resolves those ids against\nactive, transfer-enabled tenant reps and persists this compact snapshot so an AgentVersion\nnever follows a later mutable Settings record. A raw destination is deliberately not part\nof this customer/model/runtime representation."}
- Disposition: {"type":"string","enum":["completed_goal","completed_no_goal","answered_incomplete","voicemail_machine","no_answer","busy","failed","opt_out","wrong_person_opt_out","dnc_hit","scrub_blocked","blocked_by_policy","abandoned_no_slot","inbound_unavailable","browser_unavailable"],"title":"Disposition","description":"Terminal outcome of a contact attempt (docs/06-campaign-engine.md)."}
- DispositionBucketOut: {"properties":{"campaign_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Id"},"agent_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Version Id"},"day":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Day"},"disposition":{"type":"string","title":"Disposition"},"attempts":{"type":"integer","title":"Attempts"},"share":{"type":"number","title":"Share"}},"type":"object","required":["disposition","attempts","share"],"title":"DispositionBucketOut","description":"One disposition's weight inside its group."}
- DispositionOverrideRequest: {"properties":{"code":{"$ref":"#/components/schemas/Disposition"},"sub_code":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Sub Code"},"notes":{"anyOf":[{"type":"string","maxLength":10000},{"type":"null"}],"title":"Notes"}},"type":"object","required":["code"],"title":"DispositionOverrideRequest","description":"A human's verdict on a reviewed call (`POST /v1/calls/{id}/disposition`).\n\n`code` is the pinned platform vocabulary (CANON fact 28) - the same list the agent\nwrites, because \"what happened on this dial\" is one question with one answer set.\n\nThis changes the tenant's own reporting and nothing else. A disposition is not a\ncompliance control: labelling a call `opt_out` here records an opinion about the call,\nit does not suppress the number. Suppression is `POST /v1/dnc-entries`, which writes\nthe DNC entry, withdraws the consent behind the number and emits `optout.recorded` -\nnone of which this request can do."}
- DispositionRecordSummary: {"properties":{"id":{"type":"string","title":"Id"},"call_id":{"type":"string","title":"Call Id"},"code":{"$ref":"#/components/schemas/Disposition"},"sub_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sub Code"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"set_by":{"type":"string","enum":["agent_llm","human_review"],"title":"Set By"},"structured_outcome":{"additionalProperties":true,"type":"object","title":"Structured Outcome"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","call_id","code","set_by","created_at"],"title":"DispositionRecordSummary","description":"One row of a call's disposition history.\n\n`dispositions` is append-only: a human review adds a row and repoints the call at it\nrather than editing the agent's verdict, so a call whose label was corrected reads as\ntwo rows and `set_by` says which was which."}
- DispositionReport: {"properties":{"window":{"$ref":"#/components/schemas/ReportWindow"},"group_by":{"items":{"type":"string","enum":["campaign","agent_version","day"]},"type":"array","title":"Group By"},"buckets":{"items":{"$ref":"#/components/schemas/DispositionBucketOut"},"type":"array","title":"Buckets"}},"type":"object","required":["window"],"title":"DispositionReport"}
- DncEntryCreateRequest: {"properties":{"phone_e164":{"type":"string","pattern":"^\\+[1-9]\\d{1,14}$","title":"Phone E164"},"scope":{"$ref":"#/components/schemas/DncScope","default":"internal"},"source":{"type":"string","enum":["opt_out_call","sms_stop","manual","import","api"],"title":"Source","default":"api"}},"type":"object","required":["phone_e164"],"title":"DncEntryCreateRequest","description":"What a tenant may send to `POST /v1/dnc-entries`, which records a revocation.\n\n`expires_at` is **parsed but not published**. Recording this entry withdraws the consent\nbehind the number and sets the contact's do-not-call flag, and neither is restorable\nwhen a suppression lapses, so an expiry is refused with `dnc_expiry_not_supported`\n(docs/04-api.md §9). `SkipJsonSchema` keeps the field out of the generated OpenAPI -\na published optional field whose only legal value is null is a setter every SDK offers\nand every call to it 422s - while leaving the refusal reachable, so a client sending one\nis told why instead of having it silently dropped. Same reason `scope` is expressible."}
- DncEntrySummary: {"properties":{"id":{"type":"string","title":"Id"},"phone_e164":{"type":"string","title":"Phone E164"},"scope":{"$ref":"#/components/schemas/DncScope"},"state_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State Code"},"source":{"type":"string","enum":["opt_out_call","sms_stop","manual","import","api"],"title":"Source"},"origin_call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin Call Id"},"added_at":{"type":"string","format":"date-time","title":"Added At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"}},"type":"object","required":["id","phone_e164","scope","source","added_at"],"title":"DncEntrySummary"}
- DncScope: {"type":"string","enum":["internal","federal","state"],"title":"DncScope"}
- DncSubscriptionRegisterRequest: {"properties":{"account_ref":{"type":"string","maxLength":64,"minLength":1,"title":"Account Ref"},"area_codes":{"items":{"type":"string"},"type":"array","maxItems":400,"minItems":1,"title":"Area Codes"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"}},"type":"object","required":["account_ref","area_codes"],"title":"DncSubscriptionRegisterRequest","description":"Register (or refresh) this tenant's federal SAN and the area codes it covers.\n\nThe SAN is the tenant's own FTC Subscription Account Number, obtained at\ntelemarketing.donotcall.gov - registering it is what authorizes this tenant's federal\nDo-Not-Call screening. Re-registering the same SAN updates its coverage rather than\nduplicating it. One SAN serves one seller (an FTC rule), so a tenant registers its own\nand attests to that; the credential is never sent here.\n\nThis does not load DNC data. The federal registry is platform reference data, refreshed\nby ingestion and shared across tenants; a customer's subscription authorizes that tenant's\naccess to it, and its own upload is not accepted for the same reason a tenant cannot write\na federal DNC entry (the shared dataset must not be tenant-writable)."}
- DncSubscriptionSummary: {"properties":{"id":{"type":"string","title":"Id"},"kind":{"type":"string","title":"Kind"},"account_ref":{"type":"string","title":"Account Ref"},"area_codes":{"items":{"type":"string"},"type":"array","title":"Area Codes"},"status":{"type":"string","title":"Status"},"effective_at":{"type":"string","format":"date-time","title":"Effective At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"}},"type":"object","required":["id","kind","account_ref","area_codes","status","effective_at"],"title":"DncSubscriptionSummary","description":"A tenant's registered DNC registry subscription - its SAN and the NPAs it covers.\n\nThe credential behind the SAN is held in the platform vault and is never returned; only\nthe account reference and the coverage are."}
- EmailConfirmationAccepted: {"properties":{"message":{"type":"string","title":"Message"},"action_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Action Url"}},"type":"object","required":["message"],"title":"EmailConfirmationAccepted"}
- EmailRequest: {"properties":{"email":{"type":"string","maxLength":254,"minLength":3,"title":"Email"}},"type":"object","required":["email"],"title":"EmailRequest"}
- EmergencyOverride: {"properties":{"id":{"type":"string","title":"Id"},"phone_number_id":{"type":"string","title":"Phone Number Id"},"mode":{"type":"string","enum":["ai","voicemail","forward"],"title":"Mode"},"rep_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rep Id"},"status":{"type":"string","enum":["active","cleared","expired","invalid"],"title":"Status"},"version":{"type":"integer","minimum":1,"title":"Version"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"cleared_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Cleared At"},"invalidated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Invalidated At"}},"type":"object","required":["id","phone_number_id","mode","status","version","expires_at","created_at","updated_at"],"title":"EmergencyOverride","description":"The durable current override for one inbound number."}
- EmergencyOverrideClearRequest: {"properties":{"override_id":{"type":"string","maxLength":128,"minLength":1,"title":"Override Id"},"expected_version":{"type":"integer","minimum":1,"title":"Expected Version"}},"additionalProperties":false,"type":"object","required":["override_id","expected_version"],"title":"EmergencyOverrideClearRequest","description":"Compare-and-swap clear for the exact emergency route the operator reviewed.\n\nAn override row is retained and reused for a number so ``override_id`` alone cannot\ndistinguish a prior arm from a later re-arm. The client sends the current routing\n``version`` returned by the read model; the router and repository bind and condition\nthe clear against both values."}
- EmergencyOverrideRequest: {"properties":{"mode":{"type":"string","enum":["ai","voicemail","forward"],"title":"Mode"},"rep_id":{"anyOf":[{"type":"string","maxLength":128,"minLength":1},{"type":"null"}],"title":"Rep Id"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"}},"additionalProperties":false,"type":"object","required":["mode","expires_at"],"title":"EmergencyOverrideRequest","description":"A time-bounded emergency route above one number's ordinary policy.\n\n``forward`` accepts a tenant-owned human-rep id only. The router resolves it again at\nwrite time and the carrier ingress resolves it again at execution time, so a client can\nnever turn this into an arbitrary-number dialer or leave a stale forwarding destination\nactive after a rep is disabled."}
- EstimateAnswers: {"properties":{"calls_per_month":{"type":"integer","maximum":10000000,"minimum":0,"title":"Calls Per Month"},"average_call_seconds":{"type":"integer","maximum":1800,"minimum":1,"title":"Average Call Seconds"}},"additionalProperties":false,"type":"object","required":["calls_per_month","average_call_seconds"],"title":"EstimateAnswers","description":"Step 4. What the applicant expects to run, so the calculator can price it. These are\nthe applicant's own numbers and nothing downstream treats them as a commitment."}
- ExpansionCheckoutRequest: {"properties":{"amount_due_minor":{"type":"integer","exclusiveMinimum":0,"title":"Amount Due Minor"},"currency":{"type":"string","const":"usd","title":"Currency"}},"additionalProperties":false,"type":"object","required":["amount_due_minor","currency"],"title":"ExpansionCheckoutRequest","description":"Confirm the exact displayed quote; prices and redirect URLs remain server-owned."}
- ExpansionQuoteRequest: {"properties":{"target_class":{"type":"string","enum":["engage","workforce","sales"],"title":"Target Class"},"request_key":{"type":"string","maxLength":200,"minLength":1,"title":"Request Key"}},"additionalProperties":false,"type":"object","required":["target_class","request_key"],"title":"ExpansionQuoteRequest"}
- ExpansionQuoteResponse: {"properties":{"id":{"type":"string","title":"Id"},"status":{"type":"string","title":"Status"},"source_plan_id":{"type":"string","title":"Source Plan Id"},"target_plan_id":{"type":"string","title":"Target Plan Id"},"source_class":{"type":"string","enum":["engage","workforce","sales","receptionist"],"title":"Source Class"},"target_class":{"type":"string","enum":["engage","workforce","sales","receptionist"],"title":"Target Class"},"term":{"type":"string","enum":["mtm","three_year"],"title":"Term"},"amount_due_minor":{"type":"integer","title":"Amount Due Minor"},"currency":{"type":"string","title":"Currency"},"source_monthly_minor":{"type":"integer","title":"Source Monthly Minor"},"target_monthly_minor":{"type":"integer","title":"Target Monthly Minor"},"total_inbound_connections":{"type":"integer","title":"Total Inbound Connections"},"total_outbound_sessions":{"type":"integer","title":"Total Outbound Sessions"},"minute_rate_minor":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Minute Rate Minor"},"attempt_fee_minor":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Attempt Fee Minor"},"volume_ladder_applies":{"type":"boolean","title":"Volume Ladder Applies"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"checkout_available":{"type":"boolean","title":"Checkout Available","default":false},"checkout_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Checkout Url"},"checkout_expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Checkout Expires At"},"payment_received":{"type":"boolean","title":"Payment Received","default":false},"failure_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failure Code"}},"type":"object","required":["id","status","source_plan_id","target_plan_id","source_class","target_class","term","amount_due_minor","currency","source_monthly_minor","target_monthly_minor","total_inbound_connections","total_outbound_sessions","minute_rate_minor","attempt_fee_minor","volume_ladder_applies","expires_at"],"title":"ExpansionQuoteResponse","description":"Customer figures only; provider IDs and commercial evidence stay private."}
- ExportCreateRequest: {"properties":{"resource":{"type":"string","enum":["contacts","contact_attempts","calls","survey_responses","shift_confirmations","appointments","dnc_entries","scrub_results","usage_events","caller_requests"],"title":"Resource"},"format":{"type":"string","const":"csv","title":"Format","default":"csv"},"filters":{"additionalProperties":{"type":"string"},"type":"object","title":"Filters"},"columns":{"items":{"type":"string"},"type":"array","title":"Columns"}},"type":"object","required":["resource"],"title":"ExportCreateRequest","description":"A submission. `filters` uses the query-filter grammar of `vocapable_contracts.filters`\n(`{\"campaign_id\": \"camp_…\", \"created_at.gte\": \"2026-08-03T00:00:00Z\"}`), validated\nagainst the per-resource allowlist; `columns` empty means the resource's default set."}
- ExportJob: {"properties":{"id":{"type":"string","title":"Id"},"resource":{"type":"string","enum":["contacts","contact_attempts","calls","survey_responses","shift_confirmations","appointments","dnc_entries","scrub_results","usage_events","caller_requests"],"title":"Resource"},"format":{"type":"string","const":"csv","title":"Format","default":"csv"},"status":{"$ref":"#/components/schemas/JobStatus"},"filters":{"additionalProperties":{"type":"string"},"type":"object","title":"Filters"},"columns":{"items":{"type":"string"},"type":"array","title":"Columns"},"row_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Row Count"},"download_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Download Url"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"requested_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Requested By"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","resource","status","created_at","updated_at"],"title":"ExportJob","description":"A job as `POST /v1/exports` and `GET /v1/exports/{id}` return it.\n\n`columns` is always the resolved list, never the empty request default: the set a job\nwas accepted with is frozen at submission so a later change to a resource's defaults\ncannot silently reshape a queued extraction."}
- ExtraSessionsRequest: {"properties":{"extra_sessions":{"type":"integer","minimum":0,"title":"Extra Sessions"}},"type":"object","required":["extra_sessions"],"title":"ExtraSessionsRequest","description":"The absolute purchased quantity - never a delta, so retries converge on N."}
- FailureReason: {"type":"string","enum":["wss_dropped","carrier_block","bootstrap_timeout","provider_error"],"title":"FailureReason","description":"Why an attempt failed. Exactly four values (CANON fact 64).\n\nMachine detection is not here - it lands the attempt in the `machine` state. A\nmissing conversation slot is not here either - that is the `abandoned_no_slot`\ndisposition."}
- ForwardStep: {"properties":{"type":{"type":"string","const":"forward","title":"Type","default":"forward"},"to_e164":{"type":"string","pattern":"^\\+[1-9]\\d{7,14}$","title":"To E164"}},"additionalProperties":false,"type":"object","required":["to_e164"],"title":"ForwardStep","description":"Bridge the caller to one number the tenant configured.\n\nE.164 is carried here and nowhere near a model-visible shape: this is a customer typing\na destination into their own answering policy, which is the same act as configuring a\n``HumanRep``. :class:`DirectRepHandoffTarget` deliberately carries no destination and\nis not widened by this - a live model still cannot name a number to dial."}
- FtcSanRef: {"properties":{"san":{"type":"string","title":"San"},"area_codes":{"items":{"type":"string"},"type":"array","title":"Area Codes"}},"additionalProperties":false,"type":"object","required":["san"],"title":"FtcSanRef","description":"One FTC Subscription Account Number and the area codes it covers (CANON fact 5)."}
- FunnelBucketOut: {"properties":{"campaign_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Id"},"agent_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Version Id"},"day":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Day"},"attempts":{"type":"integer","title":"Attempts"},"dials":{"type":"integer","title":"Dials"},"connects":{"type":"integer","title":"Connects"},"conversations":{"type":"integer","title":"Conversations"},"outcomes":{"type":"integer","title":"Outcomes"},"goal_completions":{"type":"integer","title":"Goal Completions"},"connect_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Connect Rate"},"conversation_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Conversation Rate"},"outcome_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Outcome Rate"},"goal_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Goal Rate"}},"type":"object","required":["attempts","dials","connects","conversations","outcomes","goal_completions"],"title":"FunnelBucketOut","description":"One group of the dial → connect → conversation → outcome funnel."}
- FunnelReport: {"properties":{"window":{"$ref":"#/components/schemas/ReportWindow"},"group_by":{"items":{"type":"string","enum":["campaign","agent_version","day"]},"type":"array","title":"Group By"},"buckets":{"items":{"$ref":"#/components/schemas/FunnelBucketOut"},"type":"array","title":"Buckets"}},"type":"object","required":["window"],"title":"FunnelReport"}
- GoogleCredential: {"properties":{"credential":{"type":"string","maxLength":4096,"minLength":20,"title":"Credential"}},"type":"object","required":["credential"],"title":"GoogleCredential","description":"The GIS ID token, exactly as Google Identity Services hands it to the page."}
- Guardrails: {"properties":{"disallowed_topics":{"items":{"type":"string"},"type":"array","title":"Disallowed Topics"},"blocklist_patterns":{"items":{"type":"string"},"type":"array","title":"Blocklist Patterns"},"judge":{"additionalProperties":true,"type":"object","title":"Judge"},"max_call_seconds":{"anyOf":[{"type":"integer","maximum":900,"minimum":30},{"type":"null"}],"title":"Max Call Seconds"},"max_turns":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"title":"Max Turns"}},"additionalProperties":false,"type":"object","title":"Guardrails","description":"Conversation-level controls frozen into the published version."}
- HTTPValidationError: {"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"}
- HandoffReadiness: {"properties":{"call_id":{"type":"string","title":"Call Id"},"active":{"type":"boolean","title":"Active"},"transport":{"anyOf":[{"type":"string","enum":["phone","browser"]},{"type":"null"}],"title":"Transport"},"state":{"anyOf":[{"type":"string","enum":["active","handoff_pending","transferred","ended","expired"]},{"type":"null"}],"title":"State"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"manual_available_rep_ids":{"items":{"type":"string"},"type":"array","title":"Manual Available Rep Ids"},"transfer_available":{"type":"boolean","title":"Transfer Available"},"listen_in_available":{"type":"boolean","title":"Listen In Available"},"blockers":{"items":{"type":"string"},"type":"array","title":"Blockers"}},"type":"object","required":["call_id","active","transfer_available","listen_in_available"],"title":"HandoffReadiness","description":"Truthful live-call capability view, not a promise that the carrier supports it.\n\n``manual_available_rep_ids`` is a dashboard advertisement only. It is deliberately not\ncalled an eligible-target list: no current executor can auto-transfer a caller from a\nREST heartbeat, and both live-action flags stay false until the gateway/media path exists."}
- HeardReadbackEvidence: {"properties":{"question_id":{"type":"string","maxLength":128,"minLength":1,"title":"Question Id"},"question_text":{"type":"string","maxLength":6000,"minLength":1,"title":"Question Text"},"playback_completed_at":{"type":"string","format":"date-time","title":"Playback Completed At"},"caller_started_at":{"type":"string","format":"date-time","title":"Caller Started At"}},"additionalProperties":false,"type":"object","required":["question_id","question_text","playback_completed_at","caller_started_at"],"title":"HeardReadbackEvidence","description":"Known playback completion followed by a trusted caller speech onset.\n\nQueue/send times do not establish playback start or completion. Receive time\nis not caller onset. Unknown or cleared playback cannot produce this proof."}
- HumanRep: {"properties":{"id":{"type":"string","title":"Id"},"display_name":{"type":"string","title":"Display Name"},"destination_e164":{"type":"string","title":"Destination E164"},"user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Id"},"status":{"type":"string","enum":["active","inactive"],"title":"Status"},"transfer_enabled":{"type":"boolean","title":"Transfer Enabled"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","display_name","destination_e164","status","transfer_enabled","created_at","updated_at"],"title":"HumanRep"}
- HumanRepCreateRequest: {"properties":{"display_name":{"type":"string","maxLength":200,"minLength":1,"title":"Display Name"},"destination_e164":{"type":"string","pattern":"^\\+[1-9]\\d{7,14}$","title":"Destination E164"},"user_id":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}],"title":"User Id"},"transfer_enabled":{"type":"boolean","title":"Transfer Enabled","default":true}},"additionalProperties":false,"type":"object","required":["display_name","destination_e164"],"title":"HumanRepCreateRequest","description":"Create a tenant-owned human handoff destination."}
- HumanRepUpdateRequest: {"properties":{"display_name":{"anyOf":[{"type":"string","maxLength":200,"minLength":1},{"type":"null"}],"title":"Display Name"},"destination_e164":{"anyOf":[{"type":"string","pattern":"^\\+[1-9]\\d{7,14}$"},{"type":"null"}],"title":"Destination E164"},"user_id":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}],"title":"User Id"},"status":{"anyOf":[{"type":"string","enum":["active","inactive"]},{"type":"null"}],"title":"Status"},"transfer_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Transfer Enabled"}},"additionalProperties":false,"type":"object","title":"HumanRepUpdateRequest","description":"Change a destination deliberately; an inactive rep is never selected."}
- ImportJob: {"properties":{"id":{"type":"string","title":"Id"},"status":{"$ref":"#/components/schemas/JobStatus"},"file_uri":{"type":"string","title":"File Uri"},"column_mapping":{"additionalProperties":{"type":"string"},"type":"object","title":"Column Mapping"},"target_list_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target List Id"},"consent_attestation":{"$ref":"#/components/schemas/ConsentAttestation"},"rows_accepted":{"type":"integer","title":"Rows Accepted"},"rows_rejected":{"type":"integer","title":"Rows Rejected"},"rows_skipped":{"type":"integer","title":"Rows Skipped","default":0},"on_duplicate":{"type":"string","enum":["reject","skip","update"],"title":"On Duplicate","default":"reject"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","status","file_uri","consent_attestation","rows_accepted","rows_rejected","created_at","updated_at"],"title":"ImportJob"}
- ImportRowError: {"properties":{"row":{"type":"integer","title":"Row"},"code":{"type":"string","enum":["parse_failure","invalid_e164","duplicate","mapping_error"],"title":"Code"},"detail":{"type":"string","title":"Detail"},"column":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Column"},"value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Value"}},"type":"object","required":["row","code","detail"],"title":"ImportRowError","description":"One rejected row. `row` counts the header as row 1, as a spreadsheet does."}
- InboundIntent: {"type":"string","enum":["stop","help","other"],"title":"InboundIntent","description":"Classification of an inbound SMS; `STOP` writes an internal DNC entry."}
- InboundMessageAck: {"properties":{"message_id":{"type":"string","title":"Message Id"},"intent":{"$ref":"#/components/schemas/InboundIntent"},"dnc_entry_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dnc Entry Id"},"dnc_entry_created":{"type":"boolean","title":"Dnc Entry Created"}},"type":"object","required":["message_id","intent","dnc_entry_id","dnc_entry_created"],"title":"InboundMessageAck"}
- InboundMessageRequest: {"properties":{"from_e164":{"type":"string","pattern":"^\\+[1-9]\\d{1,14}$","title":"From E164"},"to_e164":{"type":"string","pattern":"^\\+[1-9]\\d{1,14}$","title":"To E164"},"body":{"type":"string","title":"Body"},"provider_sid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider Sid"}},"type":"object","required":["from_e164","to_e164","body"],"title":"InboundMessageRequest","description":"The normalized provider callback body (see the module docstring)."}
- InboundSmsWebhookSummary: {"properties":{"url":{"type":"string","title":"Url"}},"type":"object","required":["url"],"title":"InboundSmsWebhookSummary","description":"The exact signed Twilio Messaging URL for one eligible BYO account.\n\nThis is a configuration address, not a credential: Twilio still proves every request\nagainst the account's vaulted auth token, and the destination number is signed and\nrouted again on receipt."}
- InboundVoiceRoutingUpdateRequest: {"properties":{"inbound_agent_id":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}],"title":"Inbound Agent Id"},"mode":{"type":"string","enum":["agent","voicemail","policy"],"title":"Mode","default":"agent"},"voicemail_greeting":{"anyOf":[{"type":"string","maxLength":1000,"minLength":1},{"type":"null"}],"title":"Voicemail Greeting"}},"additionalProperties":false,"type":"object","title":"InboundVoiceRoutingUpdateRequest","description":"Choose (or clear) the receiving mode for one inbound voice number.\n\n``None`` intentionally clears the route. A non-null value is validated server-side as\na tenant-owned agent with an already-published immutable version before the number can\naccept a call, so configuring this endpoint can never make a draft callable. A\n``voicemail`` route is *inbound caller voicemail only*: its greeting is immutable per\nadmitted call, and the server refuses it unless the published profile and exact matched\ncaller jurisdiction permit a durable one-party recording. It does not enable outbound\nanswering-machine drops.\n\n``policy`` is deliberately unreachable here even though it is a member of the shared\nmode vocabulary: an answering-policy route is only meaningful alongside the document\nthat describes it, so it is armed by writing that document and cleared by deleting it.\nAccepting the mode on this endpoint would let a number claim a route with no policy\nbehind it, which the `inbound_route_shape` CHECK exists to make impossible."}
- InboundVoiceWebhookSummary: {"properties":{"url":{"type":"string","title":"Url"},"status_callback_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status Callback Url"}},"type":"object","required":["url"],"title":"InboundVoiceWebhookSummary","description":"The exact signed Twilio Voice URL for one eligible BYO account.\n\nIt is publishable configuration, not a bearer capability: Twilio still signs every\nrequest with the account's vaulted auth token, and the signed destination is then\nresolved against one active carrier-confirmed voice number with an explicit receiving\nagent. The route returns no useful response until all of those server-side checks pass."}
- InboundVoicemail: {"properties":{"id":{"type":"string","title":"Id"},"status":{"type":"string","enum":["recording","ready","importing","available","absent","failed"],"title":"Status"},"call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Id"},"recording_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recording Id"},"phone_number_id":{"type":"string","title":"Phone Number Id"},"agent_version_id":{"type":"string","title":"Agent Version Id"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"from_e164":{"type":"string","title":"From E164"},"to_e164":{"type":"string","title":"To E164"},"duration_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration Seconds"},"received_at":{"type":"string","format":"date-time","title":"Received At"},"completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Completed At"},"failure_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failure Code"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"recording_action_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Recording Action At"},"max_recording_seconds":{"type":"integer","title":"Max Recording Seconds"}},"type":"object","required":["id","status","phone_number_id","agent_version_id","from_e164","to_e164","received_at","updated_at","max_recording_seconds"],"title":"InboundVoicemail","description":"The full non-audio receipt for ``GET /calls/inbound-voicemails/{id}``."}
- InboundVoicemailSummary: {"properties":{"id":{"type":"string","title":"Id"},"status":{"type":"string","enum":["recording","ready","importing","available","absent","failed"],"title":"Status"},"call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Id"},"recording_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recording Id"},"phone_number_id":{"type":"string","title":"Phone Number Id"},"agent_version_id":{"type":"string","title":"Agent Version Id"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"from_e164":{"type":"string","title":"From E164"},"to_e164":{"type":"string","title":"To E164"},"duration_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration Seconds"},"received_at":{"type":"string","format":"date-time","title":"Received At"},"completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Completed At"},"failure_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failure Code"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","status","phone_number_id","agent_version_id","from_e164","to_e164","received_at","updated_at"],"title":"InboundVoicemailSummary","description":"One inbound voicemail receipt in the customer inbox.\n\nA receipt appears as soon as a lawful caller is invited to record, so a carrier or\nstorage failure is visible rather than silently dropping the message. ``call_id`` and\n``recording_id`` stay null until private audio has been imported into the ordinary Call\nand Recording records; clients must not construct an audio URL from this response."}
- InboxContactRef: {"properties":{"id":{"type":"string","title":"Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company"},"phone_e164":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone E164"}},"type":"object","required":["id"],"title":"InboxContactRef","description":"The caller, as the inbox names them - enough to render a row, never the full CRM."}
- InboxEntry: {"properties":{"kind":{"type":"string","enum":["message","support_ticket","callback","appointment","voicemail"],"title":"Kind"},"id":{"type":"string","title":"Id"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"contact":{"anyOf":[{"$ref":"#/components/schemas/InboxContactRef"},{"type":"null"}]},"call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Id"},"agent_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Version Id"},"phone_number_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"snippet":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Snippet"}},"type":"object","required":["kind","id","created_at"],"title":"InboxEntry","description":"One row of the merged feed. ``kind`` says which durable resource ``id`` names.\n\nA single shape rather than a five-member discriminated union: every kind carries the\nsame fields, only their nullability differs by source (an appointment names no line,\nan anonymous voicemail names no contact), and one shape is what lets the portal render\none list component over the whole queue."}
- InboxPage: {"properties":{"data":{"items":{"$ref":"#/components/schemas/InboxEntry"},"type":"array","title":"Data"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"type":"object","title":"InboxPage","description":"`GET /v1/inbox` - the merged keyset page (created_at desc, id desc)."}
- IngestedDocumentReport: {"properties":{"file_name":{"type":"string","title":"File Name"},"format":{"type":"string","title":"Format"},"sections":{"type":"integer","title":"Sections"},"tokens":{"type":"integer","title":"Tokens"}},"type":"object","required":["file_name","format","sections","tokens"],"title":"IngestedDocumentReport","description":"What one uploaded document cost, so \"distill harder\" names a file.\n\nThe parts need not sum to the version's `compiled_token_count`: a BPE tokenizer merges\nacross the joins between documents, so the pack is counted once as a whole."}
- IntegrationAuthorization: {"properties":{"provider":{"type":"string","const":"calendar","title":"Provider"},"authorization_url":{"type":"string","title":"Authorization Url"},"state_expires_at":{"type":"string","format":"date-time","title":"State Expires At"}},"type":"object","required":["provider","authorization_url","state_expires_at"],"title":"IntegrationAuthorization","description":"Where to send the customer's admin, and how long the signed state admits them."}
- IntegrationConnectionCreateRequest: {"properties":{"provider":{"type":"string","const":"calendar","title":"Provider"}},"additionalProperties":false,"type":"object","required":["provider"],"title":"IntegrationConnectionCreateRequest","description":"Start an OAuth consent. No connection exists until the callback completes it."}
- IntegrationConnectionSummary: {"properties":{"id":{"type":"string","title":"Id"},"provider":{"type":"string","title":"Provider"},"external_account_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Account Id"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes"},"status":{"type":"string","enum":["active","expired","revoked","error"],"title":"Status"},"last_refreshed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Refreshed At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","provider","status","created_at","updated_at"],"title":"IntegrationConnectionSummary","description":"A connection as its tenant sees it - token material is structurally absent."}
- Invoice: {"properties":{"id":{"type":"string","title":"Id"},"period_start":{"type":"string","format":"date-time","title":"Period Start"},"period_end":{"type":"string","format":"date-time","title":"Period End"},"line_items":{"items":{},"type":"array","title":"Line Items"},"total_minor":{"type":"integer","title":"Total Minor"},"currency":{"type":"string","title":"Currency"},"status":{"type":"string","enum":["draft","open","paid","void","uncollectible"],"title":"Status"},"external_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Ref"},"hosted_invoice_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Hosted Invoice Url"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","period_start","period_end","total_minor","currency","status","created_at","updated_at"],"title":"Invoice"}
- JobStatus: {"type":"string","enum":["queued","running","completed","failed"],"title":"JobStatus","description":"Lifecycle shared by the async import and export jobs."}
- KnowledgePackCreateRequest: {"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"}},"additionalProperties":false,"type":"object","required":["name"],"title":"KnowledgePackCreateRequest","description":"`POST /v1/knowledge-packs`. Content arrives as versions, never on the pack itself."}
- KnowledgePackDocumentUpload: {"properties":{"file_name":{"type":"string","maxLength":255,"minLength":1,"title":"File Name"},"content":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}],"title":"Content"},"content_base64":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}],"title":"Content Base64"},"format":{"anyOf":[{"type":"string","enum":["pdf","docx","csv","md","txt"]},{"type":"null"}],"title":"Format"}},"additionalProperties":false,"type":"object","required":["file_name"],"title":"KnowledgePackDocumentUpload","description":"One uploaded file.\n\nThe bytes arrive base64-encoded (`content_base64`) or, for the three text formats, as\nplain text (`content`) - never as `multipart/form-data`, which needs `python-multipart`\nand is not a workspace dependency. `ContactImportRequest` made the same call for CSV\nuploads, and the extractor takes bytes either way, so the wire shape can change later\nwithout touching ingestion."}
- KnowledgePackIngestRequest: {"properties":{"documents":{"items":{"$ref":"#/components/schemas/KnowledgePackDocumentUpload"},"type":"array","maxItems":20,"minItems":1,"title":"Documents"},"budget_tokens":{"anyOf":[{"type":"integer","maximum":16000,"minimum":1},{"type":"null"}],"title":"Budget Tokens"}},"additionalProperties":false,"type":"object","required":["documents"],"title":"KnowledgePackIngestRequest","description":"`POST /v1/knowledge-packs/{id}/documents` - upload documents as the next version.\n\n`budget_tokens` is the caller's own ceiling, not the platform's. The 16,000-token hard\ncap (docs/08-ai-quality.md, CANON fact 12) always applies and bounds this field; leaving\nit unset means only the hard cap is enforced, which is exactly what the hand-authored\nversion endpoint does."}
- KnowledgePackIngestResponse: {"properties":{"version":{"$ref":"#/components/schemas/KnowledgePackVersionResource"},"documents":{"items":{"$ref":"#/components/schemas/IngestedDocumentReport"},"type":"array","title":"Documents"}},"type":"object","required":["version","documents"],"title":"KnowledgePackIngestResponse","description":"The created version plus the ingest accounting behind it."}
- KnowledgePackSummary: {"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"current_version":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Current Version"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","name","created_at","updated_at"],"title":"KnowledgePackSummary","description":"A pack as a list row. `current_version` is the highest version that reached `ready`."}
- KnowledgePackVersionCreateRequest: {"properties":{"sources":{"items":{"$ref":"#/components/schemas/PackSource"},"type":"array","minItems":1,"title":"Sources"},"compiled_token_count":{"type":"integer","minimum":1,"title":"Compiled Token Count"}},"additionalProperties":false,"type":"object","required":["sources","compiled_token_count"],"title":"KnowledgePackVersionCreateRequest","description":"`POST /v1/knowledge-packs/{id}/versions` - publish an edit as a new version.\n\n`compiled_token_count` is measured by whatever compiled the pack - the distillation\njob, or the authoring editor's live token meter. The control plane holds no model\ntokenizer (the tokenizer lives with the serving model on the GPU plane), so it\nvalidates the declared count against the budget rather than recomputing it. The count\nis checked, never clamped: over the hard cap the request is refused and no version is\nwritten."}
- KnowledgePackVersionResource: {"properties":{"id":{"type":"string","title":"Id"},"knowledge_pack_id":{"type":"string","title":"Knowledge Pack Id"},"version":{"type":"integer","title":"Version"},"content_hash":{"type":"string","title":"Content Hash"},"compiled_token_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Compiled Token Count"},"status":{"type":"string","enum":["compiling","ready","failed"],"title":"Status"},"sources":{"items":{"$ref":"#/components/schemas/PackSource"},"type":"array","title":"Sources"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"over_default_budget":{"type":"boolean","title":"Over Default Budget","description":"Compiled above the 8k default budget but inside the 16k hard cap.\n\nPermitted, and worth telling the tenant: a cap-size pack roughly doubles cold\nprefill and halves the number of warm pack prefixes a node holds.","readOnly":true}},"type":"object","required":["id","knowledge_pack_id","version","content_hash","status","sources","created_at","updated_at","over_default_budget"],"title":"KnowledgePackVersionResource","description":"A compiled version. Immutable once `ready` - an edit is the next version."}
- LatencyBucketOut: {"properties":{"campaign_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Id"},"agent_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Version Id"},"day":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Day"},"turns":{"type":"integer","title":"Turns"},"voice_to_voice_samples":{"type":"integer","title":"Voice To Voice Samples"},"voice_to_voice_p50_ms":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Voice To Voice P50 Ms"},"voice_to_voice_p95_ms":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Voice To Voice P95 Ms"},"voice_to_voice_p99_ms":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Voice To Voice P99 Ms"},"llm_ttft_samples":{"type":"integer","title":"Llm Ttft Samples"},"llm_ttft_p50_ms":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Llm Ttft P50 Ms"},"llm_ttft_p95_ms":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Llm Ttft P95 Ms"},"llm_ttft_p99_ms":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Llm Ttft P99 Ms"}},"type":"object","required":["turns","voice_to_voice_samples","llm_ttft_samples"],"title":"LatencyBucketOut","description":"Turn-latency percentiles for one group; `null` means the group had no sample."}
- LatencyReport: {"properties":{"window":{"$ref":"#/components/schemas/ReportWindow"},"group_by":{"items":{"type":"string","enum":["campaign","agent_version","day"]},"type":"array","title":"Group By"},"buckets":{"items":{"$ref":"#/components/schemas/LatencyBucketOut"},"type":"array","title":"Buckets"}},"type":"object","required":["window"],"title":"LatencyReport"}
- LeadStatus: {"type":"string","enum":["new","working","qualified","unqualified","won","lost"],"title":"LeadStatus","description":"Where the tenant's own pipeline holds a contact (migration 0035).\n\nDurable per-contact intent, deliberately separate from `Disposition`, which is pinned\n(CANON fact 28) and answers \"what happened to the dial\" rather than \"is this person\ninterested\". A contact is dialed many times and disposed once per attempt; the lead\nstatus is the one value that survives across those attempts.\n\nIt carries NO compliance meaning whatsoever: `LOST` is not a suppression, and marking\na contact `WON` neither grants nor implies consent. Suppression is `dnc_entries` and\nconsent is `consent_records` - both have their own writers, and neither reads this\ncolumn."}
- LineType: {"type":"string","enum":["landline","mobile","fixed_voip","nonfixed_voip","unknown"],"title":"LineType"}
- LiveCallBillingProjection: {"properties":{"status":{"type":"string","enum":["not_available","estimated"],"title":"Status"},"basis":{"anyOf":[{"type":"string","const":"managed_test_call_prepaid"},{"type":"null"}],"title":"Basis"},"connected_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Connected At"},"projected_through":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Projected Through"},"elapsed_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Elapsed Seconds"},"projected_billable_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Projected Billable Seconds"},"projected_amount_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Projected Amount Minor"},"platform_rate_per_minute_minor":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Platform Rate Per Minute Minor"},"currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Currency"}},"type":"object","required":["status"],"title":"LiveCallBillingProjection","description":"A bounded elapsed-time estimate for the one live lane with a frozen platform rate.\n\nIt is available only while a platform-managed, prepaid, campaignless test call holds a\ndurable authorization. ``connected_at`` is the redeemed live-session receipt's trusted\nstart, while the carrier's terminal callback remains the authority for the actual debit.\nBYO carrier prices and ordinary production-call carrier amounts are intentionally absent."}
- LlmRoute: {"properties":{"primary":{"type":"string","minLength":1,"title":"Primary"},"fallbacks":{"items":{"type":"string"},"type":"array","title":"Fallbacks"}},"additionalProperties":false,"type":"object","required":["primary"],"title":"LlmRoute","description":"Provider routing chain with health-checked failover (CANON fact 24).\n\nLLM chain only. STT and TTS chains are platform-defaulted at P1 and are not\nAPI-settable (CANON fact 80), which is what `extra=\"forbid\"` refuses here."}
- MenuStep-Input: {"properties":{"type":{"type":"string","const":"menu","title":"Type","default":"menu"},"prompt":{"type":"string","maxLength":1000,"minLength":1,"title":"Prompt"},"branches":{"patternProperties":{"^[0-9*#]$":{"items":{"oneOf":[{"$ref":"#/components/schemas/MenuStep-Input"},{"$ref":"#/components/schemas/RingHumansStep"},{"$ref":"#/components/schemas/AiStep"},{"$ref":"#/components/schemas/VoicemailStep"},{"$ref":"#/components/schemas/SayStep"},{"$ref":"#/components/schemas/ForwardStep"}],"discriminator":{"propertyName":"type","mapping":{"ai":"#/components/schemas/AiStep","forward":"#/components/schemas/ForwardStep","menu":"#/components/schemas/MenuStep-Input","ring_humans":"#/components/schemas/RingHumansStep","say":"#/components/schemas/SayStep","voicemail":"#/components/schemas/VoicemailStep"}}},"type":"array","maxItems":12,"minItems":1}},"type":"object","maxProperties":12,"minProperties":1,"title":"Branches"},"timeout_seconds":{"type":"integer","maximum":30,"minimum":1,"title":"Timeout Seconds","default":5},"max_invalid_attempts":{"type":"integer","maximum":5,"minimum":1,"title":"Max Invalid Attempts","default":2},"timeout_branch":{"items":{"oneOf":[{"$ref":"#/components/schemas/MenuStep-Input"},{"$ref":"#/components/schemas/RingHumansStep"},{"$ref":"#/components/schemas/AiStep"},{"$ref":"#/components/schemas/VoicemailStep"},{"$ref":"#/components/schemas/SayStep"},{"$ref":"#/components/schemas/ForwardStep"}],"discriminator":{"propertyName":"type","mapping":{"ai":"#/components/schemas/AiStep","forward":"#/components/schemas/ForwardStep","menu":"#/components/schemas/MenuStep-Input","ring_humans":"#/components/schemas/RingHumansStep","say":"#/components/schemas/SayStep","voicemail":"#/components/schemas/VoicemailStep"}}},"type":"array","maxItems":12,"minItems":1,"title":"Timeout Branch"}},"additionalProperties":false,"type":"object","required":["prompt","branches","timeout_branch"],"title":"MenuStep","description":"Read options, then follow the branch the caller pressed.\n\n``timeout_branch`` is required rather than optional, and it is also where the invalid\nbudget lands once it is spent: a menu whose silent or fumbling caller has nowhere to go\nre-prompts forever, which is the single most common way a keypress tree becomes a trap."}
- MenuStep-Output: {"properties":{"type":{"type":"string","const":"menu","title":"Type","default":"menu"},"prompt":{"type":"string","maxLength":1000,"minLength":1,"title":"Prompt"},"branches":{"patternProperties":{"^[0-9*#]$":{"items":{"oneOf":[{"$ref":"#/components/schemas/MenuStep-Output"},{"$ref":"#/components/schemas/RingHumansStep"},{"$ref":"#/components/schemas/AiStep"},{"$ref":"#/components/schemas/VoicemailStep"},{"$ref":"#/components/schemas/SayStep"},{"$ref":"#/components/schemas/ForwardStep"}],"discriminator":{"propertyName":"type","mapping":{"ai":"#/components/schemas/AiStep","forward":"#/components/schemas/ForwardStep","menu":"#/components/schemas/MenuStep-Output","ring_humans":"#/components/schemas/RingHumansStep","say":"#/components/schemas/SayStep","voicemail":"#/components/schemas/VoicemailStep"}}},"type":"array","maxItems":12,"minItems":1}},"type":"object","maxProperties":12,"minProperties":1,"title":"Branches"},"timeout_seconds":{"type":"integer","maximum":30,"minimum":1,"title":"Timeout Seconds","default":5},"max_invalid_attempts":{"type":"integer","maximum":5,"minimum":1,"title":"Max Invalid Attempts","default":2},"timeout_branch":{"items":{"oneOf":[{"$ref":"#/components/schemas/MenuStep-Output"},{"$ref":"#/components/schemas/RingHumansStep"},{"$ref":"#/components/schemas/AiStep"},{"$ref":"#/components/schemas/VoicemailStep"},{"$ref":"#/components/schemas/SayStep"},{"$ref":"#/components/schemas/ForwardStep"}],"discriminator":{"propertyName":"type","mapping":{"ai":"#/components/schemas/AiStep","forward":"#/components/schemas/ForwardStep","menu":"#/components/schemas/MenuStep-Output","ring_humans":"#/components/schemas/RingHumansStep","say":"#/components/schemas/SayStep","voicemail":"#/components/schemas/VoicemailStep"}}},"type":"array","maxItems":12,"minItems":1,"title":"Timeout Branch"}},"additionalProperties":false,"type":"object","required":["prompt","branches","timeout_branch"],"title":"MenuStep","description":"Read options, then follow the branch the caller pressed.\n\n``timeout_branch`` is required rather than optional, and it is also where the invalid\nbudget lands once it is spent: a menu whose silent or fumbling caller has nowhere to go\nre-prompts forever, which is the single most common way a keypress tree becomes a trap."}
- MessageDirection: {"type":"string","enum":["outbound","inbound"],"title":"MessageDirection"}
- MessageStatus: {"type":"string","enum":["queued","sent","delivered","failed","received"],"title":"MessageStatus"}
- MessageSummary: {"properties":{"id":{"type":"string","title":"Id"},"direction":{"$ref":"#/components/schemas/MessageDirection"},"phone_number_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number Id"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"to_e164":{"type":"string","title":"To E164"},"from_e164":{"type":"string","title":"From E164"},"template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template Id"},"body_rendered":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body Rendered"},"status":{"$ref":"#/components/schemas/MessageStatus"},"provider_sid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider Sid"},"origin_call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin Call Id"},"inbound_intent":{"anyOf":[{"$ref":"#/components/schemas/InboundIntent"},{"type":"null"}]},"segments":{"type":"integer","title":"Segments"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","direction","phone_number_id","contact_id","to_e164","from_e164","template_id","body_rendered","status","provider_sid","origin_call_id","inbound_intent","segments","created_at","updated_at"],"title":"MessageSummary"}
- MessageTemplateCreateRequest: {"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"},"locale":{"type":"string","maxLength":35,"minLength":2,"title":"Locale","default":"en"},"body":{"type":"string","minLength":1,"title":"Body"},"status":{"type":"string","enum":["draft","active"],"title":"Status","default":"active"}},"type":"object","required":["name","body"],"title":"MessageTemplateCreateRequest"}
- MessageTemplateSummary: {"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"locale":{"type":"string","title":"Locale"},"body":{"type":"string","title":"Body"},"status":{"type":"string","title":"Status"},"variables":{"items":{"type":"string"},"type":"array","title":"Variables"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","name","locale","body","status","variables","created_at","updated_at"],"title":"MessageTemplateSummary"}
- MessageTemplateUpdateRequest: {"properties":{"status":{"type":"string","enum":["active","archived"],"title":"Status"}},"additionalProperties":false,"type":"object","required":["status"],"title":"MessageTemplateUpdateRequest","description":"`PATCH /v1/message-templates/{id}` - a status flip and NOTHING else (ADR-0037).\n\nBody, name, and locale are immutable by construction: agent versions pin templates by\nid (`sms_template_ids`), so an editable body would let a live agent send text nobody\nreviewed under an id that was. `extra=\"forbid\"` is what refuses the attempt with a\n422 naming the field. `archived` retires a template from sending without deleting the\nrow the pins reference; `active` restores it (and activates a draft)."}
- MeterUsage: {"properties":{"meter":{"type":"string","enum":["telephony_minutes","stt_audio_seconds","tts_characters","llm_prompt_tokens","llm_cached_prompt_tokens","llm_completion_tokens","gpu_seconds","sms_segments","lookup_requests"],"title":"Meter"},"billing":{"type":"string","enum":["billed","bundled","internal","carrier_billed"],"title":"Billing"},"quantity":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Quantity"},"event_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Event Count"},"rated_amount_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rated Amount Minor"},"included_quantity":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Included Quantity"},"overage_quantity":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Overage Quantity"}},"type":"object","required":["meter","billing"],"title":"MeterUsage","description":"One meter's totals for the period.\n\nThe component's headline invariant is the ``quantity is None`` vs\n``billing == \"carrier_billed\"`` split - they are **independent facts**:\n\n* ``quantity is None`` - the ledger holds no rows for this meter. Nothing was metered.\n* ``billing == \"carrier_billed\"`` - this tenant's own carrier bills the meter directly\n (CANON fact 70), so Vocapable never writes a row for it.\n\nA tenant that switched managed→byo mid-period therefore still reports the minutes it\nreally accrued while carrying ``carrier_billed``, and a managed tenant with no calls\nreports ``0``, never ``null``."}
- MobileAiContentReportReceipt: {"properties":{"id":{"type":"string","title":"Id"},"call_id":{"type":"string","title":"Call Id"},"reason":{"type":"string","enum":["inaccurate_or_misleading","unsafe_or_harmful","offensive_or_abusive","privacy_or_personal_data"],"title":"Reason"},"review_status":{"type":"string","const":"pending","title":"Review Status"},"created":{"type":"boolean","title":"Created"},"reported_at":{"type":"string","format":"date-time","title":"Reported At"}},"type":"object","required":["id","call_id","reason","review_status","created","reported_at"],"title":"MobileAiContentReportReceipt","description":"A truthful acknowledgement that an authorized review request was received."}
- MobileAiContentReportRequest: {"properties":{"reason":{"type":"string","enum":["inaccurate_or_misleading","unsafe_or_harmful","offensive_or_abusive","privacy_or_personal_data"],"title":"Reason"}},"additionalProperties":false,"type":"object","required":["reason"],"title":"MobileAiContentReportRequest","description":"A no-free-text report about AI-generated call/transcript output.\n\nThe source call is supplied only in the authenticated route path. Keeping this body to\none controlled reason prevents a mobile report flow from duplicating transcript text,\nrecording audio, customer contact details, or other sensitive content into a separate\nreporting store."}
- MobileAppointment: {"properties":{"id":{"type":"string","title":"Id"},"call_id":{"type":"string","title":"Call Id"},"starts_at":{"type":"string","format":"date-time","title":"Starts At"},"ends_at":{"type":"string","format":"date-time","title":"Ends At"},"contact_timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Timezone"},"status":{"type":"string","enum":["booked","rescheduled","cancelled","completed","no_show"],"title":"Status"},"invite_sent_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Invite Sent At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","call_id","starts_at","ends_at","status","created_at","updated_at"],"title":"MobileAppointment","description":"A mobile appointment receipt without contact, attendee, or provider identifiers."}
- MobileAvailabilitySetRequest: {"properties":{"available_until":{"type":"string","format":"date-time","title":"Available Until"}},"additionalProperties":false,"type":"object","required":["available_until"],"title":"MobileAvailabilitySetRequest","description":"Set the caller's own availability until one explicit UTC instant."}
- MobileAvailabilityWindow: {"properties":{"membership_id":{"type":"string","title":"Membership Id"},"rep_id":{"type":"string","title":"Rep Id"},"status":{"type":"string","enum":["available","ended","expired"],"title":"Status"},"available":{"type":"boolean","title":"Available"},"available_until":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Available Until"},"declared_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Declared At"},"ended_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ended At"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["membership_id","rep_id","status","available"],"title":"MobileAvailabilityWindow","description":"The privacy-minimized state shown in the mobile team view."}
- MobileBillingBudget: {"properties":{"amount_minor":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"title":"Amount Minor"},"currency":{"type":"string","maxLength":12,"minLength":3,"title":"Currency"},"period":{"anyOf":[{"type":"string","enum":["daily","monthly"]},{"type":"null"}],"title":"Period"},"low_balance_threshold_minor":{"anyOf":[{"type":"integer","minimum":0},{"type":"null"}],"title":"Low Balance Threshold Minor"},"version":{"type":"integer","minimum":1,"title":"Version"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"additionalProperties":false,"type":"object","required":["currency","version","updated_at"],"title":"MobileBillingBudget","description":"The current spend guardrails; no payment method or provider data is representable."}
- MobileBillingBudgetUpdateRequest: {"properties":{"expected_version":{"type":"integer","minimum":1,"title":"Expected Version"},"amount_minor":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"title":"Amount Minor"}},"additionalProperties":false,"type":"object","required":["expected_version","amount_minor"],"title":"MobileBillingBudgetUpdateRequest","description":"One displayed spend-cap edit bound to a browser step-up grant and a revision.\n\nThe server preserves the wallet's existing low-balance threshold and known period.\nThis keeps the small native editor from silently clearing a browser-configured setting."}
- MobileBillingInvoiceSummary: {"properties":{"id":{"type":"string","maxLength":128,"minLength":1,"title":"Id"},"status":{"type":"string","enum":["paid","open","past_due","void","unavailable"],"title":"Status"},"amount_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Amount Minor"},"currency":{"anyOf":[{"type":"string","maxLength":12,"minLength":3},{"type":"null"}],"title":"Currency"},"due_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Due At"}},"additionalProperties":false,"type":"object","required":["id","status"],"title":"MobileBillingInvoiceSummary","description":"A compact mirror summary. Stripe-hosted invoice links stay browser-only."}
- MobileBillingPortalRequest: {"properties":{},"additionalProperties":false,"type":"object","title":"MobileBillingPortalRequest","description":"An intentionally empty command; the server always opens the generic safe portal."}
- MobileBillingPortalSession: {"properties":{"url":{"type":"string","maxLength":2048,"minLength":8,"title":"Url"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"}},"additionalProperties":false,"type":"object","required":["url"],"title":"MobileBillingPortalSession","description":"One ephemeral Stripe-hosted URL, never a stored/customer/provider identifier."}
- MobileBootstrapProfile: {"properties":{"tenant_id":{"type":"string","title":"Tenant Id"},"membership":{"$ref":"#/components/schemas/TenantMembershipSummary"},"capabilities":{"items":{"$ref":"#/components/schemas/MobileCapability"},"type":"array","title":"Capabilities"},"device":{"$ref":"#/components/schemas/MobileDeviceSummary"}},"type":"object","required":["tenant_id","membership","capabilities","device"],"title":"MobileBootstrapProfile","description":"The authenticated app's identity/bootstrap response at ``GET /v1/mobile/me``.\n\nOperational selector data (contacts, agents, schedules, team rows) deliberately\nstays on its own capability-gated endpoints. This profile contains only the facts\nevery app boot needs and no push token or refresh/session secret."}
- MobileCalendarIntegrationReadiness: {"properties":{"status":{"type":"string","enum":["connected","reconnect_required","not_connected","unavailable"],"title":"Status"},"provider":{"anyOf":[{"type":"string","const":"google_calendar"},{"type":"null"}],"title":"Provider"},"booking_enabled":{"type":"boolean","title":"Booking Enabled"},"manage_on_web":{"type":"boolean","const":true,"title":"Manage On Web","default":true}},"type":"object","required":["status","booking_enabled"],"title":"MobileCalendarIntegrationReadiness","description":"The connection state relevant to call-bound calendar booking.\n\n``provider`` identifies only the deployment-supported adapter. It is never an OAuth\naccount identity. ``booking_enabled`` is deliberately conservative: an active\nconnection is necessary, but a particular agent still needs the approved calendar tools\nand a live call still evaluates its normal server-side policy."}
- MobileCallAiAgent: {"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"published_version":{"type":"integer","minimum":1,"title":"Published Version"},"eligibility":{"type":"string","const":"published_active","title":"Eligibility","default":"published_active"}},"type":"object","required":["id","name","published_version"],"title":"MobileCallAiAgent","description":"An active agent with the published snapshot a mobile launch may pin."}
- MobileCallAiContact: {"properties":{"id":{"type":"string","title":"Id"},"display_name":{"type":"string","title":"Display Name"},"company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company"}},"type":"object","required":["id","display_name"],"title":"MobileCallAiContact","description":"A live contact offered by the mobile Call AI selector.\n\nIts direct phone number and email intentionally stay out of this shape. Listing a\ncontact does not assert it is dialable: the shared launch path still evaluates DNC,\nconsent, jurisdiction, frequency, caller-ID, agreement, and spend controls."}
- MobileCallAiRequest: {"properties":{"agent_id":{"type":"string","maxLength":64,"minLength":1,"title":"Agent Id"},"contact_id":{"type":"string","maxLength":64,"minLength":1,"title":"Contact Id"}},"additionalProperties":false,"type":"object","required":["agent_id","contact_id"],"title":"MobileCallAiRequest","description":"A deliberately narrow native request for one compliant outbound AI call.\n\nThe app chooses both durable resources from the companion read models. It cannot\nprovide an E.164 destination, caller-ID override, carrier account, telephony mode,\nor a list of contacts, so this endpoint cannot become an alternate bulk dialer."}
- MobileCallAiResponse: {"properties":{"id":{"type":"string","title":"Id"},"agent_id":{"type":"string","title":"Agent Id"},"agent_version_id":{"type":"string","title":"Agent Version Id"},"contact_id":{"type":"string","title":"Contact Id"},"telephony":{"type":"string","const":"byo","title":"Telephony"},"state":{"type":"string","title":"State"},"disposition":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Disposition"},"call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","agent_id","agent_version_id","contact_id","telephony","state","created_at"],"title":"MobileCallAiResponse","description":"The queued result of one native Call AI request.\n\nMobile launch is always routed through the tenant's verified BYO carrier plane. The\ngeneric call contract also permits the test-only mock transport, so it must not be\nused as this endpoint's response model or a native client could be told an impossible\ntransport state."}
- MobileCallTranscript: {"properties":{"id":{"type":"string","title":"Id"},"call_id":{"type":"string","title":"Call Id"},"full_text":{"type":"string","title":"Full Text"},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"},"turns":{"items":{"$ref":"#/components/schemas/MobileCallTranscriptTurn"},"type":"array","title":"Turns"},"turns_truncated":{"type":"boolean","title":"Turns Truncated","default":false},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"},"redaction_status":{"type":"string","enum":["raw","pii_redacted"],"title":"Redaction Status"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","call_id","full_text","redaction_status","created_at","updated_at"],"title":"MobileCallTranscript","description":"A mobile-safe transcript detail with no extracted entities or tool payloads."}
- MobileCallTranscriptTurn: {"properties":{"id":{"type":"string","title":"Id"},"seq":{"type":"integer","title":"Seq"},"speaker":{"type":"string","enum":["agent","contact"],"title":"Speaker"},"text":{"type":"string","title":"Text"},"started_at_ms":{"type":"integer","title":"Started At Ms"},"ended_at_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Ended At Ms"},"interrupted":{"type":"boolean","title":"Interrupted","default":false},"revision":{"type":"integer","title":"Revision","default":0},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","seq","speaker","text","started_at_ms","created_at"],"title":"MobileCallTranscriptTurn","description":"One user-visible transcript turn for the native activity detail view.\n\nThe generic customer-call turn also contains model latency and arbitrary tool-call\npayloads. Those values are neither rendered nor safe to copy into a mobile client:\ntool arguments can contain CRM or calendar data. The protected transcript text is\nintentionally available to authorized mobile reviewers; everything else stays on the\nfull web review surface."}
- MobileCapability: {"type":"string","enum":["activity:read","insights:read","schedule:write","emergency_override:write","team:read","team:availability:write","team:members:manage","team:transfer_destinations:manage","calls:outbound:start","own_availability:write","own_handoffs:read","handoff_booking:write","device_sessions:manage","operations_tasks:read","operations_tasks:mutate","operations_tasks:assign","workspace:read","workspace:switch","billing:read","billing:write","receptionist_studio:read","receptionist_studio:write"],"title":"MobileCapability","description":"Operations the mobile receptionist surface may authorize.\n\nThese are not API-key scopes. They deliberately describe product actions rather\nthan HTTP methods so web and mobile routes can share one role decision without a\nbrowser session accidentally becoming a broad bearer credential."}
- MobileChangeFeedPage: {"properties":{"items":{"items":{"$ref":"#/components/schemas/MobileChangeHint"},"type":"array","maxItems":100,"title":"Items"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false}},"additionalProperties":false,"type":"object","title":"MobileChangeFeedPage","description":"A bounded foreground update batch.\n\nWith no cursor the service returns a recent bounded slice and establishes a\nhigh-watermark. ``has_more`` is meaningful only after a cursor: historical audit\nrecords are deliberately not replayed through this lightweight invalidation channel."}
- MobileChangeHint: {"properties":{"version":{"type":"string","maxLength":128,"minLength":1,"title":"Version"},"target":{"$ref":"#/components/schemas/MobileChangeTarget"}},"additionalProperties":false,"type":"object","required":["version","target"],"title":"MobileChangeHint","description":"One immutable, opaque version of a safe foreground invalidation target."}
- MobileChangeTarget: {"properties":{"kind":{"type":"string","enum":["activity","billing","emergency_override","handoff","integrations","notifications","operations","receptionist","schedule","team"],"title":"Kind"},"resource_id":{"anyOf":[{"type":"string","maxLength":128,"minLength":1},{"type":"null"}],"title":"Resource Id"}},"additionalProperties":false,"type":"object","required":["kind"],"title":"MobileChangeTarget","description":"A client route invalidation target without human or operational content."}
- MobileCoachPreferences: {"properties":{"telemetry_opt_in":{"type":"boolean","title":"Telemetry Opt In","default":false},"recommendations":{"items":{"$ref":"#/components/schemas/MobileCoachRecommendationPreference"},"type":"array","maxItems":9,"title":"Recommendations"}},"additionalProperties":false,"type":"object","title":"MobileCoachPreferences","description":"The entire finite Coach state for the authenticated current device."}
- MobileCoachRecommendationPreference: {"properties":{"key":{"type":"string","enum":["team_coverage","transfer_destination","availability","schedule","notifications","calendar_booking","receptionist","capacity","portal"],"title":"Key"},"state":{"type":"string","enum":["active","dismissed","snoozed"],"title":"State"},"snoozed_until":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Snoozed Until"}},"additionalProperties":false,"type":"object","required":["key","state"],"title":"MobileCoachRecommendationPreference","description":"Current-device handling of one fixed Workspace Setup recommendation key."}
- MobileCoachRecommendationPreferenceChangeRequest: {"properties":{"action":{"type":"string","enum":["dismiss","snooze","restore"],"title":"Action"},"snooze_duration":{"anyOf":[{"type":"string","enum":["one_hour","one_day","one_week"]},{"type":"null"}],"title":"Snooze Duration"}},"additionalProperties":false,"type":"object","required":["action"],"title":"MobileCoachRecommendationPreferenceChangeRequest","description":"A finite local handling action, never a way to mutate the underlying setup item."}
- MobileCoachTelemetryConsentUpdateRequest: {"properties":{"telemetry_opt_in":{"type":"boolean","title":"Telemetry Opt In"}},"additionalProperties":false,"type":"object","required":["telemetry_opt_in"],"title":"MobileCoachTelemetryConsentUpdateRequest","description":"An explicit current-device election; it is off by default."}
- MobileCoachTelemetryEventRequest: {"properties":{"action":{"type":"string","enum":["opened","recommendation_viewed","recommendation_dismissed","recommendation_snoozed","recommendation_restored","setup_action_opened"],"title":"Action"},"duration_bucket":{"type":"string","enum":["not_applicable","under_5_seconds","5_to_30_seconds","30_to_120_seconds","over_120_seconds"],"title":"Duration Bucket"},"app_version":{"type":"string","maxLength":11,"minLength":5,"pattern":"^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$","title":"App Version"},"role":{"type":"string","enum":["admin","manager"],"title":"Role"},"window_class":{"type":"string","enum":["fresh_foreground","resumed_foreground","active_foreground"],"title":"Window Class"}},"additionalProperties":false,"type":"object","required":["action","duration_bucket","app_version","role","window_class"],"title":"MobileCoachTelemetryEventRequest","description":"One anonymous, finite aggregate-measurement increment.\n\nThere is intentionally no recommendation key, tenant/device/member id, target id,\ncaller/contact data, timestamp, token, free text, or raw duration. The server verifies\nthe submitted role against the authenticated mobile session before incrementing."}
- MobileCoachTelemetryReceipt: {"properties":{"recorded":{"type":"boolean","title":"Recorded"}},"additionalProperties":false,"type":"object","required":["recorded"],"title":"MobileCoachTelemetryReceipt","description":"A no-detail acknowledgement that preserves an opt-out's privacy boundary."}
- MobileDevicePushRegistrationRequest: {"properties":{"push_token":{"anyOf":[{"type":"string","maxLength":4096,"minLength":1},{"type":"null"}],"title":"Push Token"},"device_label":{"anyOf":[{"type":"string","maxLength":200,"minLength":1},{"type":"null"}],"title":"Device Label"}},"additionalProperties":false,"type":"object","title":"MobileDevicePushRegistrationRequest","description":"Register, rotate, or clear the app's Expo push endpoint for this device."}
- MobileDeviceSummary: {"properties":{"id":{"type":"string","title":"Id"},"platform":{"type":"string","enum":["ios","android"],"title":"Platform"},"device_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device Label"},"status":{"type":"string","enum":["active","revoked"],"title":"Status"},"last_seen_at":{"type":"string","format":"date-time","title":"Last Seen At"}},"type":"object","required":["id","platform","status","last_seen_at"],"title":"MobileDeviceSummary"}
- MobileHandoffBookingAvailability: {"properties":{"handoff_id":{"type":"string","title":"Handoff Id"},"timezone":{"type":"string","title":"Timezone"},"duration_minutes":{"type":"integer","maximum":240,"minimum":5,"title":"Duration Minutes"},"slots":{"items":{"$ref":"#/components/schemas/MobileHandoffBookingSlot"},"type":"array","title":"Slots"}},"type":"object","required":["handoff_id","timezone","duration_minutes"],"title":"MobileHandoffBookingAvailability","description":"Availability narrowed to one handoff; it has no provider connection metadata."}
- MobileHandoffBookingAvailabilityRequest: {"properties":{"start_date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","title":"Start Date"},"end_date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","title":"End Date"},"duration_minutes":{"type":"integer","maximum":240,"minimum":5,"title":"Duration Minutes"},"timezone":{"type":"string","maxLength":64,"minLength":1,"title":"Timezone"}},"additionalProperties":false,"type":"object","required":["start_date","end_date","duration_minutes","timezone"],"title":"MobileHandoffBookingAvailabilityRequest","description":"A bounded, display-zone availability request for one eligible handoff.\n\nThere is no call, contact, attendee, connection, calendar, OAuth, or raw-time field in\nthis model. The route derives those facts from the authenticated handoff and returns\nopaque slot snapshots rather than a reusable provider availability response."}
- MobileHandoffBookingCreateRequest: {"properties":{"slot_id":{"type":"string","maxLength":64,"minLength":1,"title":"Slot Id"}},"additionalProperties":false,"type":"object","required":["slot_id"],"title":"MobileHandoffBookingCreateRequest","description":"Book exactly one prior server-issued availability snapshot.\n\nNotes and attendee fields are deliberately absent. The server derives the contact's\ncurrent invitation identity from the authorized carrier handoff, and v1 never turns a\nreceptionist phone into a generic calendar-event composer."}
- MobileHandoffBookingReceipt: {"properties":{"id":{"type":"string","title":"Id"},"handoff_id":{"type":"string","title":"Handoff Id"},"starts_at":{"type":"string","format":"date-time","title":"Starts At"},"ends_at":{"type":"string","format":"date-time","title":"Ends At"},"timezone":{"type":"string","title":"Timezone"},"status":{"type":"string","enum":["calendar_event_created","appointment_materialized"],"title":"Status"},"appointment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Appointment Id"},"booked_at":{"type":"string","format":"date-time","title":"Booked At"},"materialized_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Materialized At"}},"type":"object","required":["id","handoff_id","starts_at","ends_at","timezone","status","booked_at"],"title":"MobileHandoffBookingReceipt","description":"A privacy-minimized receipt for one server-authorized handoff booking."}
- MobileHandoffBookingSlot: {"properties":{"id":{"type":"string","title":"Id"},"starts_at":{"type":"string","format":"date-time","title":"Starts At"},"ends_at":{"type":"string","format":"date-time","title":"Ends At"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"}},"type":"object","required":["id","starts_at","ends_at","expires_at"],"title":"MobileHandoffBookingSlot","description":"One opaque, short-lived calendar slot the current member may choose."}
- MobileHandoffCapability: {"properties":{"available":{"type":"boolean","title":"Available"},"handoff_feature_enabled":{"type":"boolean","title":"Handoff Feature Enabled"},"verified_byo_twilio":{"type":"boolean","title":"Verified Byo Twilio"},"active_transfer_destinations":{"type":"integer","minimum":0,"title":"Active Transfer Destinations"},"blockers":{"items":{"type":"string"},"type":"array","title":"Blockers"}},"type":"object","required":["available","handoff_feature_enabled","verified_byo_twilio","active_transfer_destinations"],"title":"MobileHandoffCapability","description":"Whether the tenant can truthfully offer cellular handoff controls right now."}
- MobileInboundVoicemail: {"properties":{"id":{"type":"string","title":"Id"},"status":{"type":"string","enum":["recording","ready","importing","available","absent","failed"],"title":"Status"},"duration_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration Seconds"},"received_at":{"type":"string","format":"date-time","title":"Received At"},"completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Completed At"},"failure_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failure Code"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","status","received_at","updated_at"],"title":"MobileInboundVoicemail","description":"A mobile voicemail receipt with no caller, contact, or storage identity.\n\nAudio remains behind the authenticated signed-recording redirect. This receipt detail\ndoes not manufacture a call reference, duplicate a recording identifier, or expose any\nE.164 address."}
- MobileInsightCard: {"properties":{"key":{"type":"string","enum":["handoff_success","handoff_timing","fallback_rate","booking_conversion","voicemail_aging","coverage_health","emergency_override_duration"],"title":"Key"},"state":{"type":"string","enum":["available","insufficient_data","unavailable"],"title":"State"},"unit":{"type":"string","enum":["basis_points","seconds"],"title":"Unit"},"current_value":{"anyOf":[{"type":"integer","minimum":0},{"type":"null"}],"title":"Current Value"},"previous_value":{"anyOf":[{"type":"integer","minimum":0},{"type":"null"}],"title":"Previous Value"},"delta":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Delta"},"current_sample_size":{"type":"integer","minimum":0,"title":"Current Sample Size"},"previous_sample_size":{"type":"integer","minimum":0,"title":"Previous Sample Size"},"detail_target":{"type":"string","enum":["inbox","activity","override","team","integration"],"title":"Detail Target"}},"additionalProperties":false,"type":"object","required":["key","state","unit","current_sample_size","previous_sample_size","detail_target"],"title":"MobileInsightCard","description":"A finite, aggregate-only comparison card.\n\nRates are basis points rather than floats (``10_000`` is 100%) so an API/client cannot\naccidentally serialize non-finite values. A card is ``available`` only when both\nequal-length windows have a meaningful sample; otherwise values stay null and the\nsample counts explain why the comparison is not displayed as zero."}
- MobileInsightsWindow: {"properties":{"start":{"type":"string","format":"date-time","title":"Start"},"end":{"type":"string","format":"date-time","title":"End"}},"additionalProperties":false,"type":"object","required":["start","end"],"title":"MobileInsightsWindow","description":"One fixed, half-open comparative interval in UTC."}
- MobileIntegrationHealthIncident: {"properties":{"capability":{"type":"string","enum":["google_calendar_connection","booking_readiness","webhook_delivery"],"title":"Capability"},"health_status":{"type":"string","enum":["attention","unavailable"],"title":"Health Status"},"last_known_health_at":{"type":"string","format":"date-time","title":"Last Known Health At"},"recheck_action":{"type":"string","enum":["browser","support"],"title":"Recheck Action"},"destination":{"type":"string","enum":["calendar","webhooks","integrations","support"],"title":"Destination"}},"additionalProperties":false,"type":"object","required":["capability","health_status","last_known_health_at","recheck_action","destination"],"title":"MobileIntegrationHealthIncident","description":"One current, privacy-minimized integration repair item.\n\n``last_known_health_at`` comes from the tenant's durable integration or delivery state;\nit is never an invented client-side timestamp. The fixed action/destination pairs are\nnamed product repair surfaces, not reflected provider, endpoint, or support URLs."}
- MobileIntegrationHealthItem: {"properties":{"key":{"type":"string","enum":["google_calendar_connection","booking_readiness","crm","webhook_delivery"],"title":"Key"},"status":{"type":"string","enum":["healthy","attention","not_configured","not_observed","unavailable"],"title":"Status"},"reason_code":{"type":"string","enum":["calendar_connection_healthy","calendar_reconnect_required","calendar_not_connected","calendar_adapter_unavailable","booking_ready","booking_calendar_reconnect_required","booking_calendar_not_connected","booking_unavailable","crm_adapter_not_shipped","webhook_not_configured","webhook_endpoint_disabled","webhook_delivery_retrying","webhook_delivery_exhausted","webhook_delivery_unknown_state","webhook_delivery_in_progress","webhook_delivery_healthy","webhook_delivery_not_observed"],"title":"Reason Code"},"action":{"type":"string","enum":["none","browser","support"],"title":"Action"},"destination":{"type":"string","enum":["calendar","webhooks","integrations","support"],"title":"Destination"}},"additionalProperties":false,"type":"object","required":["key","status","reason_code","action","destination"],"title":"MobileIntegrationHealthItem","description":"One no-PII integration-health outcome and its safe repair surface."}
- MobileIntegrationHealthRegistry: {"properties":{"generated_at":{"type":"string","format":"date-time","title":"Generated At"},"items":{"items":{"$ref":"#/components/schemas/MobileIntegrationHealthItem"},"type":"array","title":"Items"}},"additionalProperties":false,"type":"object","required":["generated_at","items"],"title":"MobileIntegrationHealthRegistry","description":"The complete, safe Integration Health registry for one mobile workspace leader."}
- MobileIntegrationHealthTimeline: {"properties":{"generated_at":{"type":"string","format":"date-time","title":"Generated At"},"incidents":{"items":{"$ref":"#/components/schemas/MobileIntegrationHealthIncident"},"type":"array","title":"Incidents"}},"additionalProperties":false,"type":"object","required":["generated_at","incidents"],"title":"MobileIntegrationHealthTimeline","description":"Current evidence-backed integration incidents, newest known health state first."}
- MobileIntegrationReadiness: {"properties":{"calendar":{"$ref":"#/components/schemas/MobileCalendarIntegrationReadiness"}},"type":"object","required":["calendar"],"title":"MobileIntegrationReadiness","description":"Read-only, tenant-scoped integration health for an Admin or Manager.\n\nNew provider families may be added as separately minimized children. Do not turn this\nresponse into a copy of the browser integration-management API."}
- MobileLiveCall: {"properties":{"call_id":{"type":"string","title":"Call Id"},"state":{"type":"string","enum":["active","handoff_pending"],"title":"State"},"phone_number_id":{"type":"string","title":"Phone Number Id"},"agent_version_id":{"type":"string","title":"Agent Version Id"},"connected_at":{"type":"string","format":"date-time","title":"Connected At"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"}},"type":"object","required":["call_id","state","phone_number_id","agent_version_id","connected_at","expires_at"],"title":"MobileLiveCall","description":"An active inbound receptionist call, with no caller PII in the pushable shape."}
- MobileLiveHandoff: {"properties":{"id":{"type":"string","title":"Id"},"call_id":{"type":"string","title":"Call Id"},"call_state":{"type":"string","enum":["active","handoff_pending","transferred","ended","expired"],"title":"Call State"},"status":{"type":"string","enum":["requested","dispatching","offered","accepted","redirecting","redirected","snapshot_ready","completed","declined","timed_out","carrier_failed","reconciliation_required","cancelled"],"title":"Status"},"failure_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failure Code"},"requested_at":{"type":"string","format":"date-time","title":"Requested At"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"rep_joined_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Rep Joined At"},"caller_redirected_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Caller Redirected At"},"completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Completed At"}},"type":"object","required":["id","call_id","call_state","status","requested_at","expires_at"],"title":"MobileLiveHandoff","description":"The current carrier-confirmed lifecycle of one authorized handoff.\n\nThis is intentionally status-only. It contains no carrier operation identifiers,\ntelephone destination, handoff summary, transcript, recording URL, or media state.\nIn particular, it is not an action surface: a rep joining the carrier consult remains\nthe only way an ``accepted`` transition can occur."}
- MobileNotificationCategoryPreferences: {"properties":{"operations":{"type":"boolean","title":"Operations","default":true},"booking":{"type":"boolean","title":"Booking","default":true},"integration":{"type":"boolean","title":"Integration","default":true},"subscription":{"type":"boolean","title":"Subscription","default":true},"coach":{"type":"boolean","title":"Coach","default":true}},"additionalProperties":false,"type":"object","title":"MobileNotificationCategoryPreferences","description":"Routine delivery categories; each remains an ID-only, server-authorized alert."}
- MobileNotificationPreferences: {"properties":{"urgent_alerts_enabled":{"type":"boolean","title":"Urgent Alerts Enabled","default":false},"categories":{"$ref":"#/components/schemas/MobileNotificationCategoryPreferences"},"quiet_hours":{"anyOf":[{"$ref":"#/components/schemas/MobileNotificationQuietHours"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"MobileNotificationPreferences","description":"Per-device native-alert preferences; no critical-alert/DND-bypass option exists."}
- MobileNotificationPreferencesUpdateRequest: {"properties":{"urgent_alerts_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Urgent Alerts Enabled"},"categories":{"anyOf":[{"$ref":"#/components/schemas/MobileNotificationCategoryPreferences"},{"type":"null"}]},"quiet_hours":{"anyOf":[{"$ref":"#/components/schemas/MobileNotificationQuietHours"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"MobileNotificationPreferencesUpdateRequest"}
- MobileNotificationQuietHours: {"properties":{"enabled":{"type":"boolean","title":"Enabled"},"start_time":{"type":"string","pattern":"^(?:[01][0-9]|2[0-3]):[0-5][0-9]$","title":"Start Time"},"end_time":{"type":"string","pattern":"^(?:[01][0-9]|2[0-3]):[0-5][0-9]$","title":"End Time"},"timezone":{"type":"string","maxLength":120,"minLength":1,"title":"Timezone"}},"additionalProperties":false,"type":"object","required":["enabled","start_time","end_time","timezone"],"title":"MobileNotificationQuietHours","description":"A local-time quiet window for routine notifications only."}
- MobileNotificationTestReceipt: {"properties":{"accepted":{"type":"boolean","title":"Accepted"}},"additionalProperties":false,"type":"object","required":["accepted"],"title":"MobileNotificationTestReceipt","description":"A generic current-device delivery test acknowledgement with no push route data."}
- MobileOperationTarget: {"properties":{"kind":{"type":"string","enum":["task","activity","handoff","emergency_override","inbox"],"title":"Kind"},"resource_id":{"anyOf":[{"type":"string","maxLength":128,"minLength":1},{"type":"null"}],"title":"Resource Id"},"state":{"type":"string","enum":["available","resolved","expired","access_changed"],"title":"State"}},"additionalProperties":false,"type":"object","required":["kind","state"],"title":"MobileOperationTarget","description":"A server-authorized notification destination, never a client-inferred route."}
- MobileOperationTask: {"properties":{"id":{"type":"string","title":"Id"},"category":{"type":"string","enum":["handoff","emergency_override","voicemail","booking","integration","receptionist_health"],"title":"Category"},"severity":{"type":"string","enum":["critical","high","normal","low"],"title":"Severity"},"state":{"type":"string","enum":["open","acknowledged","resolved"],"title":"State"},"title":{"type":"string","maxLength":200,"minLength":1,"title":"Title"},"detail":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Detail"},"source_target":{"$ref":"#/components/schemas/MobileOperationTaskSourceTarget"},"next_action":{"type":"string","enum":["review_handoff","review_activity","review_override","open_integration_settings","contact_support"],"title":"Next Action"},"assignee":{"anyOf":[{"$ref":"#/components/schemas/MobileOperationTaskActor"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"due_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Due At"},"acknowledged_by":{"anyOf":[{"$ref":"#/components/schemas/MobileOperationTaskActor"},{"type":"null"}]},"acknowledged_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Acknowledged At"},"resolved_by":{"anyOf":[{"$ref":"#/components/schemas/MobileOperationTaskActor"},{"type":"null"}]},"resolved_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Resolved At"},"version":{"type":"integer","minimum":1,"title":"Version"}},"additionalProperties":false,"type":"object","required":["id","category","severity","state","title","source_target","next_action","created_at","updated_at","version"],"title":"MobileOperationTask","description":"One durable operational follow-up rendered without customer/call content."}
- MobileOperationTaskAcknowledgeRequest: {"properties":{"expected_version":{"type":"integer","minimum":1,"title":"Expected Version"}},"additionalProperties":false,"type":"object","required":["expected_version"],"title":"MobileOperationTaskAcknowledgeRequest"}
- MobileOperationTaskActor: {"properties":{"id":{"type":"string","maxLength":128,"minLength":1,"title":"Id"},"display_name":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Display Name"}},"additionalProperties":false,"type":"object","required":["id"],"title":"MobileOperationTaskActor","description":"A permitted staff label, never an account, device, email, or destination."}
- MobileOperationTaskAssignRequest: {"properties":{"expected_version":{"type":"integer","minimum":1,"title":"Expected Version"},"assignee_membership_id":{"anyOf":[{"type":"string","maxLength":128,"minLength":1},{"type":"null"}],"title":"Assignee Membership Id"}},"additionalProperties":false,"type":"object","required":["expected_version"],"title":"MobileOperationTaskAssignRequest"}
- MobileOperationTaskPage: {"properties":{"items":{"items":{"$ref":"#/components/schemas/MobileOperationTask"},"type":"array","title":"Items"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More","default":false},"open_count":{"type":"integer","minimum":0,"title":"Open Count"}},"additionalProperties":false,"type":"object","required":["open_count"],"title":"MobileOperationTaskPage","description":"Cursor page plus the visible scope's current unresolved workload count."}
- MobileOperationTaskResolveRequest: {"properties":{"expected_version":{"type":"integer","minimum":1,"title":"Expected Version"}},"additionalProperties":false,"type":"object","required":["expected_version"],"title":"MobileOperationTaskResolveRequest"}
- MobileOperationTaskSourceTarget: {"properties":{"kind":{"type":"string","enum":["activity","handoff","emergency_override","inbox"],"title":"Kind"},"resource_id":{"type":"string","maxLength":128,"minLength":1,"title":"Resource Id"}},"additionalProperties":false,"type":"object","required":["kind","resource_id"],"title":"MobileOperationTaskSourceTarget","description":"An opaque source reference used only for authenticated native navigation."}
- MobileReadiness: {"properties":{"role":{"type":"string","enum":["admin","manager","rep"],"title":"Role"},"generated_at":{"type":"string","format":"date-time","title":"Generated At"},"items":{"items":{"$ref":"#/components/schemas/MobileReadinessItem"},"type":"array","title":"Items"}},"additionalProperties":false,"type":"object","required":["role","generated_at","items"],"title":"MobileReadiness","description":"The current user's role-specific field-operations checklist."}
- MobileReadinessItem: {"properties":{"key":{"type":"string","enum":["carrier_handoff","staffing_eligibility","schedule","calendar_booking","notifications","integration_health","transfer_destination","availability"],"title":"Key"},"state":{"type":"string","enum":["ready","attention","unavailable"],"title":"State"},"reason_code":{"type":"string","pattern":"^[a-z0-9_]{1,80}$","title":"Reason Code"},"next_action":{"type":"string","enum":["native","browser","support"],"title":"Next Action"},"destination":{"type":"string","enum":["today","team","schedule","notifications","integrations","support"],"title":"Destination"}},"additionalProperties":false,"type":"object","required":["key","state","reason_code","next_action","destination"],"title":"MobileReadinessItem","description":"One no-PII readiness outcome and the surface that owns its next action."}
- MobileReceptionistActivity: {"properties":{"resource_id":{"type":"string","title":"Resource Id"},"kind":{"type":"string","enum":["live_call","call","voicemail","appointment","handoff"],"title":"Kind"},"status":{"type":"string","title":"Status"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Id"},"phone_number_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number Id"},"agent_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Version Id"}},"type":"object","required":["resource_id","kind","status","occurred_at"],"title":"MobileReceptionistActivity","description":"One privacy-minimized receptionist event for the mobile activity timeline.\n\n``resource_id`` is the source resource (for example a handoff request or voicemail\nreceipt). A client that needs a transcript, voicemail/appointment detail, or recording\nfollows the native authenticated activity-detail endpoint; this projection deliberately\ndoes not become an alternate media-delivery surface."}
- MobileReceptionistAuthorizedTest: {"properties":{"authorized_at":{"type":"string","format":"date-time","title":"Authorized At"},"result":{"anyOf":[{"type":"string","enum":["passed","failed"]},{"type":"null"}],"title":"Result"}},"additionalProperties":false,"type":"object","required":["authorized_at"],"title":"MobileReceptionistAuthorizedTest","description":"Evidence that mobile opened an authorized system-dialer test target.\n\nAuthorization does not prove that the carrier call connected, much less that the\nreceptionist completed it. ``result`` therefore stays nullable until a distinct,\ndurable carrier-result evidence path exists."}
- MobileReceptionistCoverageSummary: {"properties":{"state":{"type":"string","enum":["covered","uncovered","not_configured","unavailable"],"title":"State"},"active_rep_count":{"anyOf":[{"type":"integer","maximum":20,"minimum":0},{"type":"null"}],"title":"Active Rep Count"}},"additionalProperties":false,"type":"object","required":["state"],"title":"MobileReceptionistCoverageSummary","description":"Current direct-rep coverage, after the runtime's safety gates are intersected."}
- MobileReceptionistEmergencyOverrideSummary: {"properties":{"state":{"type":"string","enum":["none","active","cleared","expired","invalid","unavailable"],"title":"State"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"}},"additionalProperties":false,"type":"object","required":["state"],"title":"MobileReceptionistEmergencyOverrideSummary","description":"Current override state without its routing mode, destination, or control id."}
- MobileReceptionistHub: {"properties":{"generated_at":{"type":"string","format":"date-time","title":"Generated At"},"items":{"items":{"$ref":"#/components/schemas/MobileReceptionistHubItem"},"type":"array","maxItems":100,"title":"Items"}},"additionalProperties":false,"type":"object","required":["generated_at"],"title":"MobileReceptionistHub","description":"Live Receptionists hub data for Admins and Managers."}
- MobileReceptionistHubItem: {"properties":{"phone_number_id":{"type":"string","title":"Phone Number Id"},"phone_e164":{"type":"string","pattern":"^\\+[1-9]\\d{6,14}$","title":"Phone E164"},"published_state":{"type":"string","enum":["published","not_published"],"title":"Published State"},"schedule":{"$ref":"#/components/schemas/MobileReceptionistScheduleSummary"},"emergency_override":{"$ref":"#/components/schemas/MobileReceptionistEmergencyOverrideSummary"},"coverage":{"$ref":"#/components/schemas/MobileReceptionistCoverageSummary"},"last_authorized_mobile_test":{"anyOf":[{"$ref":"#/components/schemas/MobileReceptionistAuthorizedTest"},{"type":"null"}]},"open_follow_up_count":{"type":"integer","minimum":0,"title":"Open Follow Up Count"}},"additionalProperties":false,"type":"object","required":["phone_number_id","phone_e164","published_state","schedule","emergency_override","coverage","open_follow_up_count"],"title":"MobileReceptionistHubItem","description":"A privacy-safe, per-number receptionist operations card."}
- MobileReceptionistInsights: {"properties":{"generated_at":{"type":"string","format":"date-time","title":"Generated At"},"days":{"type":"integer","enum":[7,30,90],"title":"Days"},"current_window":{"$ref":"#/components/schemas/MobileInsightsWindow"},"previous_window":{"$ref":"#/components/schemas/MobileInsightsWindow"},"cards":{"items":{"$ref":"#/components/schemas/MobileInsightCard"},"type":"array","maxItems":7,"minItems":7,"title":"Cards"}},"additionalProperties":false,"type":"object","required":["generated_at","days","current_window","previous_window","cards"],"title":"MobileReceptionistInsights","description":"The complete bounded comparative Insights response for a mobile workspace leader."}
- MobileReceptionistMetrics: {"properties":{"start":{"type":"string","format":"date-time","title":"Start"},"end":{"type":"string","format":"date-time","title":"End"},"inbound_calls_received":{"type":"integer","minimum":0,"title":"Inbound Calls Received"},"handoffs_requested":{"type":"integer","minimum":0,"title":"Handoffs Requested"},"handoffs_accepted":{"type":"integer","minimum":0,"title":"Handoffs Accepted"},"handoffs_redirected":{"type":"integer","minimum":0,"title":"Handoffs Redirected"},"handoffs_completed":{"type":"integer","minimum":0,"title":"Handoffs Completed"},"handoffs_declined":{"type":"integer","minimum":0,"title":"Handoffs Declined"},"handoffs_timed_out":{"type":"integer","minimum":0,"title":"Handoffs Timed Out"},"handoffs_failed":{"type":"integer","minimum":0,"title":"Handoffs Failed"},"appointments_created":{"type":"integer","minimum":0,"title":"Appointments Created"}},"type":"object","required":["start","end","inbound_calls_received","handoffs_requested","handoffs_accepted","handoffs_redirected","handoffs_completed","handoffs_declined","handoffs_timed_out","handoffs_failed","appointments_created"],"title":"MobileReceptionistMetrics","description":"A bounded, aggregate-only receptionist operations report.\n\nThe response is a single tenant-wide total for the half-open ``[start, end)`` window;\nit never exposes source call, contact, carrier, appointment, or transfer rows. Each\nhandoff count is attributed to the durable lifecycle timestamp named by the field,\nrather than to the request's current status."}
- MobileReceptionistNumber: {"properties":{"id":{"type":"string","title":"Id"},"phone_e164":{"type":"string","title":"Phone E164"},"status":{"type":"string","title":"Status"}},"type":"object","required":["id","phone_e164","status"],"title":"MobileReceptionistNumber","description":"One tenant-owned policy number selectable by mobile routing controls.\n\n``phone_e164`` is the business's own inbound number, not caller/contact data. The\nmobile app needs it to distinguish its own receptionist lines before it reads or\nchanges a schedule or emergency override; no customer ANI is exposed here."}
- MobileReceptionistScheduleSummary: {"properties":{"state":{"type":"string","enum":["open","closed","unavailable"],"title":"State"}},"additionalProperties":false,"type":"object","required":["state"],"title":"MobileReceptionistScheduleSummary","description":"The authoritative policy schedule answer when this line has one.\n\nA direct-agent line has no tenant business-hours document, and a malformed/stale\npolicy is not safe to interpret in the app. Both are intentionally ``unavailable``."}
- MobileReceptionistStudioPatch: {"properties":{"expected_version":{"type":"integer","minimum":1,"title":"Expected Version"},"business_facts":{"additionalProperties":{"type":"string"},"propertyNames":{"enum":["business_name","service_summary","service_area","callback_preference"]},"type":"object","maxProperties":4,"title":"Business Facts"},"short_greeting":{"anyOf":[{"type":"string","maxLength":180},{"type":"null"}],"title":"Short Greeting"},"escalation_preference":{"type":"string","enum":["standard","handoff_when_available","booking_first"],"title":"Escalation Preference","default":"standard"},"temporary_status":{"anyOf":[{"$ref":"#/components/schemas/MobileTemporaryCustomerStatus"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["expected_version"],"title":"MobileReceptionistStudioPatch","description":"A complete, version-checked mobile Studio state replacement.\n\nRequiring all editable state in the request makes an explicit removal distinguishable\nfrom a dropped field on a weak network. The native app starts from a live read and\nsends this exact object through the fresh browser-proof binding."}
- MobileReceptionistStudioSummary: {"properties":{"phone_number_id":{"type":"string","title":"Phone Number Id"},"has_published_receptionist":{"type":"boolean","title":"Has Published Receptionist"},"business_facts":{"additionalProperties":{"type":"string"},"propertyNames":{"enum":["business_name","service_summary","service_area","callback_preference"]},"type":"object","title":"Business Facts"},"short_greeting":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Short Greeting"},"escalation_preference":{"type":"string","enum":["standard","handoff_when_available","booking_first"],"title":"Escalation Preference","default":"standard"},"temporary_status":{"anyOf":[{"$ref":"#/components/schemas/MobileTemporaryCustomerStatus"},{"type":"null"}]},"version":{"type":"integer","minimum":1,"title":"Version"},"updated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["phone_number_id","has_published_receptionist","version"],"title":"MobileReceptionistStudioSummary","description":"One safe receptionist fact-draft projection for native operations views."}
- MobileReceptionistTestTarget: {"properties":{"phone_number_id":{"type":"string","title":"Phone Number Id"},"phone_e164":{"type":"string","title":"Phone E164"},"carrier_charge_notice":{"type":"string","title":"Carrier Charge Notice"}},"type":"object","required":["phone_number_id","phone_e164","carrier_charge_notice"],"title":"MobileReceptionistTestTarget","description":"A one-use-safe dial target for testing an already-published business number.\n\nThe app merely opens the system dialer after this authorization result. It never\nreceives call audio, call-log data, SIM data, or any ability to control the human leg."}
- MobileSetupItem: {"properties":{"key":{"type":"string","enum":["team_coverage","transfer_destination","availability","schedule","notifications","calendar_booking","receptionist","capacity","portal"],"title":"Key"},"state":{"type":"string","enum":["ready","attention","blocked","unavailable"],"title":"State"},"title":{"type":"string","maxLength":120,"minLength":1,"title":"Title"},"detail":{"type":"string","maxLength":280,"minLength":1,"title":"Detail"},"next_action":{"type":"string","enum":["native","browser","support","none"],"title":"Next Action"},"destination":{"type":"string","enum":["team","schedule","notifications","integrations","workspace","subscription","portal","support"],"title":"Destination"}},"additionalProperties":false,"type":"object","required":["key","state","title","detail","next_action","destination"],"title":"MobileSetupItem","description":"One server-derived next action; a client cannot check it off locally."}
- MobileStepUpBeginRequest: {"properties":{"action":{"type":"string","enum":["schedule_update","emergency_override_set","emergency_override_clear","team_member_update","team_invitation_create","team_invitation_revoke","transfer_destination_confirm","device_revoke","call_ai_start","billing_budget_update","billing_portal_launch","receptionist_studio_update"],"title":"Action"},"target":{"type":"string","maxLength":160,"minLength":1,"title":"Target"},"payload_hash":{"type":"string","maxLength":64,"minLength":64,"title":"Payload Hash"},"callback_uri":{"type":"string","maxLength":2048,"minLength":8,"title":"Callback Uri"}},"additionalProperties":false,"type":"object","required":["action","target","payload_hash","callback_uri"],"title":"MobileStepUpBeginRequest","description":"A native app starts a proof for exactly one high-impact command."}
- MobileStepUpBeginResponse: {"properties":{"request_id":{"type":"string","title":"Request Id"},"portal_url":{"type":"string","title":"Portal Url"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"}},"additionalProperties":false,"type":"object","required":["request_id","portal_url","expires_at"],"title":"MobileStepUpBeginResponse"}
- MobileStepUpBrowserRequest: {"properties":{"request_id":{"type":"string","title":"Request Id"},"action":{"type":"string","enum":["schedule_update","emergency_override_set","emergency_override_clear","team_member_update","team_invitation_create","team_invitation_revoke","transfer_destination_confirm","device_revoke","call_ai_start","billing_budget_update","billing_portal_launch","receptionist_studio_update"],"title":"Action"},"action_label":{"type":"string","title":"Action Label"},"intent_summary":{"$ref":"#/components/schemas/MobileStepUpIntentSummary"},"verification_method":{"type":"string","enum":["password","email_link"],"title":"Verification Method"},"status":{"type":"string","enum":["pending","awaiting_email","verified","consumed","expired"],"title":"Status"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"email_expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Email Expires At"}},"additionalProperties":false,"type":"object","required":["request_id","action","action_label","intent_summary","verification_method","status","expires_at"],"title":"MobileStepUpBrowserRequest","description":"Safe browser projection; no payload, callback, secret or raw account detail."}
- MobileStepUpCompletion: {"properties":{"callback_url":{"type":"string","title":"Callback Url"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"}},"additionalProperties":false,"type":"object","required":["callback_url","expires_at"],"title":"MobileStepUpCompletion","description":"A browser redirects to this fragment-bearing URL immediately after receipt."}
- MobileStepUpEmailConfirmationRequest: {"properties":{"token":{"type":"string","maxLength":512,"minLength":32,"title":"Token"}},"additionalProperties":false,"type":"object","required":["token"],"title":"MobileStepUpEmailConfirmationRequest"}
- MobileStepUpEmailStarted: {"properties":{"message":{"type":"string","title":"Message"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"}},"additionalProperties":false,"type":"object","required":["message","expires_at"],"title":"MobileStepUpEmailStarted"}
- MobileStepUpIntentSummary: {"properties":{"subject":{"type":"string","maxLength":120,"minLength":1,"title":"Subject"},"effect":{"type":"string","maxLength":220,"minLength":1,"title":"Effect"},"target_reference":{"anyOf":[{"type":"string","maxLength":32,"minLength":4},{"type":"null"}],"title":"Target Reference"}},"additionalProperties":false,"type":"object","required":["subject","effect"],"title":"MobileStepUpIntentSummary","description":"Privacy-safe browser copy describing one already-bound native command.\n\nThe browser must never receive the mutation body (which can contain a phone number,\nemail address, contact selection, routing destination, or schedule details). These\nthree strings are selected exclusively by the control API from the durable action and\nopaque resource reference. They give a person enough context to recognize the class\nand scope of the change without turning the browser proof into another data surface."}
- MobileStepUpPasswordRequest: {"properties":{"password":{"type":"string","maxLength":1024,"minLength":1,"title":"Password"}},"additionalProperties":false,"type":"object","required":["password"],"title":"MobileStepUpPasswordRequest"}
- MobileStudioIntelligenceSnapshotRequest: {"properties":{"agent_id":{"anyOf":[{"type":"string","maxLength":128,"minLength":1},{"type":"null"}],"title":"Agent Id"},"expected_studio_version":{"type":"integer","minimum":1,"title":"Expected Studio Version"}},"additionalProperties":false,"type":"object","required":["expected_studio_version"],"title":"MobileStudioIntelligenceSnapshotRequest","description":"Copy one exact Studio revision into the canonical intelligence review queue.\n\nThis is deliberately a snapshot import, not a hidden prompt edit and not a publish\ncommand. The resulting source starts pending and must be reviewed/cited through the\nordinary Receptionist Intelligence workspace before it can become callable knowledge."}
- MobileStudioIntelligenceSnapshotResult: {"properties":{"agent_id":{"type":"string","title":"Agent Id"},"phone_number_id":{"type":"string","title":"Phone Number Id"},"studio_version":{"type":"integer","minimum":1,"title":"Studio Version"},"intelligence_source_id":{"type":"string","title":"Intelligence Source Id"},"source_review_status":{"type":"string","enum":["pending","approved","rejected"],"title":"Source Review Status"},"captured_at":{"type":"string","format":"date-time","title":"Captured At"},"requires_customer_review":{"type":"boolean","title":"Requires Customer Review"},"canonical_draft_changed":{"type":"boolean","const":false,"title":"Canonical Draft Changed","default":false}},"additionalProperties":false,"type":"object","required":["agent_id","phone_number_id","studio_version","intelligence_source_id","source_review_status","captured_at","requires_customer_review"],"title":"MobileStudioIntelligenceSnapshotResult","description":"The canonical source created from a bounded Mobile Studio revision."}
- MobileSubscriptionHealth: {"properties":{"plan_label":{"type":"string","maxLength":160,"minLength":1,"title":"Plan Label"},"status":{"type":"string","enum":["trialing","active","past_due","paused","canceled","unavailable"],"title":"Status"},"usage_label":{"anyOf":[{"type":"string","maxLength":160},{"type":"null"}],"title":"Usage Label"},"capacity_label":{"anyOf":[{"type":"string","maxLength":160},{"type":"null"}],"title":"Capacity Label"},"payment_attention":{"type":"string","enum":["none","attention","unavailable"],"title":"Payment Attention"},"next_renewal_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Next Renewal At"},"budget":{"anyOf":[{"$ref":"#/components/schemas/MobileBillingBudget"},{"type":"null"}]},"invoices":{"items":{"$ref":"#/components/schemas/MobileBillingInvoiceSummary"},"type":"array","maxItems":3,"title":"Invoices"},"generated_at":{"type":"string","format":"date-time","title":"Generated At"}},"additionalProperties":false,"type":"object","required":["plan_label","status","payment_attention","generated_at"],"title":"MobileSubscriptionHealth","description":"The full native billing read for Admin/Manager, still safe for an app cache policy."}
- MobileTeamMemberSummary: {"properties":{"id":{"type":"string","title":"Id"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"role":{"$ref":"#/components/schemas/TenantMembershipRole"},"human_rep_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Human Rep Id"},"status":{"type":"string","enum":["active","suspended","revoked"],"title":"Status"},"forward_eligible":{"type":"boolean","title":"Forward Eligible","default":false}},"type":"object","required":["id","role","status"],"title":"MobileTeamMemberSummary","description":"The capability-gated mobile team directory projection.\n\nA manager needs a recognizable staff label plus operational role, membership state, the\nserver-owned rep link used by handoff routing, and a safe selector fact for emergency\nforwarding. Account/tenant ids and session invalidation timestamps are intentionally\nabsent: they are not team-management inputs and would expose identity/security internals\nto every directory reader."}
- MobileTemporaryCustomerStatus: {"properties":{"template":{"type":"string","enum":["service_update","special_hours","weather_delay"],"title":"Template"},"message":{"type":"string","maxLength":180,"minLength":1,"title":"Message"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"}},"additionalProperties":false,"type":"object","required":["template","message","expires_at"],"title":"MobileTemporaryCustomerStatus","description":"One bounded, expiring proposed public-status update.\n\nThe finite template is shown in the native preview and recorded in the audit trail.\nThe message remains concise, cannot alter policy/tool wording, and must be reviewed in\ncanonical intelligence before it can be spoken. Its expiry travels with the source\nevidence so reviewers do not mistake a temporary notice for durable business knowledge."}
- MobileTokenRequest: {"properties":{"grant_type":{"type":"string","enum":["authorization_code","refresh_token"],"title":"Grant Type"},"client_id":{"type":"string","const":"vocapable-mobile","title":"Client Id","default":"vocapable-mobile"},"authorization_code":{"anyOf":[{"type":"string","maxLength":512,"minLength":43},{"type":"null"}],"title":"Authorization Code"},"code_verifier":{"anyOf":[{"type":"string","maxLength":128,"minLength":43},{"type":"null"}],"title":"Code Verifier"},"redirect_uri":{"anyOf":[{"type":"string","maxLength":2048,"minLength":1},{"type":"null"}],"title":"Redirect Uri"},"refresh_token":{"anyOf":[{"type":"string","maxLength":512,"minLength":43},{"type":"null"}],"title":"Refresh Token"}},"additionalProperties":false,"type":"object","required":["grant_type"],"title":"MobileTokenRequest","description":"The two native token-exchange grants; no API key may satisfy either one."}
- MobileTokenResponse: {"properties":{"access_token":{"type":"string","title":"Access Token"},"refresh_token":{"type":"string","title":"Refresh Token"},"token_type":{"type":"string","const":"Bearer","title":"Token Type","default":"Bearer"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"tenant_id":{"type":"string","title":"Tenant Id"},"membership_id":{"type":"string","title":"Membership Id"},"device_id":{"type":"string","title":"Device Id"},"role":{"$ref":"#/components/schemas/TenantMembershipRole"}},"additionalProperties":false,"type":"object","required":["access_token","refresh_token","expires_at","tenant_id","membership_id","device_id","role"],"title":"MobileTokenResponse","description":"Tokens returned once over TLS to the native app's secure device storage."}
- MobileTransferDestinationVerificationCheckRequest: {"properties":{"verification_id":{"type":"string","maxLength":128,"minLength":1,"title":"Verification Id"},"code":{"type":"string","pattern":"^\\d{6}$","title":"Code"}},"additionalProperties":false,"type":"object","required":["verification_id","code"],"title":"MobileTransferDestinationVerificationCheckRequest","description":"One bounded OTP attempt for one exact transfer-destination challenge.\n\n``verification_id`` is deliberately part of the native request even though the\nmember id is in the route. A fresh browser proof is bound to this opaque challenge\nid, rather than the six-digit OTP, so a resend or replacement cannot make an older\nproof authorize the newest candidate destination."}
- MobileTransferDestinationVerificationStartRequest: {"properties":{"destination_e164":{"type":"string","pattern":"^\\+[1-9]\\d{7,14}$","title":"Destination E164"},"transfer_enabled":{"type":"boolean","title":"Transfer Enabled","default":true}},"additionalProperties":false,"type":"object","required":["destination_e164"],"title":"MobileTransferDestinationVerificationStartRequest","description":"Start an OTP proof before a linked rep's transfer phone may change.\n\nThe candidate number is accepted only here and is never echoed in a response or push\npayload. The server binds the proof to the member's currently linked human rep, so a\nverified code cannot be replayed to alter a different person's destination."}
- MobileTransferDestinationVerificationSummary: {"properties":{"id":{"type":"string","title":"Id"},"membership_id":{"type":"string","title":"Membership Id"},"rep_id":{"type":"string","title":"Rep Id"},"status":{"type":"string","enum":["pending","verified","expired"],"title":"Status"},"transfer_enabled":{"type":"boolean","title":"Transfer Enabled"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"verified_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Verified At"},"verification_attempts":{"type":"integer","minimum":0,"title":"Verification Attempts"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","membership_id","rep_id","status","transfer_enabled","verification_attempts","created_at","updated_at"],"title":"MobileTransferDestinationVerificationSummary","description":"Non-PII progress state for a member's transfer-destination verification."}
- MobileTransferHistoryEntry: {"properties":{"id":{"type":"string","title":"Id"},"call_id":{"type":"string","title":"Call Id"},"requested_by":{"type":"string","enum":["agent","user","system"],"title":"Requested By"},"requested_rep_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Requested Rep Id"},"requested_rep_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Requested Rep Name"},"requested_group_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Requested Group Id"},"resolved_rep_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolved Rep Id"},"resolved_rep_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolved Rep Name"},"mode":{"type":"string","title":"Mode"},"status":{"type":"string","title":"Status"},"failure_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failure Code"},"requested_at":{"type":"string","format":"date-time","title":"Requested At"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Completed At"}},"type":"object","required":["id","call_id","requested_by","mode","status","requested_at","expires_at"],"title":"MobileTransferHistoryEntry","description":"The tenant-safe audit view of one direct-rep handoff request."}
- MobileWorkspaceIdentity: {"properties":{"membership_id":{"type":"string","maxLength":128,"minLength":1,"title":"Membership Id"},"tenant_id":{"type":"string","maxLength":128,"minLength":1,"title":"Tenant Id"},"label":{"type":"string","maxLength":200,"minLength":1,"title":"Label"},"role":{"type":"string","enum":["admin","manager","rep"],"title":"Role"}},"additionalProperties":false,"type":"object","required":["membership_id","tenant_id","label","role"],"title":"MobileWorkspaceIdentity","description":"The active workspace label and role; no user account directory fields."}
- MobileWorkspaceList: {"properties":{"items":{"items":{"$ref":"#/components/schemas/MobileWorkspaceOption"},"type":"array","title":"Items"},"active_membership_id":{"type":"string","maxLength":128,"minLength":1,"title":"Active Membership Id"}},"additionalProperties":false,"type":"object","required":["active_membership_id"],"title":"MobileWorkspaceList"}
- MobileWorkspaceOption: {"properties":{"membership_id":{"type":"string","maxLength":128,"minLength":1,"title":"Membership Id"},"tenant_id":{"type":"string","maxLength":128,"minLength":1,"title":"Tenant Id"},"label":{"type":"string","maxLength":200,"minLength":1,"title":"Label"},"role":{"type":"string","enum":["admin","manager","rep"],"title":"Role"}},"additionalProperties":false,"type":"object","required":["membership_id","tenant_id","label","role"],"title":"MobileWorkspaceOption","description":"A safe native reauthorization choice; it is not a switching credential."}
- MobileWorkspaceOverview: {"properties":{"workspace":{"$ref":"#/components/schemas/MobileWorkspaceIdentity"},"receptionist":{"$ref":"#/components/schemas/MobileWorkspaceReceptionistHealth"},"subscription":{"anyOf":[{"$ref":"#/components/schemas/MobileWorkspaceSubscriptionPreview"},{"type":"null"}]},"generated_at":{"type":"string","format":"date-time","title":"Generated At"}},"additionalProperties":false,"type":"object","required":["workspace","receptionist","generated_at"],"title":"MobileWorkspaceOverview"}
- MobileWorkspaceReceptionistHealth: {"properties":{"state":{"type":"string","enum":["healthy","attention","unavailable"],"title":"State"},"label":{"type":"string","maxLength":120,"minLength":1,"title":"Label"},"detail":{"type":"string","maxLength":280,"minLength":1,"title":"Detail"},"open_task_count":{"type":"integer","minimum":0,"title":"Open Task Count"},"production_ready":{"type":"boolean","title":"Production Ready"}},"additionalProperties":false,"type":"object","required":["state","label","detail","open_task_count","production_ready"],"title":"MobileWorkspaceReceptionistHealth"}
- MobileWorkspaceSetup: {"properties":{"generated_at":{"type":"string","format":"date-time","title":"Generated At"},"items":{"items":{"$ref":"#/components/schemas/MobileSetupItem"},"type":"array","title":"Items"}},"additionalProperties":false,"type":"object","required":["generated_at"],"title":"MobileWorkspaceSetup"}
- MobileWorkspaceSubscriptionPreview: {"properties":{"status":{"type":"string","enum":["trialing","active","past_due","paused","canceled","unavailable"],"title":"Status"},"plan_label":{"type":"string","maxLength":160,"minLength":1,"title":"Plan Label"},"usage_label":{"anyOf":[{"type":"string","maxLength":160},{"type":"null"}],"title":"Usage Label"},"capacity_label":{"anyOf":[{"type":"string","maxLength":160},{"type":"null"}],"title":"Capacity Label"},"spend_cap_progress_bps":{"anyOf":[{"type":"integer","maximum":10000,"minimum":0},{"type":"null"}],"title":"Spend Cap Progress Bps"},"payment_attention":{"type":"string","enum":["none","attention","unavailable"],"title":"Payment Attention"},"next_renewal_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Next Renewal At"}},"additionalProperties":false,"type":"object","required":["status","plan_label","payment_attention"],"title":"MobileWorkspaceSubscriptionPreview","description":"The compact billing/capacity headline allowed on the Workspace Hub."}
- Money: {"properties":{"amount_minor":{"type":"integer","title":"Amount Minor"},"currency":{"type":"string","title":"Currency"}},"type":"object","required":["amount_minor","currency"],"title":"Money"}
- NumberPoolCreateRequest: {"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"},"rotation_strategy":{"type":"string","const":"round_robin","title":"Rotation Strategy","default":"round_robin"}},"additionalProperties":false,"type":"object","required":["name"],"title":"NumberPoolCreateRequest"}
- NumberPoolMemberCreateRequest: {"properties":{"phone_number_id":{"type":"string","title":"Phone Number Id"}},"additionalProperties":false,"type":"object","required":["phone_number_id"],"title":"NumberPoolMemberCreateRequest"}
- NumberPoolMemberSummary: {"properties":{"id":{"type":"string","title":"Id"},"number_pool_id":{"type":"string","title":"Number Pool Id"},"phone_number_id":{"type":"string","title":"Phone Number Id"},"added_at":{"type":"string","format":"date-time","title":"Added At"}},"type":"object","required":["id","number_pool_id","phone_number_id","added_at"],"title":"NumberPoolMemberSummary"}
- NumberPoolSummary: {"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"rotation_strategy":{"type":"string","const":"round_robin","title":"Rotation Strategy"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","name","rotation_strategy","created_at"],"title":"NumberPoolSummary"}
- OnboardingChecklist: {"properties":{"application_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Application Id"},"application_status":{"anyOf":[{"type":"string","enum":["started","screening_hold","refused","paid","live","abandoned"]},{"type":"null"}],"title":"Application Status"},"mode":{"type":"string","enum":["sandbox","production"],"title":"Mode"},"next_key":{"anyOf":[{"type":"string","enum":["account","screening","plan","sandbox","sandbox_key","agreements","subscription","carrier","compliance_data","activation"]},{"type":"null"}],"title":"Next Key"},"complete":{"type":"boolean","title":"Complete"},"items":{"items":{"$ref":"#/components/schemas/OnboardingChecklistItem"},"type":"array","title":"Items"}},"type":"object","required":["application_id","application_status","mode","next_key","complete","items"],"title":"OnboardingChecklist","description":"The whole setup journey as one ordered, state-driven checklist.\n\nComposed on the server from the application and the production preflight; it authorizes\nnothing and every gating decision still lives in the endpoints the items describe."}
- OnboardingChecklistItem: {"properties":{"key":{"type":"string","enum":["account","screening","plan","sandbox","sandbox_key","agreements","subscription","carrier","compliance_data","activation"],"title":"Key"},"phase":{"type":"string","enum":["sandbox","production"],"title":"Phase"},"title":{"type":"string","title":"Title"},"summary":{"type":"string","title":"Summary"},"status":{"type":"string","enum":["done","active","upcoming","in_review","blocked"],"title":"Status"},"blocking":{"type":"boolean","title":"Blocking"}},"type":"object","required":["key","phase","title","summary","status","blocking"],"title":"OnboardingChecklistItem","description":"One step in the customer's setup, with its live status."}
- OnboardingState: {"properties":{"id":{"type":"string","title":"Id"},"email":{"type":"string","title":"Email"},"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"},"status":{"type":"string","enum":["started","screening_hold","refused","paid","live","abandoned"],"title":"Status"},"current_step":{"type":"string","enum":["account","business","compliance","estimate","plan","payment","documents","production"],"title":"Current Step"},"furthest_step":{"type":"string","enum":["account","business","compliance","estimate","plan","payment","documents","production"],"title":"Furthest Step"},"answers":{"additionalProperties":{"additionalProperties":true,"type":"object"},"type":"object","title":"Answers"},"screening":{"anyOf":[{"$ref":"#/components/schemas/ScreeningResult"},{"type":"null"}]},"term":{"anyOf":[{"type":"string","enum":["mtm","three_year"]},{"type":"null"}],"title":"Term"},"tenant_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tenant Id"},"paid_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Paid At"},"first_api_key_claimable":{"type":"boolean","title":"First Api Key Claimable"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","email","full_name","status","current_step","furthest_step","answers","first_api_key_claimable","created_at","updated_at"],"title":"OnboardingState","description":"The whole application as the wizard sees it."}
- OnboardingStepRequest: {"properties":{"step":{"type":"string","enum":["business","estimate","plan"],"title":"Step"},"business":{"anyOf":[{"$ref":"#/components/schemas/BusinessAnswers"},{"type":"null"}]},"estimate":{"anyOf":[{"$ref":"#/components/schemas/EstimateAnswers"},{"type":"null"}]},"plan":{"anyOf":[{"$ref":"#/components/schemas/PlanAnswers"},{"type":"null"}]},"attribution":{"anyOf":[{"$ref":"#/components/schemas/AttributionPayload"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["step"],"title":"OnboardingStepRequest","description":"Save one step. The compliance step is not accepted here - it has its own endpoint,\nbecause it produces a verdict rather than storing an answer."}
- PackSource: {"properties":{"kind":{"type":"string","enum":["pdf","url","text"],"title":"Kind"},"label":{"type":"string","maxLength":200,"minLength":1,"title":"Label"},"text":{"type":"string","minLength":1,"title":"Text"},"uri":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Uri"}},"additionalProperties":false,"type":"object","required":["kind","label","text"],"title":"PackSource","description":"One distilled section of a pack, plus where it came from.\n\n`text` is the artifact, not a summary of one: `knowledge_pack_versions` carries\n`sources` and no separate body column, so these sections *are* the compiled pack.\nPhrase them spoken-answer-ready - short declaratives an agent can say verbatim."}
- PasswordResetConfirmRequest: {"properties":{"token":{"type":"string","maxLength":512,"minLength":32,"title":"Token"},"password":{"type":"string","maxLength":1024,"minLength":8,"title":"Password"}},"type":"object","required":["token","password"],"title":"PasswordResetConfirmRequest"}
- PathwaysBucketOut: {"properties":{"agent_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Version Id"},"day":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Day"},"leads_captured":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Leads Captured"},"messages_taken":{"type":"integer","title":"Messages Taken"},"tickets_opened":{"type":"integer","title":"Tickets Opened"},"tickets_resolved":{"type":"integer","title":"Tickets Resolved"},"callbacks_scheduled":{"type":"integer","title":"Callbacks Scheduled"},"appointments_created":{"type":"integer","title":"Appointments Created"}},"type":"object","required":["messages_taken","tickets_opened","tickets_resolved","callbacks_scheduled","appointments_created"],"title":"PathwaysBucketOut","description":"One group of the business-pathway counters (ADR-0037).\n\n`leads_captured` is null - never zero - under an `agent_version` slice: the durable\nsource (`contacts.lead_captured_at`) carries no version attribution, and a zero\nwould put a false data point on a chart."}
- PathwaysReport: {"properties":{"window":{"$ref":"#/components/schemas/ReportWindow"},"group_by":{"items":{"type":"string","enum":["agent_version","day"]},"type":"array","title":"Group By"},"buckets":{"items":{"$ref":"#/components/schemas/PathwaysBucketOut"},"type":"array","title":"Buckets"}},"type":"object","required":["window"],"title":"PathwaysReport"}
- PhoneNumberCreateRequest: {"properties":{"phone_e164":{"type":"string","pattern":"^\\+[1-9]\\d{7,14}$","title":"Phone E164"},"telephony_account_id":{"type":"string","title":"Telephony Account Id"},"capabilities":{"items":{"type":"string","enum":["voice","sms"]},"type":"array","title":"Capabilities"},"provider_sid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider Sid"},"a2p_campaign_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"A2P Campaign Ref"},"daily_dial_cap":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"title":"Daily Dial Cap"}},"additionalProperties":false,"type":"object","required":["phone_e164","telephony_account_id"],"title":"PhoneNumberCreateRequest"}
- PhoneNumberSummary: {"properties":{"id":{"type":"string","title":"Id"},"phone_e164":{"type":"string","title":"Phone E164"},"telephony_account_id":{"type":"string","title":"Telephony Account Id"},"provider_sid":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider Sid"},"capabilities":{"items":{"type":"string","enum":["voice","sms"]},"type":"array","title":"Capabilities"},"a2p_campaign_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"A2P Campaign Ref"},"status":{"type":"string","enum":["provisioning","active","quarantined","resting","releasing","released"],"title":"Status"},"assigned_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Assigned At"},"daily_dial_cap":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Daily Dial Cap"},"dials_today":{"type":"integer","title":"Dials Today"},"reputation_state":{"type":"string","enum":["clean","at_risk","flagged","burned"],"title":"Reputation State"},"inbound_agent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Inbound Agent Id"},"inbound_route_mode":{"type":"string","enum":["agent","voicemail","policy"],"title":"Inbound Route Mode","default":"agent"},"inbound_voicemail_greeting":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Inbound Voicemail Greeting"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","phone_e164","telephony_account_id","status","dials_today","reputation_state","created_at"],"title":"PhoneNumberSummary"}
- PlanAnswers: {"properties":{"term":{"type":"string","enum":["mtm","three_year"],"title":"Term"},"workload_class":{"anyOf":[{"type":"string","enum":["engage","workforce","sales"]},{"type":"null"}],"title":"Workload Class"}},"additionalProperties":false,"type":"object","required":["term"],"title":"PlanAnswers","description":"Step 5. The term the customer is buying, plus the workload class.\n\nThe class is bounded below by the compliance screen's verdict and validated\nserver-side - a class is a ceiling, so buying up is allowed and buying down is not,\nand the browser disabling an option is a courtesy rather than the control."}
- PlanSummary: {"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"base_price_minor":{"type":"integer","title":"Base Price Minor"},"currency":{"type":"string","title":"Currency"},"included_quantities":{"additionalProperties":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},"type":"object","title":"Included Quantities"},"overage_rates":{"additionalProperties":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},"type":"object","title":"Overage Rates"},"concurrency_limit":{"type":"integer","title":"Concurrency Limit"},"receptionist_connections_included":{"type":"integer","title":"Receptionist Connections Included","default":0},"term":{"anyOf":[{"type":"string","enum":["mtm","three_year"]},{"type":"null"}],"title":"Term"},"feature_flags":{"additionalProperties":true,"type":"object","title":"Feature Flags"}},"type":"object","required":["id","name","base_price_minor","currency","concurrency_limit"],"title":"PlanSummary"}
- PortalTelemetryEventRequest: {"properties":{"event":{"type":"string","enum":["job_selected","setup_completed","setup_blocked","first_test_started","developer_handoff_opened","time_to_live_recorded","route_error","readiness_mismatch","setup_abandoned"],"title":"Event"},"workflow":{"type":"string","enum":["receptionist","workforce","csat","sales","not_applicable"],"title":"Workflow","default":"not_applicable"},"workforce_track":{"type":"string","enum":["availability","shift-reminders","candidate-follow-up","not_applicable"],"title":"Workforce Track","default":"not_applicable"},"surface":{"type":"string","enum":["home","setup","try","developer","activity","billing","manage","unknown"],"title":"Surface","default":"unknown"},"test_channel":{"type":"string","enum":["browser","text","phone","not_applicable"],"title":"Test Channel","default":"not_applicable"},"blocker":{"anyOf":[{"type":"string","enum":["account_agreements_required","agents_write_scope_required","calls_write_scope_required","runtime_not_configured","browser_runtime_not_configured","fleet_placement_not_configured","agent_chat_not_configured","assisted_setup_not_configured","voice_catalog_empty","verified_test_number_required","test_call_daily_cap_reached","telephony_account_required","telephony_account_unverified","telephony_worker_unavailable","calendar_integration_not_configured","calendar_connection_required","calendar_connection_reconnect_required"]},{"type":"string","enum":["agent_required","workflow_not_ready","readiness_unknown","route_not_found","query_invalid","request_failed","not_applicable"]}],"title":"Blocker","default":"not_applicable"},"duration_bucket":{"type":"string","enum":["under_1_minute","1_to_5_minutes","5_to_30_minutes","over_30_minutes","not_applicable"],"title":"Duration Bucket","default":"not_applicable"}},"additionalProperties":false,"type":"object","required":["event"],"title":"PortalTelemetryEventRequest","description":"One finite product-funnel event from the authenticated portal.\n\nThe event is intentionally not an audit trail. Authentication and normal\nper-key rate limiting still apply to the request, but neither the payload\nnor the monitoring event receives a caller, contact, helper, campaign,\nroute URL, exception string, or raw elapsed time."}
- PortalTelemetryReceipt: {"properties":{"recorded":{"type":"boolean","title":"Recorded"}},"additionalProperties":false,"type":"object","required":["recorded"],"title":"PortalTelemetryReceipt","description":"An intentionally content-free acknowledgement of a monitoring event."}
- PrepaidFundingIntent: {"properties":{"id":{"type":"string","title":"Id"},"amount_minor":{"type":"integer","title":"Amount Minor"},"currency":{"type":"string","title":"Currency"},"status":{"type":"string","title":"Status"},"checkout_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Checkout Url"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"pending":{"type":"boolean","title":"Pending","default":false}},"type":"object","required":["id","amount_minor","currency","status"],"title":"PrepaidFundingIntent"}
- PrepaidFundingRequest: {"properties":{"amount_minor":{"type":"integer","minimum":1,"title":"Amount Minor"},"currency":{"type":"string","const":"usd","title":"Currency","default":"usd"},"idempotency_key":{"type":"string","maxLength":160,"minLength":8,"title":"Idempotency Key"}},"type":"object","required":["amount_minor","idempotency_key"],"title":"PrepaidFundingRequest","description":"One manual top-up. The browser supplies no price id or currency conversion."}
- PrepaidReceipt: {"properties":{"id":{"type":"string","title":"Id"},"funding_intent_id":{"type":"string","title":"Funding Intent Id"},"kind":{"type":"string","title":"Kind"},"amount_minor":{"type":"integer","title":"Amount Minor"},"currency":{"type":"string","title":"Currency"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","funding_intent_id","kind","amount_minor","currency","created_at"],"title":"PrepaidReceipt"}
- PrepaidReconciliation: {"properties":{"id":{"type":"string","title":"Id"},"contact_attempt_id":{"type":"string","title":"Contact Attempt Id"},"status":{"type":"string","title":"Status"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"},"provider_call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider Call Id"},"actual_billed_seconds":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Actual Billed Seconds"},"actual_amount_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Actual Amount Minor"},"required_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Required At"},"resolved_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Resolved At"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","contact_attempt_id","status","reason","provider_call_id","actual_billed_seconds","actual_amount_minor","required_at","resolved_at","created_at"],"title":"PrepaidReconciliation","description":"A customer-visible hold exception; the opaque callback correlation stays private."}
- PrepaidWallet: {"properties":{"currency":{"type":"string","title":"Currency"},"prepaid_balance_minor":{"type":"integer","title":"Prepaid Balance Minor"},"prepaid_reserved_minor":{"type":"integer","title":"Prepaid Reserved Minor"},"prepaid_available_minor":{"type":"integer","title":"Prepaid Available Minor"},"spend_cap_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Spend Cap Minor"},"spend_cap_period":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Spend Cap Period"},"low_balance_threshold_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Low Balance Threshold Minor"},"funding_min_minor":{"type":"integer","minimum":1,"title":"Funding Min Minor"},"funding_max_minor":{"type":"integer","minimum":1,"title":"Funding Max Minor"},"applies_to":{"type":"string","const":"managed_verified_number_test_calls_only","title":"Applies To","default":"managed_verified_number_test_calls_only"},"auto_recharge_supported":{"type":"boolean","title":"Auto Recharge Supported","default":false},"funding_intents":{"items":{"$ref":"#/components/schemas/PrepaidFundingIntent"},"type":"array","title":"Funding Intents"},"receipts":{"items":{"$ref":"#/components/schemas/PrepaidReceipt"},"type":"array","title":"Receipts"},"reconciliations":{"items":{"$ref":"#/components/schemas/PrepaidReconciliation"},"type":"array","title":"Reconciliations"}},"type":"object","required":["currency","prepaid_balance_minor","prepaid_reserved_minor","prepaid_available_minor","spend_cap_minor","spend_cap_period","low_balance_threshold_minor","funding_min_minor","funding_max_minor","funding_intents","receipts","reconciliations"],"title":"PrepaidWallet"}
- ProductionActivationResponse: {"properties":{"application_id":{"type":"string","title":"Application Id"},"application_status":{"type":"string","enum":["started","screening_hold","refused","paid","live","abandoned"],"title":"Application Status"},"activated":{"type":"boolean","title":"Activated"},"review_requested":{"type":"boolean","title":"Review Requested"},"ready":{"type":"boolean","title":"Ready"},"checks":{"items":{"$ref":"#/components/schemas/ProductionReadinessCheck"},"type":"array","title":"Checks"}},"type":"object","required":["application_id","application_status","activated","review_requested","ready","checks"],"title":"ProductionActivationResponse","description":"The outcome of a self-serve production-activation attempt.\n\nExactly one of `activated` / `review_requested` is true on a successful call, and both\nare false only when the workspace was already live (an idempotent no-op). A workspace\nthat is not yet ready does not reach this model - the endpoint refuses with the failed\nchecks named, the same as the review-request preflight.\n\n`activated` true means this call moved the workspace to production itself: the\napplication screened cleanly (eligible, no waiver, no gated states) and every readiness\ngate was met, so no human review was required. `review_requested` true means the\nworkspace was ready but the screen was not clean - a waiver, a gated state, or a hold -\nso it was routed to staff review instead of activated, exactly as\n`POST /onboarding/production-readiness/request` would."}
- ProductionReadinessCheck: {"properties":{"key":{"type":"string","enum":["tenant_active","application_paid","current_agreements","subscription_workload_class","verified_byo_carrier"],"title":"Key"},"ready":{"type":"boolean","title":"Ready"},"detail":{"type":"string","title":"Detail"}},"type":"object","required":["key","ready","detail"],"title":"ProductionReadinessCheck"}
- ProductionReadinessStatus: {"properties":{"application_id":{"type":"string","title":"Application Id"},"application_status":{"type":"string","enum":["started","screening_hold","refused","paid","live","abandoned"],"title":"Application Status"},"ready":{"type":"boolean","title":"Ready"},"request_status":{"type":"string","enum":["not_requested","requested","live"],"title":"Request Status"},"requested_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Requested At"},"activated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Activated At"},"staff_approval_required":{"type":"boolean","const":true,"title":"Staff Approval Required","default":true},"checks":{"items":{"$ref":"#/components/schemas/ProductionReadinessCheck"},"type":"array","title":"Checks"}},"type":"object","required":["application_id","application_status","ready","request_status","checks"],"title":"ProductionReadinessStatus","description":"Owner-visible production preflight, never an authorization to go live."}
- ProductionReviewRequestResponse: {"properties":{"application_id":{"type":"string","title":"Application Id"},"application_status":{"type":"string","enum":["started","screening_hold","refused","paid","live","abandoned"],"title":"Application Status"},"ready":{"type":"boolean","title":"Ready"},"request_status":{"type":"string","enum":["not_requested","requested","live"],"title":"Request Status"},"requested_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Requested At"},"activated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Activated At"},"staff_approval_required":{"type":"boolean","const":true,"title":"Staff Approval Required","default":true},"checks":{"items":{"$ref":"#/components/schemas/ProductionReadinessCheck"},"type":"array","title":"Checks"},"request_created":{"type":"boolean","title":"Request Created"}},"type":"object","required":["application_id","application_status","ready","request_status","checks","request_created"],"title":"ProductionReviewRequestResponse","description":"The durable outcome of a retry-safe request for staff review."}
- QualificationCoverage: {"properties":{"status":{"type":"string","enum":["available","invalid_record","invalid_configuration","workflow_mismatch"],"title":"Status"},"questions":{"items":{"$ref":"#/components/schemas/QualificationQuestionCoverage"},"type":"array","maxItems":50,"title":"Questions"}},"type":"object","required":["status"],"title":"QualificationCoverage","description":"Complete configured question inventory for one current receipt, not a page total."}
- QualificationFitCount: {"properties":{"workflow":{"type":"string","enum":["sales","recruiting"],"title":"Workflow"},"status":{"type":"string","enum":["good_fit","not_a_fit","needs_follow_up","unreviewed","superseded"],"title":"Status"},"count":{"type":"integer","minimum":0,"title":"Count"}},"type":"object","required":["workflow","status","count"],"title":"QualificationFitCount"}
- QualificationFitPreview: {"properties":{"status":{"anyOf":[{"type":"string","enum":["good_fit","not_a_fit","needs_follow_up"]},{"type":"string","const":"unreviewed"}],"title":"Status"},"reason":{"anyOf":[{"type":"string","maxLength":240},{"type":"null"}],"title":"Reason"},"reason_truncated":{"type":"boolean","title":"Reason Truncated","default":false}},"type":"object","required":["status"],"title":"QualificationFitPreview","description":"A bounded team-decision preview for a current qualification receipt."}
- QualificationFitSummary: {"properties":{"window":{"$ref":"#/components/schemas/DashboardWindow"},"as_of":{"type":"string","format":"date-time","title":"As Of"},"counts":{"items":{"$ref":"#/components/schemas/QualificationFitCount"},"type":"array","title":"Counts"}},"type":"object","required":["window","as_of","counts"],"title":"QualificationFitSummary","description":"Current team decisions for latest answer receipts saved in the response window."}
- QualificationQuestionCoverage: {"properties":{"question_id":{"type":"string","title":"Question Id"},"prompt":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prompt"},"answer_status":{"type":"string","enum":["answered","unknown","refused","not_recorded"],"title":"Answer Status"},"answer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Answer"}},"type":"object","required":["question_id","answer_status"],"title":"QualificationQuestionCoverage"}
- QualificationReview: {"properties":{"id":{"type":"string","title":"Id"},"action_id":{"type":"string","title":"Action Id"},"revision":{"type":"integer","minimum":1,"title":"Revision"},"decision":{"type":"string","enum":["good_fit","not_a_fit","needs_follow_up"],"title":"Decision"},"reason":{"type":"string","title":"Reason"},"reviewer_ref":{"type":"string","title":"Reviewer Ref"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","action_id","revision","decision","reason","reviewer_ref","created_at"],"title":"QualificationReview"}
- QualificationReviewDetails: {"properties":{"fit_review":{"$ref":"#/components/schemas/QualificationReviewState"},"qualification_coverage":{"anyOf":[{"$ref":"#/components/schemas/QualificationCoverage"},{"type":"null"}]}},"type":"object","required":["fit_review"],"title":"QualificationReviewDetails","description":"Pinned answers and the current review state for a call's source receipt."}
- QualificationReviewRequest: {"properties":{"decision":{"type":"string","enum":["good_fit","not_a_fit","needs_follow_up"],"title":"Decision"},"reason":{"type":"string","maxLength":2000,"minLength":1,"title":"Reason"},"expected_revision":{"type":"integer","minimum":0,"title":"Expected Revision"}},"additionalProperties":false,"type":"object","required":["decision","reason","expected_revision"],"title":"QualificationReviewRequest"}
- QualificationReviewState: {"properties":{"status":{"type":"string","enum":["unreviewed","reviewed","superseded","unavailable"],"title":"Status"},"current_action_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Action Id"},"latest_review":{"anyOf":[{"$ref":"#/components/schemas/QualificationReview"},{"type":"null"}]}},"type":"object","required":["status"],"title":"QualificationReviewState","description":"A review applies only to its source receipt, never to later corrections."}
- RealtimeToken: {"properties":{"token":{"type":"string","title":"Token"},"token_id":{"type":"string","title":"Token Id"},"call_id":{"type":"string","title":"Call Id"},"actor_api_key_id":{"type":"string","title":"Actor Api Key Id"},"channel":{"type":"string","const":"calls.audio","title":"Channel"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"listener_url":{"type":"string","title":"Listener Url"},"single_use":{"type":"boolean","const":true,"title":"Single Use","default":true}},"type":"object","required":["token","token_id","call_id","actor_api_key_id","channel","expires_at","listener_url"],"title":"RealtimeToken","description":"A short-lived, single-use realtime gateway credential.\n\nThis is intentionally not a general API key and must never be persisted. The gateway\nverifies its call, tenant, channel and JTI, then creates the supervision-session audit\nrow only after it has a carrier/media source to attach."}
- ReceptionistActionTargetRequest: {"properties":{"endpoint_id":{"type":"string","maxLength":128,"minLength":1,"title":"Endpoint Id"}},"additionalProperties":false,"type":"object","required":["endpoint_id"],"title":"ReceptionistActionTargetRequest","description":"Bind one predefined receptionist outcome to the tenant's signed endpoint.\n\nThe endpoint URL and signing secret are configured through ``/webhook-endpoint``.\nThis resource deliberately accepts no tool name, JSON Schema, command, or arbitrary\nevent type: its path parameter is the entire approved action catalog."}
- ReceptionistActionTargetSummary: {"properties":{"id":{"type":"string","title":"Id"},"outcome_type":{"type":"string","title":"Outcome Type"},"event_type":{"type":"string","title":"Event Type"},"endpoint_id":{"type":"string","title":"Endpoint Id"},"endpoint_url":{"type":"string","title":"Endpoint Url"},"endpoint_status":{"type":"string","title":"Endpoint Status"},"delivery_count":{"type":"integer","title":"Delivery Count"},"latest_delivery_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Latest Delivery Status"},"latest_delivery_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Latest Delivery At"},"last_tested_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Tested At"},"last_test_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Test Status"},"last_test_response_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Test Response Code"},"signing_secret_source":{"type":"string","title":"Signing Secret Source","default":"webhook_endpoint_create_or_rotate"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","outcome_type","event_type","endpoint_id","endpoint_url","endpoint_status","delivery_count","latest_delivery_status","latest_delivery_at","last_tested_at","last_test_status","last_test_response_code","created_at","updated_at"],"title":"ReceptionistActionTargetSummary","description":"One native action outcome plus the target's present delivery health."}
- ReceptionistConnectionQuote: {"properties":{"receptionist_connections":{"type":"integer","title":"Receptionist Connections"},"total_inbound_connections":{"type":"integer","title":"Total Inbound Connections"},"amount_due_minor":{"type":"integer","title":"Amount Due Minor"},"currency":{"type":"string","const":"usd","title":"Currency","default":"usd"},"recurring_connections_minor":{"type":"integer","title":"Recurring Connections Minor"},"renewal_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Renewal At"},"quote_fingerprint":{"type":"string","title":"Quote Fingerprint"},"purchase_available":{"type":"boolean","title":"Purchase Available"},"prepay_exempt":{"type":"boolean","title":"Prepay Exempt"}},"type":"object","required":["receptionist_connections","total_inbound_connections","amount_due_minor","recurring_connections_minor","renewal_at","quote_fingerprint","purchase_available","prepay_exempt"],"title":"ReceptionistConnectionQuote"}
- ReceptionistConnectionsRequest: {"properties":{"receptionist_connections":{"type":"integer","minimum":0,"title":"Receptionist Connections"}},"type":"object","required":["receptionist_connections"],"title":"ReceptionistConnectionsRequest","description":"The absolute purchased quantity - never a delta, so retries converge on N.\n\nCounted the way the checkout body counts it: connections bought ON TOP of whatever the\nplan already includes. A standalone receptionist tenant at `0` still answers one call\nat a time, because its plan's $250 base fee carries the first connection."}
- ReceptionistDryRunActionExpectation: {"properties":{"mode":{"type":"string","const":"dry_run","title":"Mode","default":"dry_run"},"tool":{"type":"string","enum":["take_message","open_ticket","update_lead","book_meeting","schedule_callback","record_optout","end_call"],"title":"Tool"},"expected_effect":{"type":"string","enum":["message_recorded","ticket_opened","lead_updated","booking_created","callback_scheduled","opt_out_recorded","call_ended"],"title":"Expected Effect"}},"additionalProperties":false,"type":"object","required":["tool","expected_effect"],"title":"ReceptionistDryRunActionExpectation","description":"One exact native action the runner must observe through its dry-run facade."}
- ReceptionistFaq: {"properties":{"question":{"type":"string","maxLength":500,"minLength":1,"title":"Question"},"answer":{"type":"string","maxLength":2000,"minLength":1,"title":"Answer"},"source_refs":{"items":{"type":"string"},"type":"array","maxItems":20,"title":"Source Refs"}},"additionalProperties":false,"type":"object","required":["question","answer"],"title":"ReceptionistFaq","description":"An approved caller question and its grounded spoken answer."}
- ReceptionistIntelligenceDraft: {"properties":{"business_name":{"type":"string","maxLength":200,"minLength":1,"title":"Business Name"},"business_summary":{"type":"string","maxLength":2000,"minLength":1,"title":"Business Summary"},"business_summary_source_refs":{"items":{"type":"string"},"type":"array","maxItems":20,"title":"Business Summary Source Refs"},"locations":{"items":{"$ref":"#/components/schemas/ReceptionistLocation"},"type":"array","maxItems":50,"title":"Locations"},"services":{"items":{"$ref":"#/components/schemas/ReceptionistService"},"type":"array","maxItems":100,"title":"Services"},"pricing_policy":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Pricing Policy"},"pricing_policy_source_refs":{"items":{"type":"string"},"type":"array","maxItems":20,"title":"Pricing Policy Source Refs"},"faqs":{"items":{"$ref":"#/components/schemas/ReceptionistFaq"},"type":"array","maxItems":100,"title":"Faqs"},"customer_policies":{"items":{"$ref":"#/components/schemas/ReceptionistPolicyFact"},"type":"array","maxItems":100,"title":"Customer Policies"},"supported_intents":{"items":{"type":"string"},"type":"array","maxItems":50,"title":"Supported Intents"},"tone":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Tone"},"escalation_rules":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Escalation Rules"},"escalation_rules_source_refs":{"items":{"type":"string"},"type":"array","maxItems":20,"title":"Escalation Rules Source Refs"},"prohibited_claims":{"items":{"type":"string"},"type":"array","maxItems":50,"title":"Prohibited Claims"},"approved_actions":{"items":{"type":"string","enum":["check_calendar","book_meeting","send_sms","send_verification_code","verify_identity","record_sms_consent","take_message","open_ticket","update_lead","record_survey_answer","confirm_shift","record_shift_response","record_qualification","schedule_callback","transfer_to_human","record_optout","end_call","custom_webhook"]},"type":"array","title":"Approved Actions"}},"additionalProperties":false,"type":"object","required":["business_name","business_summary"],"title":"ReceptionistIntelligenceDraft","description":"The customer-editable intelligence document for one receptionist."}
- ReceptionistIntelligencePreview: {"properties":{"compiled_knowledge":{"type":"string","title":"Compiled Knowledge"},"compiled_character_count":{"type":"integer","title":"Compiled Character Count"},"compiled_token_count":{"type":"integer","title":"Compiled Token Count"},"location_token_counts":{"patternProperties":{"^[a-z][a-z0-9_-]{0,63}$":{"type":"integer"}},"type":"object","title":"Location Token Counts"},"source_ids":{"items":{"type":"string"},"type":"array","title":"Source Ids"}},"additionalProperties":false,"type":"object","required":["compiled_knowledge","compiled_character_count","compiled_token_count"],"title":"ReceptionistIntelligencePreview","description":"A customer-visible render of exactly the compact knowledge that can be published."}
- ReceptionistIntelligenceSummary: {"properties":{"id":{"type":"string","title":"Id"},"agent_definition_id":{"type":"string","title":"Agent Definition Id"},"current_version":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Current Version"},"draft":{"$ref":"#/components/schemas/ReceptionistIntelligenceDraft"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"additionalProperties":false,"type":"object","required":["id","agent_definition_id","draft","updated_at","created_at"],"title":"ReceptionistIntelligenceSummary"}
- ReceptionistIntelligenceUpsertRequest: {"properties":{"draft":{"$ref":"#/components/schemas/ReceptionistIntelligenceDraft"}},"additionalProperties":false,"type":"object","required":["draft"],"title":"ReceptionistIntelligenceUpsertRequest","description":"Replace the draft atomically; an immutable version is created separately."}
- ReceptionistIntelligenceVersionResource: {"properties":{"id":{"type":"string","title":"Id"},"receptionist_intelligence_id":{"type":"string","title":"Receptionist Intelligence Id"},"version":{"type":"integer","title":"Version"},"content_hash":{"type":"string","title":"Content Hash"},"source_ids":{"items":{"type":"string"},"type":"array","title":"Source Ids"},"compiled_knowledge":{"type":"string","title":"Compiled Knowledge"},"compiled_character_count":{"type":"integer","title":"Compiled Character Count"},"compiled_token_count":{"type":"integer","title":"Compiled Token Count"},"location_token_counts":{"patternProperties":{"^[a-z][a-z0-9_-]{0,63}$":{"type":"integer"}},"type":"object","title":"Location Token Counts"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"draft":{"$ref":"#/components/schemas/ReceptionistIntelligenceDraft"},"sources":{"items":{"$ref":"#/components/schemas/ReceptionistSourceSummary"},"type":"array","title":"Sources"}},"additionalProperties":false,"type":"object","required":["id","receptionist_intelligence_id","version","content_hash","compiled_knowledge","compiled_character_count","compiled_token_count","created_at","draft"],"title":"ReceptionistIntelligenceVersionResource"}
- ReceptionistIntelligenceVersionSummary: {"properties":{"id":{"type":"string","title":"Id"},"receptionist_intelligence_id":{"type":"string","title":"Receptionist Intelligence Id"},"version":{"type":"integer","title":"Version"},"content_hash":{"type":"string","title":"Content Hash"},"source_ids":{"items":{"type":"string"},"type":"array","title":"Source Ids"},"compiled_knowledge":{"type":"string","title":"Compiled Knowledge"},"compiled_character_count":{"type":"integer","title":"Compiled Character Count"},"compiled_token_count":{"type":"integer","title":"Compiled Token Count"},"location_token_counts":{"patternProperties":{"^[a-z][a-z0-9_-]{0,63}$":{"type":"integer"}},"type":"object","title":"Location Token Counts"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"additionalProperties":false,"type":"object","required":["id","receptionist_intelligence_id","version","content_hash","compiled_knowledge","compiled_character_count","compiled_token_count","created_at"],"title":"ReceptionistIntelligenceVersionSummary"}
- ReceptionistLiveModelPreflightEvidence: {"properties":{"capability_manifest_hash":{"type":"string","pattern":"^[0-9a-f]{64}$","title":"Capability Manifest Hash"},"rendered_token_budget":{"$ref":"#/components/schemas/ReceptionistRenderedTokenBudget"},"nonempty_stream":{"type":"boolean","title":"Nonempty Stream"},"automatic_tool_choice":{"type":"boolean","title":"Automatic Tool Choice"},"automatic_tool_call":{"type":"boolean","title":"Automatic Tool Call"},"tool_result_continuation":{"type":"boolean","title":"Tool Result Continuation"},"parser_name":{"type":"string","maxLength":100,"minLength":1,"title":"Parser Name"},"parser_validated":{"type":"boolean","title":"Parser Validated"},"authenticated_runtime_access":{"type":"boolean","title":"Authenticated Runtime Access"},"loopback_only_access":{"type":"boolean","title":"Loopback Only Access"}},"additionalProperties":false,"type":"object","required":["capability_manifest_hash","rendered_token_budget","nonempty_stream","automatic_tool_choice","automatic_tool_call","tool_result_continuation","parser_name","parser_validated","authenticated_runtime_access","loopback_only_access"],"title":"ReceptionistLiveModelPreflightEvidence","description":"The non-scripted serving proof required before a receptionist is production-ready.\n\nThis is persisted under ``eval_runs.results.live_model_preflight`` by an operational\nrouting-chain worker. It is intentionally structured so a generic successful model\nreplay cannot be misread as a proof that the exact production request fits and tools\ncan continue after a result."}
- ReceptionistLocation: {"properties":{"ref":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$","title":"Ref"},"name":{"type":"string","maxLength":160,"minLength":1,"title":"Name"},"address":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Address"},"phone":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Phone"},"service_area":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Service Area"},"facts":{"items":{"type":"string"},"type":"array","maxItems":30,"title":"Facts"},"source_refs":{"items":{"type":"string"},"type":"array","maxItems":20,"title":"Source Refs"}},"additionalProperties":false,"type":"object","required":["ref","name"],"title":"ReceptionistLocation","description":"A stable business-location reference used by a phone line's AI policy step."}
- ReceptionistPolicyFact: {"properties":{"name":{"type":"string","maxLength":160,"minLength":1,"title":"Name"},"value":{"type":"string","maxLength":2000,"minLength":1,"title":"Value"},"source_refs":{"items":{"type":"string"},"type":"array","maxItems":20,"title":"Source Refs"}},"additionalProperties":false,"type":"object","required":["name","value"],"title":"ReceptionistPolicyFact","description":"A named business policy, distinct from platform safety/compliance controls."}
- ReceptionistPurchaseInput: {"properties":{"receptionist_connections":{"type":"integer","minimum":0,"title":"Receptionist Connections"},"request_key":{"type":"string","maxLength":200,"minLength":1,"pattern":"\\S","title":"Request Key"},"amount_due_minor":{"type":"integer","minimum":0,"title":"Amount Due Minor"},"currency":{"type":"string","const":"usd","title":"Currency"},"quote_fingerprint":{"type":"string","pattern":"^[a-f0-9]{64}$","title":"Quote Fingerprint"}},"additionalProperties":false,"type":"object","required":["receptionist_connections","request_key","amount_due_minor","currency","quote_fingerprint"],"title":"ReceptionistPurchaseInput"}
- ReceptionistPurchaseResponse: {"properties":{"id":{"type":"string","title":"Id"},"status":{"type":"string","title":"Status"},"receptionist_connections":{"type":"integer","title":"Receptionist Connections"},"total_inbound_connections":{"type":"integer","title":"Total Inbound Connections"},"amount_due_minor":{"type":"integer","title":"Amount Due Minor"},"currency":{"type":"string","const":"usd","title":"Currency","default":"usd"},"recurring_connections_minor":{"type":"integer","title":"Recurring Connections Minor"},"renewal_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Renewal At"},"failure_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failure Code"},"payment_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Payment Url"}},"type":"object","required":["id","status","receptionist_connections","total_inbound_connections","amount_due_minor","recurring_connections_minor","renewal_at","failure_code"],"title":"ReceptionistPurchaseResponse"}
- ReceptionistQuoteRequest: {"properties":{"receptionist_connections":{"type":"integer","minimum":0,"title":"Receptionist Connections"}},"additionalProperties":false,"type":"object","required":["receptionist_connections"],"title":"ReceptionistQuoteRequest"}
- ReceptionistRegressionCaseArchiveRequest: {"properties":{"expected_version":{"type":"integer","minimum":1,"title":"Expected Version"}},"additionalProperties":false,"type":"object","required":["expected_version"],"title":"ReceptionistRegressionCaseArchiveRequest","description":"Explicit confirmation that a current case should stop participating in releases."}
- ReceptionistRegressionCaseCreateRequest: {"properties":{"name":{"type":"string","maxLength":160,"minLength":1,"title":"Name"},"caller_utterance":{"type":"string","maxLength":2000,"minLength":1,"title":"Caller Utterance"},"expected_facts":{"items":{"type":"string","maxLength":1000,"minLength":1},"type":"array","maxItems":20,"title":"Expected Facts"},"prohibited_claims":{"items":{"type":"string","maxLength":1000,"minLength":1},"type":"array","maxItems":20,"title":"Prohibited Claims"},"routing":{"anyOf":[{"$ref":"#/components/schemas/ReceptionistRoutingExpectation"},{"type":"null"}]},"intended_action":{"anyOf":[{"type":"string","enum":["take_message","open_ticket","update_lead","book_meeting","schedule_callback","record_optout","end_call"]},{"type":"null"}],"title":"Intended Action"},"dry_run_action":{"anyOf":[{"$ref":"#/components/schemas/ReceptionistDryRunActionExpectation"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["name","caller_utterance"],"title":"ReceptionistRegressionCaseCreateRequest","description":"Create a stable case identity and immutable revision 1."}
- ReceptionistRegressionCaseSummary: {"properties":{"name":{"type":"string","maxLength":160,"minLength":1,"title":"Name"},"caller_utterance":{"type":"string","maxLength":2000,"minLength":1,"title":"Caller Utterance"},"expected_facts":{"items":{"type":"string","maxLength":1000,"minLength":1},"type":"array","maxItems":20,"title":"Expected Facts"},"prohibited_claims":{"items":{"type":"string","maxLength":1000,"minLength":1},"type":"array","maxItems":20,"title":"Prohibited Claims"},"routing":{"anyOf":[{"$ref":"#/components/schemas/ReceptionistRoutingExpectation"},{"type":"null"}]},"intended_action":{"anyOf":[{"type":"string","enum":["take_message","open_ticket","update_lead","book_meeting","schedule_callback","record_optout","end_call"]},{"type":"null"}],"title":"Intended Action"},"dry_run_action":{"anyOf":[{"$ref":"#/components/schemas/ReceptionistDryRunActionExpectation"},{"type":"null"}]},"id":{"type":"string","title":"Id"},"receptionist_regression_case_id":{"type":"string","title":"Receptionist Regression Case Id"},"version":{"type":"integer","title":"Version"},"content_hash":{"type":"string","title":"Content Hash"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"agent_definition_id":{"type":"string","title":"Agent Definition Id"},"status":{"type":"string","enum":["active","archived"],"title":"Status"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"additionalProperties":false,"type":"object","required":["name","caller_utterance","id","receptionist_regression_case_id","version","content_hash","created_at","agent_definition_id","status","updated_at"],"title":"ReceptionistRegressionCaseSummary","description":"The current immutable revision selected by a stable customer case resource."}
- ReceptionistRegressionCaseUpdateRequest: {"properties":{"name":{"type":"string","maxLength":160,"minLength":1,"title":"Name"},"caller_utterance":{"type":"string","maxLength":2000,"minLength":1,"title":"Caller Utterance"},"expected_facts":{"items":{"type":"string","maxLength":1000,"minLength":1},"type":"array","maxItems":20,"title":"Expected Facts"},"prohibited_claims":{"items":{"type":"string","maxLength":1000,"minLength":1},"type":"array","maxItems":20,"title":"Prohibited Claims"},"routing":{"anyOf":[{"$ref":"#/components/schemas/ReceptionistRoutingExpectation"},{"type":"null"}]},"intended_action":{"anyOf":[{"type":"string","enum":["take_message","open_ticket","update_lead","book_meeting","schedule_callback","record_optout","end_call"]},{"type":"null"}],"title":"Intended Action"},"dry_run_action":{"anyOf":[{"$ref":"#/components/schemas/ReceptionistDryRunActionExpectation"},{"type":"null"}]},"expected_version":{"type":"integer","minimum":1,"title":"Expected Version"}},"additionalProperties":false,"type":"object","required":["name","caller_utterance","expected_version"],"title":"ReceptionistRegressionCaseUpdateRequest","description":"Append an immutable case revision after optimistic-concurrency confirmation."}
- ReceptionistRegressionCaseVersionSummary: {"properties":{"name":{"type":"string","maxLength":160,"minLength":1,"title":"Name"},"caller_utterance":{"type":"string","maxLength":2000,"minLength":1,"title":"Caller Utterance"},"expected_facts":{"items":{"type":"string","maxLength":1000,"minLength":1},"type":"array","maxItems":20,"title":"Expected Facts"},"prohibited_claims":{"items":{"type":"string","maxLength":1000,"minLength":1},"type":"array","maxItems":20,"title":"Prohibited Claims"},"routing":{"anyOf":[{"$ref":"#/components/schemas/ReceptionistRoutingExpectation"},{"type":"null"}]},"intended_action":{"anyOf":[{"type":"string","enum":["take_message","open_ticket","update_lead","book_meeting","schedule_callback","record_optout","end_call"]},{"type":"null"}],"title":"Intended Action"},"dry_run_action":{"anyOf":[{"$ref":"#/components/schemas/ReceptionistDryRunActionExpectation"},{"type":"null"}]},"id":{"type":"string","title":"Id"},"receptionist_regression_case_id":{"type":"string","title":"Receptionist Regression Case Id"},"version":{"type":"integer","title":"Version"},"content_hash":{"type":"string","title":"Content Hash"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"additionalProperties":false,"type":"object","required":["name","caller_utterance","id","receptionist_regression_case_id","version","content_hash","created_at"],"title":"ReceptionistRegressionCaseVersionSummary","description":"One immutable case version, including the exact user-authored assertions."}
- ReceptionistReleaseCheck: {"properties":{"key":{"type":"string","title":"Key"},"status":{"type":"string","enum":["ready","pending","blocked"],"title":"Status"},"detail":{"type":"string","title":"Detail"}},"additionalProperties":false,"type":"object","required":["key","status","detail"],"title":"ReceptionistReleaseCheck","description":"One server-derived readiness fact for a receptionist release."}
- ReceptionistReleaseStatus: {"properties":{"agent_definition_id":{"type":"string","title":"Agent Definition Id"},"active_version":{"anyOf":[{"$ref":"#/components/schemas/AgentVersionSummary"},{"type":"null"}]},"rollback_candidate":{"anyOf":[{"$ref":"#/components/schemas/AgentVersionSummary"},{"type":"null"}]},"current_draft_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Draft Hash"},"current_gate":{"anyOf":[{"$ref":"#/components/schemas/ReleaseGateEvaluation"},{"type":"null"}]},"current_live_model_preflight":{"anyOf":[{"$ref":"#/components/schemas/ReleaseGateEvaluation"},{"type":"null"}]},"live_model_preflight_evidence":{"anyOf":[{"$ref":"#/components/schemas/ReceptionistLiveModelPreflightEvidence"},{"type":"null"}]},"publish_ready":{"type":"boolean","title":"Publish Ready"},"rollback_ready":{"type":"boolean","title":"Rollback Ready"},"production_ready":{"type":"boolean","title":"Production Ready","default":false},"checks":{"items":{"$ref":"#/components/schemas/ReceptionistReleaseCheck"},"type":"array","title":"Checks"}},"additionalProperties":false,"type":"object","required":["agent_definition_id","publish_ready","rollback_ready"],"title":"ReceptionistReleaseStatus","description":"Current publish/rollback state without exposing the sealed prompt evidence."}
- ReceptionistRenderedTokenBudget: {"properties":{"context_capacity_tokens":{"type":"integer","minimum":1,"title":"Context Capacity Tokens"},"rendered_prompt_tokens":{"type":"integer","minimum":0,"title":"Rendered Prompt Tokens"},"completion_reserve_tokens":{"type":"integer","minimum":1,"title":"Completion Reserve Tokens"},"requested_tokens":{"type":"integer","minimum":1,"title":"Requested Tokens"},"request_fits":{"type":"boolean","title":"Request Fits"}},"additionalProperties":false,"type":"object","required":["context_capacity_tokens","rendered_prompt_tokens","completion_reserve_tokens","requested_tokens","request_fits"],"title":"ReceptionistRenderedTokenBudget","description":"Exact serving-envelope evidence captured by a live-model preflight."}
- ReceptionistRollbackRequest: {"properties":{"target_version":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"title":"Target Version"}},"additionalProperties":false,"type":"object","title":"ReceptionistRollbackRequest","description":"Restore a previously healthy receptionist snapshot; omitted selects the predecessor."}
- ReceptionistRollbackResult: {"properties":{"agent_definition_id":{"type":"string","title":"Agent Definition Id"},"restored_from_version":{"$ref":"#/components/schemas/AgentVersionSummary"},"active_version":{"$ref":"#/components/schemas/AgentVersionSummary"},"checked_policy_number_ids":{"items":{"type":"string"},"type":"array","title":"Checked Policy Number Ids"}},"additionalProperties":false,"type":"object","required":["agent_definition_id","restored_from_version","active_version"],"title":"ReceptionistRollbackResult","description":"A new active immutable snapshot cloned from a verified prior release."}
- ReceptionistRoutingExpectation: {"properties":{"branch":{"type":"string","enum":["open","closed"],"title":"Branch"},"step_type":{"type":"string","enum":["ai","ring_humans","voicemail","say","forward","menu"],"title":"Step Type"},"location_ref":{"anyOf":[{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,63}$"},{"type":"null"}],"title":"Location Ref"}},"additionalProperties":false,"type":"object","required":["branch","step_type"],"title":"ReceptionistRoutingExpectation","description":"The answering-policy branch and terminal step a case expects.\n\nThis does not carry a phone number, transfer destination, or policy document. The\nrelease runner resolves these small assertions against the pinned line policy, keeping\na customer test case from becoming an alternate routing control plane."}
- ReceptionistService: {"properties":{"name":{"type":"string","maxLength":160,"minLength":1,"title":"Name"},"description":{"type":"string","maxLength":1000,"minLength":1,"title":"Description"},"availability_note":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Availability Note"},"source_refs":{"items":{"type":"string"},"type":"array","maxItems":20,"title":"Source Refs"}},"additionalProperties":false,"type":"object","required":["name","description"],"title":"ReceptionistService","description":"One service the receptionist may describe without inventing availability."}
- ReceptionistSourceCreateRequest: {"properties":{"kind":{"type":"string","enum":["document","website"],"title":"Kind"},"label":{"type":"string","maxLength":255,"minLength":1,"title":"Label"},"content":{"type":"string","maxLength":100000,"minLength":1,"title":"Content"},"source_url":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Source Url"},"provenance":{"anyOf":[{"type":"string","enum":["customer_upload","customer_website_snapshot"]},{"type":"null"}],"title":"Provenance"},"extraction_report":{"anyOf":[{"$ref":"#/components/schemas/ReceptionistSourceExtractionReport"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["kind","label","content"],"title":"ReceptionistSourceCreateRequest","description":"A pending immutable snapshot of a customer-provided document or website page.\n\nWebsite contents are snapshots supplied/imported at a specific point in time. The\nlive call path never fetches URLs and never follows arbitrary web content."}
- ReceptionistSourceExtractionReport: {"properties":{"title":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Title"},"extracted_characters":{"type":"integer","maximum":100000,"minimum":0,"title":"Extracted Characters"},"warnings":{"items":{"type":"string"},"type":"array","maxItems":20,"title":"Warnings"}},"additionalProperties":false,"type":"object","required":["extracted_characters"],"title":"ReceptionistSourceExtractionReport","description":"Bounded import evidence shown before a customer promotes facts into the draft."}
- ReceptionistSourceRefreshRequest: {"properties":{"content":{"type":"string","maxLength":100000,"minLength":1,"title":"Content"},"extraction_report":{"anyOf":[{"$ref":"#/components/schemas/ReceptionistSourceExtractionReport"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["content"],"title":"ReceptionistSourceRefreshRequest","description":"Manual refresh of an existing website snapshot; it creates a new immutable row."}
- ReceptionistSourceResource: {"properties":{"id":{"type":"string","title":"Id"},"kind":{"type":"string","enum":["document","website"],"title":"Kind"},"label":{"type":"string","title":"Label"},"source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Url"},"provenance":{"type":"string","enum":["customer_upload","customer_website_snapshot"],"title":"Provenance"},"content_hash":{"type":"string","title":"Content Hash"},"content_characters":{"type":"integer","title":"Content Characters"},"content_token_count":{"type":"integer","title":"Content Token Count"},"extraction_report":{"$ref":"#/components/schemas/ReceptionistSourceExtractionReport"},"refresh_status":{"type":"string","const":"manual_refresh_required","title":"Refresh Status"},"refresh_of_source_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refresh Of Source Id"},"captured_at":{"type":"string","format":"date-time","title":"Captured At"},"review_status":{"type":"string","enum":["pending","approved","rejected"],"title":"Review Status"},"review_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Review Note"},"reviewed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Reviewed At"},"approved_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Approved At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"content":{"type":"string","title":"Content"}},"additionalProperties":false,"type":"object","required":["id","kind","label","provenance","content_hash","content_characters","content_token_count","extraction_report","refresh_status","captured_at","review_status","created_at","content"],"title":"ReceptionistSourceResource","description":"A customer-readable immutable snapshot used to review a citation."}
- ReceptionistSourceReviewRequest: {"properties":{"note":{"anyOf":[{"type":"string","maxLength":2000,"minLength":1},{"type":"null"}],"title":"Note"}},"additionalProperties":false,"type":"object","title":"ReceptionistSourceReviewRequest","description":"Optional reviewer note; rejection endpoints require one to preserve the reason."}
- ReceptionistSourceSummary: {"properties":{"id":{"type":"string","title":"Id"},"kind":{"type":"string","enum":["document","website"],"title":"Kind"},"label":{"type":"string","title":"Label"},"source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Url"},"provenance":{"type":"string","enum":["customer_upload","customer_website_snapshot"],"title":"Provenance"},"content_hash":{"type":"string","title":"Content Hash"},"content_characters":{"type":"integer","title":"Content Characters"},"content_token_count":{"type":"integer","title":"Content Token Count"},"extraction_report":{"$ref":"#/components/schemas/ReceptionistSourceExtractionReport"},"refresh_status":{"type":"string","const":"manual_refresh_required","title":"Refresh Status"},"refresh_of_source_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refresh Of Source Id"},"captured_at":{"type":"string","format":"date-time","title":"Captured At"},"review_status":{"type":"string","enum":["pending","approved","rejected"],"title":"Review Status"},"review_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Review Note"},"reviewed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Reviewed At"},"approved_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Approved At"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"additionalProperties":false,"type":"object","required":["id","kind","label","provenance","content_hash","content_characters","content_token_count","extraction_report","refresh_status","captured_at","review_status","created_at"],"title":"ReceptionistSourceSummary"}
- RecordedCallResponse: {"properties":{"id":{"type":"string","title":"Id"},"kind":{"type":"string","enum":["shift_response","qualification","identity_capture","identity_correction"],"title":"Kind"},"subject_ref":{"type":"string","title":"Subject Ref"},"response":{"type":"string","title":"Response"},"revision":{"type":"integer","title":"Revision"},"source_turn_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Source Turn Number"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"},"note_truncated":{"type":"boolean","title":"Note Truncated","default":false},"value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Value"},"value_truncated":{"type":"boolean","title":"Value Truncated","default":false},"fit_review":{"anyOf":[{"$ref":"#/components/schemas/QualificationFitPreview"},{"type":"null"}]}},"type":"object","required":["id","kind","subject_ref","response","revision"],"title":"RecordedCallResponse","description":"Preview of a saved action, distinct from generated post-call analysis."}
- ReleaseGateEvaluation: {"properties":{"id":{"type":"string","title":"Id"},"agent_definition_id":{"type":"string","title":"Agent Definition Id"},"status":{"type":"string","enum":["queued","running","completed","failed"],"title":"Status"},"mode":{"type":"string","enum":["scripted_stand_in","routing_chain"],"title":"Mode"},"draft_hash":{"type":"string","title":"Draft Hash"},"draft_updated_at":{"type":"string","format":"date-time","title":"Draft Updated At"},"dataset_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dataset Ref"},"results":{"additionalProperties":true,"type":"object","title":"Results"},"aggregate_score":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Aggregate Score"},"passed":{"type":"boolean","title":"Passed"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","agent_definition_id","status","mode","draft_hash","draft_updated_at","results","passed","created_at"],"title":"ReleaseGateEvaluation","description":"A customer-visible release-gate request and its immutable replay verdict.\n\nThe request snapshot remains in the restricted eval ledger rather than being echoed\nwholesale to a browser. These fields show which draft revision the worker evaluated,\nwhich built-in dataset it used, and every assertion result once it settles."}
- ReleaseGateEvaluationRequest: {"properties":{"id":{"type":"string","title":"Id"},"agent_definition_id":{"type":"string","title":"Agent Definition Id"},"status":{"type":"string","enum":["queued","running","completed","failed"],"title":"Status"},"mode":{"type":"string","enum":["scripted_stand_in","routing_chain"],"title":"Mode"},"draft_hash":{"type":"string","title":"Draft Hash"},"draft_updated_at":{"type":"string","format":"date-time","title":"Draft Updated At"},"dataset_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dataset Ref"},"results":{"additionalProperties":true,"type":"object","title":"Results"},"aggregate_score":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Aggregate Score"},"passed":{"type":"boolean","title":"Passed"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"reused":{"type":"boolean","title":"Reused"}},"type":"object","required":["id","agent_definition_id","status","mode","draft_hash","draft_updated_at","results","passed","created_at","reused"],"title":"ReleaseGateEvaluationRequest","description":"A duplicate request returns its existing durable queue row."}
- RepGroup: {"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"selection_strategy":{"type":"string","const":"first_available","title":"Selection Strategy"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","name","selection_strategy","created_at","updated_at"],"title":"RepGroup"}
- RepGroupCreateRequest: {"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"},"selection_strategy":{"type":"string","const":"first_available","title":"Selection Strategy","default":"first_available"}},"additionalProperties":false,"type":"object","required":["name"],"title":"RepGroupCreateRequest"}
- RepGroupMember: {"properties":{"id":{"type":"string","title":"Id"},"rep_group_id":{"type":"string","title":"Rep Group Id"},"rep_id":{"type":"string","title":"Rep Id"},"added_at":{"type":"string","format":"date-time","title":"Added At"}},"type":"object","required":["id","rep_group_id","rep_id","added_at"],"title":"RepGroupMember"}
- RepPresence: {"properties":{"rep_id":{"type":"string","title":"Rep Id"},"status":{"type":"string","enum":["available","busy","offline"],"title":"Status"},"lease_expires_at":{"type":"string","format":"date-time","title":"Lease Expires At"},"last_heartbeat_at":{"type":"string","format":"date-time","title":"Last Heartbeat At"},"fresh":{"type":"boolean","title":"Fresh"},"manual_only":{"type":"boolean","const":true,"title":"Manual Only","default":true},"automatic_transfer_eligible":{"type":"boolean","const":false,"title":"Automatic Transfer Eligible","default":false}},"type":"object","required":["rep_id","status","lease_expires_at","last_heartbeat_at","fresh"],"title":"RepPresence"}
- RepPresenceHeartbeatRequest: {"properties":{"status":{"type":"string","enum":["available","busy","offline"],"title":"Status"}},"additionalProperties":false,"type":"object","required":["status"],"title":"RepPresenceHeartbeatRequest","description":"Renew one rep's short manual-availability lease.\n\nThe caller cannot choose a longer expiry. A real gateway will authenticate its own\nrep-session credential in addition to this tenant API surface; until then this endpoint\nis an auditable management-plane advertisement, not proof of a connected phone and never\nsufficient for automatic transfer selection."}
- ReportWindow: {"properties":{"start":{"type":"string","format":"date-time","title":"Start"},"end":{"type":"string","format":"date-time","title":"End"}},"type":"object","required":["start","end"],"title":"ReportWindow","description":"The half-open window the report was actually computed over."}
- RequestResolutionCount: {"properties":{"kind":{"type":"string","enum":["message","support_ticket"],"title":"Kind"},"open_count":{"type":"integer","minimum":0,"title":"Open Count","default":0},"acknowledged_count":{"type":"integer","minimum":0,"title":"Acknowledged Count","default":0},"resolved_count":{"type":"integer","minimum":0,"title":"Resolved Count","default":0}},"type":"object","required":["kind"],"title":"RequestResolutionCount"}
- RequestResolutionSummary: {"properties":{"window":{"$ref":"#/components/schemas/DashboardWindow"},"as_of":{"type":"string","format":"date-time","title":"As Of"},"counts":{"items":{"$ref":"#/components/schemas/RequestResolutionCount"},"type":"array","title":"Counts"}},"type":"object","required":["window","as_of","counts"],"title":"RequestResolutionSummary","description":"Request creation defines the cohort; state reflects current team progress.\n\nCounts are requests, not calls. Two messages on one call count separately.\nA resolution after the selected period is included in the current state."}
- RetentionNotice: {"properties":{"category":{"type":"string","title":"Category"},"handling":{"type":"string","title":"Handling"}},"type":"object","required":["category","handling"],"title":"RetentionNotice"}
- RetryPolicy: {"properties":{"backoff_minutes":{"items":{"type":"integer"},"type":"array","title":"Backoff Minutes"},"retry_dispositions":{"items":{"$ref":"#/components/schemas/Disposition"},"type":"array","title":"Retry Dispositions"}},"additionalProperties":false,"type":"object","title":"RetryPolicy","description":"Disposition-driven backoff: the spacing ladder and the retriable dispositions."}
- RingHumansStep: {"properties":{"type":{"type":"string","const":"ring_humans","title":"Type","default":"ring_humans"},"rep_ids":{"items":{"type":"string","maxLength":128,"minLength":1},"type":"array","maxItems":10,"minItems":1,"title":"Rep Ids"},"timeout_seconds":{"type":"integer","maximum":120,"minimum":5,"title":"Timeout Seconds","default":20}},"additionalProperties":false,"type":"object","required":["rep_ids"],"title":"RingHumansStep","description":"Ring real people, in the order the customer listed them.\n\nOnly ``rep_id`` values, resolved at build time against active transfer-enabled reps.\nOrder is meaningful and preserved, because an escalation list whose order the server\nre-derives is not the list the customer wrote."}
- SayStep: {"properties":{"type":{"type":"string","const":"say","title":"Type","default":"say"},"message":{"type":"string","maxLength":1000,"minLength":1,"title":"Message"}},"additionalProperties":false,"type":"object","required":["message"],"title":"SayStep","description":"Speak one message, then hang up. The terminal step every chain can fall back to."}
- ScoringBlock: {"properties":{"scale":{"$ref":"#/components/schemas/SurveyScale"},"points":{"anyOf":[{"type":"integer","minimum":2},{"type":"null"}],"title":"Points"},"mapping":{"additionalProperties":{"type":"number"},"type":"object","minProperties":1,"title":"Mapping"},"weight":{"type":"number","minimum":0,"title":"Weight","default":1}},"additionalProperties":false,"type":"object","required":["scale","mapping"],"title":"ScoringBlock","description":"One question's frozen scoring vocabulary (docs/08 §Objective scoring)."}
- ScreeningResult: {"properties":{"verdict":{"type":"string","enum":["eligible","needs_review","blocked"],"title":"Verdict"},"reasons":{"items":{"type":"string"},"type":"array","title":"Reasons"},"minimum_class":{"anyOf":[{"type":"string","enum":["engage","workforce","sales"]},{"type":"null"}],"title":"Minimum Class"},"uncovered_states":{"items":{"type":"string"},"type":"array","title":"Uncovered States"},"waiver_applied":{"type":"boolean","title":"Waiver Applied","default":false},"curable_by_signature":{"type":"boolean","title":"Curable By Signature","default":false},"screened_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Screened At"}},"type":"object","required":["verdict","reasons"],"title":"ScreeningResult","description":"The server's verdict. `blocked` and `needs_review` are both closed doors - neither\nis a state the applicant can click past."}
- ScrubResultSummary: {"properties":{"id":{"type":"string","title":"Id"},"contact_id":{"type":"string","title":"Contact Id"},"verdict":{"type":"string","enum":["dialable","blocked"],"title":"Verdict"},"blocked_reasons":{"items":{"type":"string"},"type":"array","title":"Blocked Reasons"},"risk_flags":{"items":{"type":"string"},"type":"array","title":"Risk Flags"},"line_type":{"anyOf":[{"$ref":"#/components/schemas/LineType"},{"type":"null"}]},"consent_record_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Consent Record Id"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","contact_id","verdict","created_at"],"title":"ScrubResultSummary","description":"One contact's row in the report."}
- ScrubRun: {"properties":{"id":{"type":"string","title":"Id"},"contact_list_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact List Id"},"status":{"$ref":"#/components/schemas/JobStatus"},"totals":{"$ref":"#/components/schemas/ScrubRunTotals"},"blocked_breakdown":{"additionalProperties":{"type":"integer"},"type":"object","title":"Blocked Breakdown"},"risk_flags":{"items":{"type":"string"},"type":"array","title":"Risk Flags"},"dataset_versions":{"additionalProperties":{"type":"string"},"type":"object","title":"Dataset Versions"},"waived_stages":{"items":{"$ref":"#/components/schemas/WaivedStage"},"type":"array","title":"Waived Stages"},"started_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Started At"},"completed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Completed At"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"is_stale":{"type":"boolean","title":"Is Stale","default":false},"acknowledged_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Acknowledged By"},"acknowledged_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Acknowledged At"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","contact_list_id","status","totals","created_at"],"title":"ScrubRun","description":"The customer-facing scrub report."}
- ScrubRunAcknowledgeRequest: {"properties":{"acknowledged_by":{"type":"string","minLength":1,"title":"Acknowledged By"},"totals":{"$ref":"#/components/schemas/ScrubRunTotals"}},"type":"object","required":["acknowledged_by","totals"],"title":"ScrubRunAcknowledgeRequest","description":"`totals` is the report the acknowledger saw, and is checked against the run."}
- ScrubRunAcknowledgement: {"properties":{"scrub_run_id":{"type":"string","title":"Scrub Run Id"},"acknowledged_by":{"type":"string","title":"Acknowledged By"},"acknowledged_at":{"type":"string","format":"date-time","title":"Acknowledged At"},"totals":{"$ref":"#/components/schemas/ScrubRunTotals"}},"type":"object","required":["scrub_run_id","acknowledged_by","acknowledged_at","totals"],"title":"ScrubRunAcknowledgement"}
- ScrubRunCreateRequest: {"properties":{"target":{"$ref":"#/components/schemas/ScrubRunTarget"}},"type":"object","required":["target"],"title":"ScrubRunCreateRequest"}
- ScrubRunTarget: {"properties":{"contact_list_id":{"type":"string","title":"Contact List Id"}},"type":"object","required":["contact_list_id"],"title":"ScrubRunTarget"}
- ScrubRunTotals: {"properties":{"contacts":{"type":"integer","title":"Contacts"},"dialable":{"type":"integer","title":"Dialable"},"blocked":{"type":"integer","title":"Blocked"},"flagged":{"type":"integer","title":"Flagged"}},"type":"object","required":["contacts","dialable","blocked","flagged"],"title":"ScrubRunTotals","description":"The funnel a launch is authorized against."}
- SessionSummary: {"properties":{"account":{"$ref":"#/components/schemas/AccountSummary"},"csrf_token":{"type":"string","title":"Csrf Token"}},"type":"object","required":["account","csrf_token"],"title":"SessionSummary"}
- ShiftConfirmation: {"properties":{"id":{"type":"string","title":"Id"},"call_id":{"type":"string","title":"Call Id"},"contact_id":{"type":"string","title":"Contact Id"},"shift_ref":{"type":"string","title":"Shift Ref"},"site":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site"},"shift_starts_at":{"type":"string","format":"date-time","title":"Shift Starts At"},"response":{"type":"string","enum":["confirmed","declined","tentative","unreachable"],"title":"Response"},"decline_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Decline Reason"},"confirmed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Confirmed At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","call_id","contact_id","shift_ref","shift_starts_at","response","created_at","updated_at"],"title":"ShiftConfirmation"}
- ShiftConfirmationOutcome: {"properties":{"kind":{"type":"string","const":"shift_confirmation","title":"Kind","default":"shift_confirmation"},"id":{"type":"string","title":"Id"},"response":{"type":"string","enum":["confirmed","declined","tentative","unreachable"],"title":"Response"},"shift_ref":{"type":"string","title":"Shift Ref"},"site":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site"},"shift_starts_at":{"type":"string","format":"date-time","title":"Shift Starts At"}},"type":"object","required":["id","response","shift_ref","shift_starts_at"],"title":"ShiftConfirmationOutcome"}
- SideEffect: {"type":"string","enum":["read_only","mutating"],"title":"SideEffect","description":"Whether an invocation only reads, or changes state something else can observe.\n\nTwo values by design: anything that is not a lookup gets an idempotency key. That is\nwhy ``end_call`` - runtime-local, but a state change - is ``MUTATING``."}
- SmsSendRequest: {"properties":{"from_phone_number_id":{"type":"string","title":"From Phone Number Id"},"to_e164":{"type":"string","pattern":"^\\+[1-9]\\d{1,14}$","title":"To E164"},"template_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Template Id"},"body":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}],"title":"Body"},"variables":{"additionalProperties":{"anyOf":[{"type":"string"},{"type":"integer"},{"type":"number"},{"type":"boolean"}]},"type":"object","title":"Variables"},"origin_call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin Call Id"}},"type":"object","required":["from_phone_number_id","to_e164"],"title":"SmsSendRequest"}
- SpendCap: {"properties":{"amount_minor":{"type":"integer","title":"Amount Minor"},"period":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Period"},"source":{"type":"string","enum":["wallet","subscription_override"],"title":"Source"}},"type":"object","required":["amount_minor","source"],"title":"SpendCap","description":"The cap pacer governor #4 enforces, wherever it is currently stored.\n\n``wallet`` is the canonical P1 store; ``subscription_override`` is the P0 store that\nremains readable until every cap has migrated (docs/11-billing.md §Wallet)."}
- Subscription: {"properties":{"id":{"type":"string","title":"Id"},"status":{"type":"string","enum":["trialing","active","past_due","unpaid","canceled"],"title":"Status"},"plan":{"$ref":"#/components/schemas/PlanSummary"},"workload_class":{"anyOf":[{"type":"string","enum":["engage","workforce","sales","receptionist"]},{"type":"null"}],"title":"Workload Class"},"extra_sessions":{"type":"integer","title":"Extra Sessions","default":0},"total_sessions":{"type":"integer","title":"Total Sessions"},"receptionist_connections":{"type":"integer","title":"Receptionist Connections","default":0},"total_receptionist_connections":{"type":"integer","title":"Total Receptionist Connections"},"minute_rate_minor":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Minute Rate Minor"},"current_period_start":{"type":"string","format":"date-time","title":"Current Period Start"},"current_period_end":{"type":"string","format":"date-time","title":"Current Period End"},"overrides":{"additionalProperties":true,"type":"object","title":"Overrides"},"spend_cap":{"anyOf":[{"$ref":"#/components/schemas/SpendCap"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","status","plan","total_sessions","total_receptionist_connections","minute_rate_minor","current_period_start","current_period_end","created_at","updated_at"],"title":"Subscription"}
- SubscriptionCommitment: {"properties":{"status":{"type":"string","enum":["not_committed","active","term_elapsed","review_required"],"title":"Status"},"starts_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Starts At"},"ends_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ends At"}},"type":"object","required":["status"],"title":"SubscriptionCommitment","description":"Known original term dates; an elapsed term states no renewal or cancellation policy."}
- SubscriptionCreated: {"properties":{"subscription_id":{"type":"string","title":"Subscription Id"},"external_ref":{"type":"string","title":"External Ref"},"status":{"type":"string","title":"Status"},"plan_id":{"type":"string","title":"Plan Id"},"plan_name":{"type":"string","title":"Plan Name"},"workload_class":{"type":"string","enum":["engage","workforce","sales","receptionist"],"title":"Workload Class"},"term":{"type":"string","enum":["mtm","three_year"],"title":"Term"},"base_price_minor":{"type":"integer","title":"Base Price Minor"},"currency":{"type":"string","title":"Currency"},"extra_sessions":{"type":"integer","title":"Extra Sessions"},"total_sessions":{"type":"integer","title":"Total Sessions"},"extra_session_monthly_minor":{"type":"integer","title":"Extra Session Monthly Minor"},"receptionist_connections":{"type":"integer","title":"Receptionist Connections"},"total_receptionist_connections":{"type":"integer","title":"Total Receptionist Connections"},"receptionist_base_monthly_minor":{"type":"integer","title":"Receptionist Base Monthly Minor"},"receptionist_connection_monthly_minor":{"type":"integer","title":"Receptionist Connection Monthly Minor"},"current_period_start":{"type":"string","format":"date-time","title":"Current Period Start"},"current_period_end":{"type":"string","format":"date-time","title":"Current Period End"},"sandbox_credit_redeemed_minor":{"type":"integer","title":"Sandbox Credit Redeemed Minor"},"sandbox_credit_unconfirmed":{"type":"boolean","title":"Sandbox Credit Unconfirmed","default":false}},"type":"object","required":["subscription_id","external_ref","status","plan_id","plan_name","workload_class","term","base_price_minor","currency","extra_sessions","total_sessions","extra_session_monthly_minor","receptionist_connections","total_receptionist_connections","receptionist_base_monthly_minor","receptionist_connection_monthly_minor","current_period_start","current_period_end","sandbox_credit_redeemed_minor"],"title":"SubscriptionCreated"}
- SubscriptionRequest: {"properties":{"workload_class":{"type":"string","enum":["engage","workforce","sales","receptionist"],"title":"Workload Class"},"term":{"type":"string","enum":["mtm","three_year"],"title":"Term"},"extra_sessions":{"type":"integer","minimum":0,"title":"Extra Sessions","default":0},"receptionist_connections":{"type":"integer","minimum":0,"title":"Receptionist Connections","default":0}},"type":"object","required":["workload_class","term"],"title":"SubscriptionRequest","description":"The published card's two axes plus the one published add-on quantity.\n\nNo price and no rate: the catalog owns every figure (`vocapable_domain.rate_card`),\nand a body that could name one would be a client setting its own price.\n``extra_sessions`` is a *quantity* of the one published $200/mo add-on (ADR-0022) -\ndeliberately unbounded above, per the card (\"any quantity\"); actual concurrency stays\nbounded by fleet capacity, which is an operations fact, not a checkout refusal.\n\n``receptionist_connections`` is the same shape for inbound answering (ADR-0025) and is\na count of connections bought **on top of** whatever the plan already includes: the\nstandalone `receptionist` plan's $250 base fee carries the first one, so `0` there is a\nworking one-connection receptionist, while an outbound plan includes none and buys its\nfirst connection here. `receptionist_line_items` owns that asymmetry; nothing on this\nrequest states which item bills."}
- SuccessEvaluation: {"properties":{"verdict":{"type":"string","enum":["success","failure"],"title":"Verdict"},"rubric_version":{"type":"string","title":"Rubric Version"},"dimensions":{"$ref":"#/components/schemas/AnalysisDimensions"},"score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Score"}},"type":"object","required":["verdict","rubric_version","dimensions"],"title":"SuccessEvaluation","description":"The verdict block. `rubric_version` pins the `AgentVersion` whose rubric scored the\ncall, so re-publishing an agent never restates past analyses (docs/04-api.md Step 8a)."}
- SurveyAggregate: {"properties":{"csat":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Csat"},"nps":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Nps"},"weighted_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Weighted Score"},"scored_responses":{"type":"integer","title":"Scored Responses"}},"type":"object","required":["scored_responses"],"title":"SurveyAggregate","description":"The scale-native aggregates plus the normalized score."}
- SurveyFunnel: {"properties":{"targeted":{"type":"integer","title":"Targeted"},"dialed":{"type":"integer","title":"Dialed"},"answered":{"type":"integer","title":"Answered"},"survey_started":{"type":"integer","title":"Survey Started"},"survey_complete":{"type":"integer","title":"Survey Complete"},"survey_partial":{"type":"integer","title":"Survey Partial"},"survey_refused":{"type":"integer","title":"Survey Refused"},"response_rate":{"type":"number","title":"Response Rate"},"completion_rate":{"type":"number","title":"Completion Rate"}},"type":"object","required":["targeted","dialed","answered","survey_started","survey_complete","survey_partial","survey_refused","response_rate","completion_rate"],"title":"SurveyFunnel","description":"Contacts, not attempts.\n\n`response_rate` is completed surveys over **contacts dialed** and `completion_rate` is\ncompleted over started (docs/04-api.md §2 step 8c), so the three funnel counts above\nthem are per-contact too - a contact dialed three times is one dialed contact, and the\nfunnel stays monotone."}
- SurveyQuestionRollup: {"properties":{"objective_id":{"type":"string","title":"Objective Id"},"prompt":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prompt"},"scale":{"$ref":"#/components/schemas/SurveyScale"},"points":{"type":"integer","title":"Points"},"answered":{"type":"integer","title":"Answered"},"distribution":{"additionalProperties":{"type":"integer"},"type":"object","title":"Distribution"},"mean":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Mean"},"top_2_box_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Top 2 Box Rate"},"promoters":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Promoters"},"passives":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Passives"},"detractors":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Detractors"}},"type":"object","required":["objective_id","scale","points","answered"],"title":"SurveyQuestionRollup","description":"One scored question of the pinned version.\n\n`scale` is the complete enum `csat|nps|ordinal|boolean` and `points` is the separate\nchoice count - the count is never folded into the scale name (CANON fact 63). An\nobjective carrying no `scoring` block is unscored and never appears here, though its\nanswers still count toward the funnel's completion states."}
- SurveyResponse: {"properties":{"id":{"type":"string","title":"Id"},"call_id":{"type":"string","title":"Call Id"},"agent_version_id":{"type":"string","title":"Agent Version Id"},"answers":{"additionalProperties":true,"type":"object","title":"Answers"},"completion_status":{"type":"string","enum":["complete","partial","refused"],"title":"Completion Status"},"score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Score"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","call_id","agent_version_id","completion_status","created_at","updated_at"],"title":"SurveyResponse"}
- SurveyResponseOutcome: {"properties":{"kind":{"type":"string","const":"survey_response","title":"Kind","default":"survey_response"},"id":{"type":"string","title":"Id"},"completion_status":{"type":"string","enum":["complete","partial","refused"],"title":"Completion Status"},"score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Score"}},"type":"object","required":["id","completion_status"],"title":"SurveyResponseOutcome"}
- SurveyRollup: {"properties":{"campaign_id":{"type":"string","title":"Campaign Id"},"agent_version_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Version Id"},"window":{"$ref":"#/components/schemas/SurveyRollupWindow"},"funnel":{"$ref":"#/components/schemas/SurveyFunnel"},"questions":{"items":{"$ref":"#/components/schemas/SurveyQuestionRollup"},"type":"array","title":"Questions"},"aggregate":{"$ref":"#/components/schemas/SurveyAggregate"}},"type":"object","required":["campaign_id","window","funnel","aggregate"],"title":"SurveyRollup"}
- SurveyRollupWindow: {"properties":{"from":{"type":"string","format":"date-time","title":"From"},"to":{"type":"string","format":"date-time","title":"To"}},"type":"object","required":["from","to"],"title":"SurveyRollupWindow","description":"The window actually aggregated - the request's, clamped to the campaign's lifetime."}
- SurveyScale: {"type":"string","enum":["csat","nps","ordinal","boolean"],"title":"SurveyScale","description":"Scoring scales for survey objectives (CANON fact 63).\n\nThe number of points is a separate `points` field, never part of the scale name; a\nquestion with no `scoring` block at all is simply unscored."}
- TelephonyAccountCreateRequest: {"properties":{"mode":{"type":"string","enum":["byo","managed"],"title":"Mode"},"provider":{"type":"string","title":"Provider","default":"twilio"},"credentials":{"anyOf":[{"$ref":"#/components/schemas/TelephonyCredentials"},{"type":"null"}]},"a2p_brand_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"A2P Brand Ref"},"ftc_san_refs":{"items":{"$ref":"#/components/schemas/FtcSanRef"},"type":"array","title":"Ftc San Refs"}},"additionalProperties":false,"type":"object","required":["mode"],"title":"TelephonyAccountCreateRequest","description":"Connect a BYO subaccount, or elect the platform account with ``mode: managed``."}
- TelephonyAccountSummary: {"properties":{"id":{"type":"string","title":"Id"},"mode":{"type":"string","enum":["byo","managed"],"title":"Mode"},"provider":{"type":"string","title":"Provider"},"credentials_stored":{"type":"boolean","title":"Credentials Stored"},"cps_limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Cps Limit"},"a2p_brand_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"A2P Brand Ref"},"ftc_san_refs":{"items":{"$ref":"#/components/schemas/FtcSanRef"},"type":"array","title":"Ftc San Refs"},"verification_status":{"type":"string","enum":["pending","verifying","mock_verified","verified","failed","suspended"],"title":"Verification Status"},"verified_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Verified At"},"telephony_minutes_metered":{"type":"boolean","title":"Telephony Minutes Metered"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","mode","provider","credentials_stored","verification_status","telephony_minutes_metered","created_at"],"title":"TelephonyAccountSummary","description":"An account as its tenant sees it - credential material is structurally absent."}
- TelephonyAccountUpdateRequest: {"properties":{"credentials":{"anyOf":[{"$ref":"#/components/schemas/TelephonyCredentials"},{"type":"null"}]},"a2p_brand_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"A2P Brand Ref"},"ftc_san_refs":{"anyOf":[{"items":{"$ref":"#/components/schemas/FtcSanRef"},"type":"array"},{"type":"null"}],"title":"Ftc San Refs"}},"additionalProperties":false,"type":"object","title":"TelephonyAccountUpdateRequest","description":"Partial update. Rotation is this request carrying a fresh credential pair."}
- TelephonyCredentials: {"properties":{"account_sid":{"type":"string","format":"password","title":"Account Sid","writeOnly":true},"auth_token":{"type":"string","format":"password","title":"Auth Token","writeOnly":true}},"additionalProperties":false,"type":"object","required":["account_sid","auth_token"],"title":"TelephonyCredentials","description":"Carrier credentials on their way to the vault. Never present on a response.\n\n``SecretStr`` so a body that reaches a log line, a traceback, or a validation error\nrenders as ``**********`` rather than a usable secret (docs/10-security.md §secrets).\n\nThe two fields are the incumbent's credential pair, and pinning them here is\ndeliberate: the vault beneath stores an arbitrary flat map and the account row holds\nan opaque text reference, so a second carrier's credential shape is a change to this\nmodel plus its readers - a contracts change, never a migration (ADR-0030 decision 8)."}
- TemplateInstantiateRequest: {"properties":{"name":{"type":"string","maxLength":200,"minLength":1,"title":"Name"},"disclosure_config":{"additionalProperties":{"type":"string"},"type":"object","minProperties":1,"title":"Disclosure Config"},"persona":{"additionalProperties":true,"type":"object","title":"Persona"},"conversation_settings":{"$ref":"#/components/schemas/ConversationSettings"},"llm_route":{"anyOf":[{"$ref":"#/components/schemas/LlmRoute"},{"type":"null"}]},"stock_voice_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stock Voice Id"},"voice_profile_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Voice Profile Id"},"knowledge_pack_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Knowledge Pack Id"},"knowledge_pack_version":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"title":"Knowledge Pack Version"},"compliance_profile_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Compliance Profile Id"}},"additionalProperties":false,"type":"object","required":["name","disclosure_config"],"title":"TemplateInstantiateRequest","description":"`POST /v1/templates/{id}/instantiate`.\n\n`disclosure_config` is required here even though the template has none: the opener\nnames the calling business, so it cannot be seeded from platform-owned reference data,\nand an AgentDefinition may not exist without one (CANON fact 71)."}
- TemplateResource: {"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"use_case":{"type":"string","enum":["sdr","staffing","survey","reminder","custom","receptionist","recruiting"],"title":"Use Case"},"version":{"type":"integer","title":"Version"},"outcome_type":{"type":"string","enum":["appointment","shift_confirmation","survey_response","none"],"title":"Outcome Type"},"tool_grants":{"items":{"type":"string","enum":["check_calendar","book_meeting","send_sms","send_verification_code","verify_identity","record_sms_consent","take_message","open_ticket","update_lead","record_survey_answer","confirm_shift","record_shift_response","record_qualification","schedule_callback","transfer_to_human","record_optout","end_call","custom_webhook"]},"type":"array","title":"Tool Grants"},"webhook_payload_schema":{"additionalProperties":true,"type":"object","title":"Webhook Payload Schema"},"content_hash":{"type":"string","title":"Content Hash"},"status":{"type":"string","enum":["draft","published","deprecated"],"title":"Status"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"objectives":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Objectives"}},"type":"object","required":["id","name","use_case","version","outcome_type","tool_grants","webhook_payload_schema","content_hash","status","created_at","updated_at","objectives"],"title":"TemplateResource","description":"Adds the full seed - objectives and their `scoring` blocks."}
- TemplateSummary: {"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"use_case":{"type":"string","enum":["sdr","staffing","survey","reminder","custom","receptionist","recruiting"],"title":"Use Case"},"version":{"type":"integer","title":"Version"},"outcome_type":{"type":"string","enum":["appointment","shift_confirmation","survey_response","none"],"title":"Outcome Type"},"tool_grants":{"items":{"type":"string","enum":["check_calendar","book_meeting","send_sms","send_verification_code","verify_identity","record_sms_consent","take_message","open_ticket","update_lead","record_survey_answer","confirm_shift","record_shift_response","record_qualification","schedule_callback","transfer_to_human","record_optout","end_call","custom_webhook"]},"type":"array","title":"Tool Grants"},"webhook_payload_schema":{"additionalProperties":true,"type":"object","title":"Webhook Payload Schema"},"content_hash":{"type":"string","title":"Content Hash"},"status":{"type":"string","enum":["draft","published","deprecated"],"title":"Status"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","name","use_case","version","outcome_type","tool_grants","webhook_payload_schema","content_hash","status","created_at","updated_at"],"title":"TemplateSummary","description":"A platform-owned, versioned seed (CANON fact 72). Templates carry no tenant."}
- TenantInvitationAcceptanceRequest: {"properties":{"token":{"type":"string","maxLength":512,"minLength":43,"title":"Token"}},"additionalProperties":false,"type":"object","required":["token"],"title":"TenantInvitationAcceptanceRequest","description":"Browser-authenticated acceptance of a secret delivered only by invitation email."}
- TenantInvitationCreateRequest: {"properties":{"email":{"type":"string","maxLength":254,"minLength":3,"title":"Email"},"role":{"$ref":"#/components/schemas/TenantMembershipRole"},"human_rep_id":{"anyOf":[{"type":"string","maxLength":128,"minLength":1},{"type":"null"}],"title":"Human Rep Id"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"}},"additionalProperties":false,"type":"object","required":["email","role","expires_at"],"title":"TenantInvitationCreateRequest","description":"An admin-issued invitation. The server hashes its generated secret before write."}
- TenantInvitationSummary: {"properties":{"id":{"type":"string","title":"Id"},"email":{"type":"string","title":"Email"},"role":{"$ref":"#/components/schemas/TenantMembershipRole"},"human_rep_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Human Rep Id"},"status":{"type":"string","enum":["pending","accepted","revoked"],"title":"Status"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"accepted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Accepted At"},"revoked_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Revoked At"}},"type":"object","required":["id","email","role","status","expires_at"],"title":"TenantInvitationSummary"}
- TenantMembershipRole: {"type":"string","enum":["admin","manager","rep"],"title":"TenantMembershipRole","description":"The only roles a customer can hold in one workspace."}
- TenantMembershipSummary: {"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"account_id":{"type":"string","title":"Account Id"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"role":{"$ref":"#/components/schemas/TenantMembershipRole"},"human_rep_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Human Rep Id"},"status":{"type":"string","enum":["active","suspended","revoked"],"title":"Status"},"sessions_valid_after":{"type":"string","format":"date-time","title":"Sessions Valid After"}},"type":"object","required":["id","tenant_id","account_id","role","status","sessions_valid_after"],"title":"TenantMembershipSummary"}
- TenantMembershipUpdateRequest: {"properties":{"role":{"anyOf":[{"$ref":"#/components/schemas/TenantMembershipRole"},{"type":"null"}]},"status":{"anyOf":[{"type":"string","enum":["active","suspended","revoked"]},{"type":"null"}],"title":"Status"},"human_rep_id":{"anyOf":[{"type":"string","maxLength":128,"minLength":1},{"type":"null"}],"title":"Human Rep Id"},"replace_human_rep":{"type":"boolean","title":"Replace Human Rep","default":false}},"additionalProperties":false,"type":"object","title":"TenantMembershipUpdateRequest","description":"An admin's explicit membership role/status or human-rep-link change.\n\n``human_rep_id`` is intentionally ignored unless ``replace_human_rep`` is true. This\nmakes an omitted field different from an explicit unlink and prevents a mobile client\nfrom accidentally clearing a transfer-rep association while changing only a role."}
- TenantUserSummary: {"properties":{"id":{"type":"string","title":"Id"},"email":{"type":"string","title":"Email"},"role":{"type":"string","title":"Role"},"status":{"type":"string","title":"Status"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","email","role","status","created_at"],"title":"TenantUserSummary","description":"One row of the read-only tenant user directory (`GET /v1/users`).\n\nDeliberately narrow: enough to render an assignee picker and attribute work, never a\nmanagement surface - there is no invitation, role, or status write on `/v1`."}
- TestCall: {"properties":{"id":{"type":"string","title":"Id"},"agent_id":{"type":"string","title":"Agent Id"},"agent_version_id":{"type":"string","title":"Agent Version Id"},"to":{"type":"string","title":"To"},"contact_id":{"type":"string","title":"Contact Id"},"telephony":{"type":"string","enum":["mock","platform"],"title":"Telephony"},"state":{"type":"string","title":"State"},"disposition":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Disposition"},"call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","agent_id","agent_version_id","to","contact_id","telephony","state","created_at"],"title":"TestCall","description":"A test call as the 202 answers it - the campaign-less ContactAttempt (`att_…`) IS\nthe test-call resource, so its lifecycle is read from the attempt feed's vocabulary.\n\n`telephony` says which plane carried it: `mock` (a `vcp_test_` key - synchronous\nfull-pipeline dial, no PSTN) or `platform` (a live key - the attempt is `scheduled`\nand the origination loop sweeps it). `call_id` is written at teardown and is always\nnull in the 202."}
- TestCallRequest: {"properties":{"to":{"type":"string","title":"To"},"timezone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Timezone"},"jurisdictions":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Jurisdictions"}},"type":"object","required":["to"],"title":"TestCallRequest","description":"`POST /v1/agents/{id}/test-call` (docs/04-api.md §3).\n\n`to` must be a currently `verified` VerifiedNumber of the calling tenant - the\ntest-call precondition (CANON fact 120). The two optional gate facts exist because\nthe platform holds no NPA geodata: when no live Contact already carries them, the\nbody must supply the callee-local zone and jurisdictions or the gate fails closed\n(`contact_timezone_unresolved` / `jurisdiction_unresolved`)."}
- UsageEventSummary: {"properties":{"id":{"type":"string","title":"Id"},"meter":{"type":"string","enum":["telephony_minutes","stt_audio_seconds","tts_characters","llm_prompt_tokens","llm_cached_prompt_tokens","llm_completion_tokens","gpu_seconds","sms_segments","lookup_requests"],"title":"Meter"},"quantity":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Quantity"},"call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Id"},"campaign_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Campaign Id"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"rated_amount_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rated Amount Minor"},"attributes":{"additionalProperties":true,"type":"object","title":"Attributes"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","meter","quantity","occurred_at","created_at"],"title":"UsageEventSummary","description":"One ledger row. Append-only - there is no update or delete surface anywhere."}
- UsageSummary: {"properties":{"period":{"type":"string","title":"Period"},"period_start":{"type":"string","format":"date-time","title":"Period Start"},"period_end":{"type":"string","format":"date-time","title":"Period End"},"telephony_mode":{"type":"string","enum":["managed","byo","mixed","unelected"],"title":"Telephony Mode"},"currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Currency"},"rated_total_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rated Total Minor"},"billing_period_estimate":{"anyOf":[{"$ref":"#/components/schemas/BillingPeriodEstimate"},{"type":"null"}]},"meters":{"items":{"$ref":"#/components/schemas/MeterUsage"},"type":"array","title":"Meters"}},"type":"object","required":["period","period_start","period_end","telephony_mode","meters"],"title":"UsageSummary","description":"One billing month across all nine meters, in registry order."}
- ValidationError: {"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}
- VerifiedNumberCheckRequest: {"properties":{"code":{"type":"string","maxLength":10,"minLength":4,"title":"Code"}},"additionalProperties":false,"type":"object","required":["code"],"title":"VerifiedNumberCheckRequest","description":"Confirm the one-time code delivered to the number."}
- VerifiedNumberCreateRequest: {"properties":{"phone_e164":{"type":"string","pattern":"^\\+[1-9]\\d{7,14}$","title":"Phone E164"},"label":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Label"}},"additionalProperties":false,"type":"object","required":["phone_e164"],"title":"VerifiedNumberCreateRequest","description":"Register a destination for OTP verification (docs/03-domain-model.md §VerifiedNumber)."}
- VerifiedNumberCreated: {"properties":{"id":{"type":"string","title":"Id"},"phone_e164":{"type":"string","title":"Phone E164"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"status":{"type":"string","enum":["pending","verified","expired","revoked"],"title":"Status"},"code_expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Code Expires At"},"verification_attempts":{"type":"integer","title":"Verification Attempts"},"verified_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Verified At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"verification_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Verification Code"}},"type":"object","required":["id","phone_e164","status","verification_attempts","created_at"],"title":"VerifiedNumberCreated","description":"The registration response. `verification_code` is populated **only** in non-prod\nenvironments, where no SMS/voice delivery channel exists yet; production refuses the\nregistration outright until the messaging adapter lands, rather than minting a code\nnobody can receive (no fake success)."}
- VerifiedNumberSummary: {"properties":{"id":{"type":"string","title":"Id"},"phone_e164":{"type":"string","title":"Phone E164"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"status":{"type":"string","enum":["pending","verified","expired","revoked"],"title":"Status"},"code_expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Code Expires At"},"verification_attempts":{"type":"integer","title":"Verification Attempts"},"verified_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Verified At"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","phone_e164","status","verification_attempts","created_at"],"title":"VerifiedNumberSummary","description":"A registration as its tenant sees it - the code and its hash are structurally absent."}
- VoicePreview: {"properties":{"script":{"type":"string","enum":["availability","pitch","survey"],"title":"Script"},"uri":{"type":"string","title":"Uri"}},"type":"object","required":["script","uri"],"title":"VoicePreview","description":"One rendered audition clip, addressable by the script it speaks.\n\n`uri` always sits on the platform asset origin: the catalog loader refuses any other\nhost, because a third-party synthesis presented as one of our voices misrepresents\nthe product. A script with no rendered clip is ABSENT from `VoiceSummary.previews`\nrather than present with a null uri - \"not rendered yet\" is a missing entry, never a\nplayer pointed at nothing."}
- VoiceProfileSummary: {"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"provider":{"type":"string","title":"Provider"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"},"status":{"type":"string","enum":["pending_review","approved","rejected","revoked"],"title":"Status"},"sample_recording_uri":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sample Recording Uri"},"model_artifact_uri":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model Artifact Uri"},"consent_proof_uri":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Consent Proof Uri"},"consent_verified_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Consent Verified By"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","name","provider","status","created_at","updated_at"],"title":"VoiceProfileSummary","description":"A voice profile as its tenant sees it.\n\n`consent_proof_uri` being null is exactly the gate: a profile cannot reach `approved`\nwithout one, enforced in the repository and again by a table CHECK constraint."}
- VoiceRightsAttestationDocument: {"properties":{"version":{"type":"string","title":"Version"},"content_sha256":{"type":"string","title":"Content Sha256"},"markdown":{"type":"string","title":"Markdown"}},"type":"object","required":["version","content_sha256","markdown"],"title":"VoiceRightsAttestationDocument","description":"`GET /v1/voice-rights-attestation` - the document to render and echo back."}
- VoiceSession: {"properties":{"call_id":{"type":"string","title":"Call Id"},"agent_id":{"type":"string","title":"Agent Id"},"agent_version_id":{"type":"string","title":"Agent Version Id"},"ws_url":{"type":"string","title":"Ws Url"},"gpu_node_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gpu Node Id"},"session_token":{"type":"string","title":"Session Token"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"protocol":{"type":"string","const":"browser.v1","title":"Protocol","default":"browser.v1"},"audio":{"$ref":"#/components/schemas/VoiceSessionAudio"},"max_call_seconds":{"type":"integer","title":"Max Call Seconds"},"disclosure_opener":{"type":"string","title":"Disclosure Opener"},"runs_draft":{"type":"boolean","title":"Runs Draft","default":false},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["call_id","agent_id","agent_version_id","ws_url","session_token","expires_at","audio","max_call_seconds","disclosure_opener","created_at"],"title":"VoiceSession","description":"`POST /v1/agents/{id}/voice-session` - one in-browser voice session, as the 201\nanswers it (docs/05-voice-runtime.md §Browser transport, CANON fact 103).\n\nNothing here names a called party, because there is none: the session is a transport\nswap onto the same runtime, so it carries the same pinned `AgentVersion`, the same\ncompliance floor, and the same single-use session token the carrier leg redeems - and\nno number, no contact, no attempt and no telephony account.\n\n`session_token` is a **single-use bearer credential**: it is redeemed once at\n`POST /internal/call-sessions/redeem` and is worthless afterwards. It is returned in\nthe body and never persisted by the API, which is why this endpoint accepts no\n`Idempotency-Key` (see the router's docstring).\n\n`disclosure_opener` is the exact first utterance the session will open with. Returned\nbefore the microphone is ever enabled so the portal can show the operator what the\nagent is about to say - the AI-disclosure floor as visible product surface rather than\nan invisible control (CANON fact 71)."}
- VoiceSessionAudio: {"properties":{"encoding":{"type":"string","const":"pcm16","title":"Encoding","default":"pcm16"},"sample_rate_hz":{"type":"integer","title":"Sample Rate Hz"},"channels":{"type":"integer","title":"Channels"}},"type":"object","required":["sample_rate_hz","channels"],"title":"VoiceSessionAudio","description":"The wire audio format both directions of a browser voice session carry.\n\nLoad-bearing rather than descriptive: `BrowserTransportSink.send_audio` RAISES on\nanything but 8 kHz mono PCM16 (`apps/voice-runtime/.../browser_transport.py`), and the\nbrowser is the side that resamples - there is no server-side resampler to rescue a\nclient that guessed 48 kHz. Returned in the 201 so the client reads the contract it\nmust satisfy instead of inferring it from a socket that closes mid-call."}
- VoiceSummary: {"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"language":{"type":"string","title":"Language"},"gender":{"type":"string","enum":["female","male","neutral"],"title":"Gender"},"style_tags":{"items":{"type":"string"},"type":"array","title":"Style Tags"},"previews":{"items":{"$ref":"#/components/schemas/VoicePreview"},"type":"array","title":"Previews"},"status":{"type":"string","enum":["available","coming_soon"],"title":"Status"},"preview_uri":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Preview Uri","description":"The single representative clip - kept so the one-player surfaces that predate\nthe three scripts (the agent editor's Voice sheet row, the admin roster row) keep\nworking unchanged against a catalog that now carries a set. The portal gallery is\nno longer one of them: it plays the whole set and captions each clip by script,\nand falls back here only against a deployment too old to serve `previews`.\n\nDerived, never authored: it is the first entry in :data:`PREVIEW_SCRIPT_ORDER`\nthis voice actually has, so it cannot drift out of step with `previews`. Null\nexactly when `previews` is empty.","readOnly":true}},"type":"object","required":["id","name","language","gender","style_tags","status","preview_uri"],"title":"VoiceSummary"}
- VoicemailConfig: {"properties":{"body":{"additionalProperties":{"type":"string"},"type":"object","title":"Body"},"callback_e164":{"anyOf":[{"type":"string","pattern":"^\\+[1-9]\\d{6,14}$"},{"type":"null"}],"title":"Callback E164"}},"additionalProperties":false,"type":"object","title":"VoicemailConfig","description":"The tenant's two slots in a platform-composed voicemail drop (ADR-0023).\n\nThis is the `DisclosureSlots` doctrine applied to voicemail: the customer supplies a\nbody and a callback number, and **nothing else**. The AI-disclosure opener, the order\nof the elements, and the identification/opt-out closing are composed by the platform\nfrom the same pinned version the live conversation discloses from, so a voicemail and\na conversation can never disagree about who is calling or that it is artificial.\n\n`body` is keyed by language tag exactly as `disclosure_config` is, and the drop\nresolves the *same* tag for both: a message whose opener and body were in different\nlanguages would be a disclosure in name only. An empty model is the ordinary state -\nit means this agent leaves no messages."}
- VoicemailDropPolicy: {"properties":{"enabled":{"type":"boolean","title":"Enabled","default":false}},"additionalProperties":false,"type":"object","title":"VoicemailDropPolicy","description":"Whether this program leaves artificial-voice voicemail (ADR-0023).\n\nOne field, and it is an election rather than a configuration: *what* is said belongs to\nthe pinned agent version, and *how many times* is not a knob at all. A drop is terminal\n- the attempt disposes `voicemail_machine` and never re-enters the retry ladder - so a\ncontact receives at most one message per campaign by construction, not by a counter a\ncaller could raise.\n\nLaunching with this enabled additionally requires the executed Cold Outreach & Data\nProvenance Waiver, and the waiver is re-checked again at the instant of every drop: a\ncampaign runs for days and a waiver version bump re-arms the gate by design."}
- VoicemailMessage: {"properties":{"agent_version_id":{"type":"string","title":"Agent Version Id"},"composable":{"type":"boolean","title":"Composable"},"refusal":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refusal"},"text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Text"},"elements":{"items":{"type":"string"},"type":"array","title":"Elements"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"},"callback_e164":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Callback E164"},"content_sha256":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content Sha256"},"estimated_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Estimated Seconds"}},"type":"object","required":["agent_version_id","composable"],"title":"VoicemailMessage","description":"`GET /v1/agents/{id}/versions/{v}/voicemail-message` - what will actually be said.\n\nA customer running a waivered cold-outreach program is legally responsible for the\nmessage; they must be able to read it, in full, before it is left on anyone's machine.\nServing it from the same composer and the same pinned snapshot the drop reads is what\nmakes this the message rather than a preview of one.\n\n`composable` is false - with `refusal` naming the floor control - where the version\nwould leave no message at all. That is a state worth reporting rather than a 409: the\nquestion \"what will be said\" has the honest answer \"nothing, and here is why\"."}
- VoicemailStep: {"properties":{"type":{"type":"string","const":"voicemail","title":"Type","default":"voicemail"},"greeting":{"type":"string","maxLength":1000,"minLength":1,"title":"Greeting"}},"additionalProperties":false,"type":"object","required":["greeting"],"title":"VoicemailStep","description":"Take a message. Consent is re-decided per call, never by this configuration.\n\nThe greeting here is the same kind of value ``inbound_voicemail_greeting`` is: a\ntenant-authored announcement. Whether a recording may actually be made is settled at\nadmission against the published recording policy and the caller's matched jurisdiction,\nexactly as the legacy ``voicemail`` route settles it - a policy document cannot grant\nitself a recording basis."}
- VolumeTierStatus: {"properties":{"base_minute_rate_minor":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Base Minute Rate Minor"},"current_minute_rate_minor":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Current Minute Rate Minor"},"bracket_size_minor":{"type":"integer","title":"Bracket Size Minor"},"discount_per_bracket_minor":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Discount Per Bracket Minor"},"floor_minute_rate_minor":{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$","title":"Floor Minute Rate Minor"},"cumulative_period_spend_minor":{"type":"integer","title":"Cumulative Period Spend Minor"},"next_tier_threshold_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Tier Threshold Minor"},"next_tier_minute_rate_minor":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Next Tier Minute Rate Minor"}},"type":"object","required":["base_minute_rate_minor","current_minute_rate_minor","bracket_size_minor","discount_per_bracket_minor","floor_minute_rate_minor","cumulative_period_spend_minor"],"title":"VolumeTierStatus","description":"Where the period stands on the volume-discount ladder (ADR-0022).\n\nRates are minor units per minute; ``cumulative_period_spend_minor`` is the bracket\nbasis - total period spend: base fee, extra-session fees, call fees, and minute\ncharges. ``base_minute_rate_minor`` is the tenant's RESOLVED starting rate (ADR-0028\nd7 as amended): the open ``subscription_terms`` row's negotiated rate when one\nexists, the term's published rate otherwise - the rate the engine actually descends\nfrom. ``next_tier_threshold_minor``/``next_tier_minute_rate_minor`` are null once\nthe floor is reached, and whenever the rate is pinned - a terms row snapshotting\n``volume_ladder_applies`` false, or a negotiated start already below the ladder's\nfloor."}
- WaivedStage: {"properties":{"stage":{"type":"string","title":"Stage"},"attestation_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attestation Ref"},"attested_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Attested By"}},"type":"object","required":["stage"],"title":"WaivedStage","description":"A bureau stage a signed attestation elected out of.\n\nReported so a stage that did not run never reads as a stage that found nothing. Under\nADR-0018 the election is a condition of production activation rather than a preference,\nwhich is why the signer is reported next to the reference: the report has to say whose\nsignature the missing stage stands on."}
- WebhookDelivery: {"properties":{"id":{"type":"string","title":"Id"},"endpoint_id":{"type":"string","title":"Endpoint Id"},"event_id":{"type":"string","title":"Event Id"},"attempt":{"type":"integer","title":"Attempt"},"status":{"type":"string","title":"Status"},"response_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Response Code"},"next_retry_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Next Retry At"},"delivered_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Delivered At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","endpoint_id","event_id","attempt","status","response_code","next_retry_at","delivered_at","created_at","updated_at"],"title":"WebhookDelivery","description":"One ledger row: a single (endpoint, event) pair, retried in place."}
- WebhookEndpointRequest: {"properties":{"url":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Url"},"enabled_events":{"items":{"type":"string","enum":["call.started","call.ended","campaign.started","campaign.paused","campaign.completed","appointment.booked","shift.confirmed","conversation.action_recorded","survey.completed","call.analyzed","optout.recorded","scrub.completed","export.completed","usage.threshold","lead.captured","message.taken","ticket.opened","ticket.resolved"]},"type":"array","title":"Enabled Events"},"api_version_pin":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Version Pin"}},"additionalProperties":false,"type":"object","required":["url","enabled_events"],"title":"WebhookEndpointRequest","description":"`PUT /webhook-endpoint` - the whole configuration, replaced as a unit.\n\n`enabled_events` is typed as the `EventType` literal, so subscribing to an event the\ncatalog does not define fails validation rather than silently never firing. An empty\nlist is legal: it pauses delivery without discarding the endpoint or its secret."}
- WebhookEndpointSecretView: {"properties":{"id":{"type":"string","title":"Id"},"url":{"type":"string","title":"Url"},"enabled_events":{"items":{"type":"string"},"type":"array","title":"Enabled Events"},"status":{"type":"string","title":"Status"},"api_version_pin":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Version Pin"},"previous_secret_expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Previous Secret Expires At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"secret":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Secret"}},"type":"object","required":["id","url","enabled_events","status","api_version_pin","previous_secret_expires_at","created_at","updated_at","secret"],"title":"WebhookEndpointSecretView","description":"A summary plus the plaintext secret, when the answering call minted one."}
- WebhookEndpointSummary: {"properties":{"id":{"type":"string","title":"Id"},"url":{"type":"string","title":"Url"},"enabled_events":{"items":{"type":"string"},"type":"array","title":"Enabled Events"},"status":{"type":"string","title":"Status"},"api_version_pin":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Version Pin"},"previous_secret_expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Previous Secret Expires At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","url","enabled_events","status","api_version_pin","previous_secret_expires_at","created_at","updated_at"],"title":"WebhookEndpointSummary","description":"The endpoint as every read renders it - structurally incapable of leaking a secret."}
- WebhookTargetCreateRequest: {"properties":{"webhook_id":{"type":"string","maxLength":64,"minLength":1,"title":"Webhook Id"},"description":{"type":"string","maxLength":500,"minLength":1,"title":"Description"},"parameters":{"additionalProperties":true,"type":"object","title":"Parameters"},"side_effect":{"$ref":"#/components/schemas/SideEffect"},"url":{"type":"string","maxLength":2048,"minLength":1,"title":"Url"},"timeout_seconds":{"type":"number","maximum":30,"exclusiveMinimum":0,"title":"Timeout Seconds","default":5},"max_invocations_per_call":{"type":"integer","maximum":20,"minimum":1,"title":"Max Invocations Per Call","default":3}},"additionalProperties":false,"type":"object","required":["webhook_id","description","parameters","side_effect","url"],"title":"WebhookTargetCreateRequest","description":"`POST /webhook-targets` - one spoken-name integration a live call may invoke.\n\nThe field bounds are the prompt-budget bounds from `vocapable_contracts.toolschema`:\nthe id lands in a schema enum the model reads and the description in the catalog it\nre-reads every turn, so what this model refuses is exactly what bind time would."}
- WebhookTargetSecretView: {"properties":{"id":{"type":"string","title":"Id"},"webhook_id":{"type":"string","title":"Webhook Id"},"description":{"type":"string","title":"Description"},"parameters":{"additionalProperties":true,"type":"object","title":"Parameters"},"side_effect":{"type":"string","title":"Side Effect"},"timeout_seconds":{"type":"number","title":"Timeout Seconds"},"max_invocations_per_call":{"type":"integer","title":"Max Invocations Per Call"},"url":{"type":"string","title":"Url"},"status":{"type":"string","title":"Status"},"previous_secret_expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Previous Secret Expires At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"secret":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Secret"}},"type":"object","required":["id","webhook_id","description","parameters","side_effect","timeout_seconds","max_invocations_per_call","url","status","previous_secret_expires_at","created_at","updated_at","secret"],"title":"WebhookTargetSecretView","description":"A summary plus the plaintext secret, when the answering call minted one."}
- WebhookTargetSummary: {"properties":{"id":{"type":"string","title":"Id"},"webhook_id":{"type":"string","title":"Webhook Id"},"description":{"type":"string","title":"Description"},"parameters":{"additionalProperties":true,"type":"object","title":"Parameters"},"side_effect":{"type":"string","title":"Side Effect"},"timeout_seconds":{"type":"number","title":"Timeout Seconds"},"max_invocations_per_call":{"type":"integer","title":"Max Invocations Per Call"},"url":{"type":"string","title":"Url"},"status":{"type":"string","title":"Status"},"previous_secret_expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Previous Secret Expires At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","webhook_id","description","parameters","side_effect","timeout_seconds","max_invocations_per_call","url","status","previous_secret_expires_at","created_at","updated_at"],"title":"WebhookTargetSummary","description":"A target as every read renders it - structurally incapable of leaking a secret."}
- WebhookTargetUpdateRequest: {"properties":{"description":{"anyOf":[{"type":"string","maxLength":500,"minLength":1},{"type":"null"}],"title":"Description"},"parameters":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Parameters"},"side_effect":{"anyOf":[{"$ref":"#/components/schemas/SideEffect"},{"type":"null"}]},"url":{"anyOf":[{"type":"string","maxLength":2048,"minLength":1},{"type":"null"}],"title":"Url"},"timeout_seconds":{"anyOf":[{"type":"number","maximum":30,"exclusiveMinimum":0},{"type":"null"}],"title":"Timeout Seconds"},"max_invocations_per_call":{"anyOf":[{"type":"integer","maximum":20,"minimum":1},{"type":"null"}],"title":"Max Invocations Per Call"},"status":{"anyOf":[{"type":"string","pattern":"^(active|disabled)$"},{"type":"null"}],"title":"Status"}},"additionalProperties":false,"type":"object","title":"WebhookTargetUpdateRequest","description":"`PATCH /webhook-targets/{id}`. Only the fields present in the body are applied.\n\n`webhook_id` is deliberately not editable: it is the identity the model reads, the\ntranscript records, and the eval store keys on. Renaming a target is registering a new\none and disabling the old. `status: \"active\"` is the re-enable path; the one-way\nconvenience for the other direction is `DELETE`."}
- WebhookTestResult: {"properties":{"delivered":{"type":"boolean","title":"Delivered"},"status_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Status Code"},"event_type":{"type":"string","title":"Event Type"},"detail":{"type":"string","title":"Detail"}},"type":"object","required":["delivered","status_code","event_type","detail"],"title":"WebhookTestResult","description":"What one signed sample POST to the tenant's endpoint answered."}
- WorkflowOutcome: {"properties":{"id":{"type":"string","title":"Id"},"call_id":{"type":"string","title":"Call Id"},"contact_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Id"},"kind":{"type":"string","enum":["shift_response","qualification","identity_capture","identity_correction"],"title":"Kind"},"subject_ref":{"type":"string","title":"Subject Ref"},"revision":{"type":"integer","minimum":1,"title":"Revision"},"response":{"type":"string","title":"Response"},"payload":{"additionalProperties":true,"type":"object","title":"Payload"},"source_turns":{"items":{"$ref":"#/components/schemas/ConversationEvidence"},"type":"array","title":"Source Turns"},"action_confirmation":{"anyOf":[{"$ref":"#/components/schemas/ActionConfirmationProof"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time","title":"Created At"},"workflow":{"type":"string","enum":["workforce","sales","recruiting"],"title":"Workflow"},"agent_id":{"type":"string","title":"Agent Id"},"agent_name":{"type":"string","title":"Agent Name"},"contact_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contact Name"},"use_case":{"type":"string","title":"Use Case"},"call_record_available":{"type":"boolean","title":"Call Record Available"},"qualification_coverage":{"anyOf":[{"$ref":"#/components/schemas/QualificationCoverage"},{"type":"null"}]},"fit_review":{"anyOf":[{"$ref":"#/components/schemas/QualificationReviewState"},{"type":"null"}]}},"type":"object","required":["id","call_id","contact_id","kind","subject_ref","revision","response","payload","created_at","workflow","agent_id","agent_name","use_case","call_record_available"],"title":"WorkflowOutcome"}
- WorkflowOutcomeCount: {"properties":{"workflow":{"type":"string","enum":["workforce","sales","recruiting"],"title":"Workflow"},"response":{"type":"string","title":"Response"},"count":{"type":"integer","minimum":0,"title":"Count"}},"type":"object","required":["workflow","response","count"],"title":"WorkflowOutcomeCount"}
- WorkflowOutcomeSummary: {"properties":{"window":{"$ref":"#/components/schemas/DashboardWindow"},"as_of":{"type":"string","format":"date-time","title":"As Of"},"counts":{"items":{"$ref":"#/components/schemas/WorkflowOutcomeCount"},"type":"array","title":"Counts"}},"type":"object","required":["window","as_of","counts"],"title":"WorkflowOutcomeSummary","description":"Latest caller response per call/subject as of end, saved within the window."}
- WorkspaceCapabilities: {"properties":{"generated_at":{"type":"string","format":"date-time","title":"Generated At"},"capabilities":{"items":{"$ref":"#/components/schemas/WorkspaceCapability"},"type":"array","maxItems":6,"minItems":6,"title":"Capabilities"}},"additionalProperties":false,"type":"object","required":["generated_at","capabilities"],"title":"WorkspaceCapabilities","description":"The complete, fresh capability map used by the customer portal."}
- WorkspaceCapability: {"properties":{"key":{"type":"string","enum":["browser_voice_test","text_chat_test","phone_test_call","voice_preview","calendar_booking","guided_setup"],"title":"Key"},"state":{"type":"string","enum":["ready","needs_setup","unavailable"],"title":"State"},"blockers":{"items":{"type":"string","enum":["account_agreements_required","agents_write_scope_required","calls_write_scope_required","runtime_not_configured","browser_runtime_not_configured","fleet_placement_not_configured","agent_chat_not_configured","assisted_setup_not_configured","voice_catalog_empty","verified_test_number_required","test_call_daily_cap_reached","telephony_account_required","telephony_account_unverified","telephony_worker_unavailable","calendar_integration_not_configured","calendar_connection_required","calendar_connection_reconnect_required"]},"type":"array","title":"Blockers"}},"additionalProperties":false,"type":"object","required":["key","state"],"title":"WorkspaceCapability","description":"One finite capability with the current reason it cannot be used, if any.\n\n``needs_setup`` names something the tenant or its developer can complete. ``unavailable``\nnames a deployment/service condition; the portal should explain it plainly and avoid\npresenting a setup CTA that cannot succeed. ``ready`` always carries no blockers."}
- WorkspaceImpact: {"properties":{"tenant_id":{"type":"string","title":"Tenant Id"},"tenant_name":{"type":"string","title":"Tenant Name"},"tenant_status":{"type":"string","title":"Tenant Status"},"mobile_membership_role":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mobile Membership Role"},"legacy_role":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Legacy Role"},"has_active_subscription":{"type":"boolean","title":"Has Active Subscription"},"is_last_active_owner":{"type":"boolean","title":"Is Last Active Owner"}},"type":"object","required":["tenant_id","tenant_name","tenant_status","mobile_membership_role","legacy_role","has_active_subscription","is_last_active_owner"],"title":"WorkspaceImpact"}
# Error registry
Every RFC 9457 `type` URI the API answers with resolves to https://docs.vocapable.com/errors/{code}.
## Transport, authentication, and tenancy
- `unauthenticated` (401): Absent, malformed, unrecognized, revoked, or expired API key. **One byte-identical body covers all five deliberately** - "revoked" and "expired" are facts about a key that was issued, so a per-reason message would answer whether a token ever existed here - and the same body answers the `/internal` s
- `insufficient_scope` (403): The key is valid but does not carry the scope this endpoint declares (`03-domain-model.md` §Authorization lists the twenty-five). Also raised by the key-management routes when a caller tries to *grant* a scope it does not hold, or to revoke or rotate a key broader than itself (§1).
- `tenant_suspended` (403): The credential is good; the tenant is not `active` - `suspended` or `closed`, the only other values `ck_tenants_status` allows (§4). A billing/compliance state, not a bad key, so it gets its own type.
- `mobile_rollout_disabled` (403): A valid native-device authority or browser PKCE request names a workspace that the deployment master or its explicit tenant mobile-pilot decision does not admit. The product remains fail-closed even if the account/membership itself is active.
- `mobile_rollout_unavailable` (503): The deployment master is enabled, but the narrow database rollout decision could not be read. The API never assumes a tenant is enabled when that dependency is unavailable.
- `forbidden` (403): The admin surface is not reachable from this address (IP allowlist, evaluated before authentication), and the generic 403 fallback.
- `not_found` (404): No route matches the path. Resource-level misses use the specific `*_not_found` codes below.
- `subscription_expansion_not_found` (404): The requested upgrade quote does not belong to this workspace.
- `subscription_expansion_conflict` (409): The upgrade needs commercial/source reconciliation, another purchase holds ownership, or the request key names a different target. Staff subscription-terms, commercial-status and subscription-override writes also return this when an unfinished purchase reserves the commercial source; neither the acc
- `subscription_expansion_unavailable` (503): Billing is unconfigured or the provider could not verify the upgrade price. An unsuccessful preview may be retried; it does not dispatch a payment.
- `method_not_allowed` (405): The path exists; the method does not.
- `conflict` (409): Generic conflict fallback for a framework-raised 409.
- `rate_limited` (429): Token bucket exhausted for this key. `Retry-After` and the `RateLimit-*` headers accompany it (§1).
- `request_failed` (as raised): Fallback for any other framework-raised HTTP error, so no response escapes without a code.
- `database_unavailable` (503): The API started without a database connection.
- `telephony_unavailable` (503): No live telephony provider is configured in this deployment.
- `internal_auth_unavailable` (503): The `/internal` service credential is not configured, so the voice runtime cannot be authenticated.
- `admin_auth_unavailable` (503): The staff plane is not configured in this deployment, so `/admin` cannot authenticate anyone. Since ADR-0017 the missing dependency is the admin database engine - staff identity needs no identity provider, because a staff session IS a first-party session. **Was 501 until 2026-08-09** (`adr/0015-besp
## Request shape: validation, filtering, pagination, idempotency
- `invalid_request` (422): Body or query failed contract validation. `errors[]` names each field and its reason - this is the only code that routinely carries many entries.
- `invalid_filter` (400): An unsupported field, operator, or value in a filter query param (§1 Filtering). `errors[]` names the offending field.
- `invalid_cursor` (400): The `cursor` was not a verbatim `next_cursor` from a previous response. Cursors are opaque; do not construct them.
- `idempotency_key_required` (400): The endpoint requires `Idempotency-Key` and none was sent - the two where a duplicate costs money or dials a human twice (§1).
- `idempotency_key_invalid` (400): The key exceeds the length ceiling. A UUID is the documented shape.
- `idempotency_key_reuse` (409): This key was already used for a *different* request - another endpoint, or a different body. A key identifies one request; send a new one.
- `idempotency_key_in_flight` (409): A request with this key is still running (or died between claiming the key and recording its result), so this one must not perform the side effect a second time. Retry in a moment.
## Agents, templates, knowledge packs, voice profiles
- `agent_not_found` (404): No agent definition of that id belongs to this tenant.
- `agent_version_not_found` (404): No published agent version of that id belongs to this tenant.
- `agent_use_case_immutable` (409): A published agent definition cannot change `use_case`. Calling purpose determines the consent floor for its immutable versions and any campaign already using them; create a new draft definition for a different purpose.
- `voicemail_message_uncomposable` (409): The agent carries a `voicemail_config` that composes no message (ADR-0023). A drop is a regulated artificial-voice call and is composed from the AI-disclosure opener, the configured body, and a platform opt-out closing; `errors[]` names the element that refuses. Also raised at campaign launch when t
- `voicemail_callback_unavailable` (409): The `callback_e164` a voicemail message would name is not an active, voice-capable number assigned to this tenant. The opt-out number a message reads aloud has to be one a callee can actually reach, so it is validated when the agent is saved rather than when the first message is left.
- `template_not_found` (404): No platform template of that id.
- `template_not_published` (409): The template exists but is `draft` or `deprecated`, so it cannot be instantiated.
- `eval_gate_failed` (409): Publish is blocked: no completed golden-transcript replay run has passed for this agent's **exact current draft hash and revision**, the queued/running/latest run did not pass, or the definition changed while publish acquired its lock. `errors[]` carries the `EvalRun` id when one exists (§2 Step 2).
- `conversation_release_qualification_required` (409): Sales, staffing or recruiting publication lacks three complete passing real-model repetitions for the exact current draft, languages, corpus and resolved provider/model/endpoint. Run the operator routing-chain check.
- `conversation_evidence_missing` (422): A conversation action lacks actual caller-turn evidence or cites a turn the trusted runtime did not supply. Capture the caller response before submitting the action.
- `action_confirmation_policy_invalid` (409): Publication requested an unsupported owner confirmation policy for this workflow. The policy is selected by the service, not a tenant draft field.
- `objective_scoring_invalid` (409): Publish (or a release-gate request) is blocked: one or more objectives carry a `scoring` block the survey scorer cannot read - unknown scale, mapping keys outside the scale's answer set, `ordinal` without a matching `points` count, a scored question with no `id` or no question text, or duplicate obj
- `compliance_profile_unresolved` (409): Publish is blocked: the agent names no compliance profile and the tenant has no default, so the version would pin nothing.
- `knowledge_pack_version_unresolved` (409): Publish is blocked: the named knowledge pack has no compiled version to pin (none `ready`, or the pinned number does not exist).
- `receptionist_intelligence_version_unresolved` (409): Publish is blocked: this receptionist owns a structured business-intelligence draft but no current immutable intelligence version. Review and publish the facts first; a live call may never follow the mutable draft.
- `receptionist_intelligence_not_found` (404): This receptionist has no structured intelligence document yet. Create one before adding source snapshots, previewing compact facts, or publishing an intelligence version.
- `receptionist_intelligence_requires_receptionist` (422): The requested agent is not a `receptionist` use case. Structured business intelligence is intentionally agent-owned and cannot become a second prompt system for another modality.
- `receptionist_intelligence_conflict` (409): A concurrent writer created or changed the receptionist's intelligence draft. Refresh the document and retry the edit.
- `receptionist_intelligence_compile_failed` (409): The approved structured facts cannot be rendered into the bounded spoken-knowledge bundle. Correct the referenced location or reduce the facts; source documents themselves are never injected into a live prompt.
- `receptionist_intelligence_source_limit` (422): The receptionist already has the bounded maximum number of immutable source snapshots. Consolidate or retire sources before importing another snapshot.
- `receptionist_intelligence_source_not_found` (404): The requested immutable receptionist source snapshot does not belong to this receptionist.
- `receptionist_intelligence_source_review_conflict` (409): The immutable source snapshot already has an approve/reject decision. Capture a new snapshot if the source or the decision needs to change.
- `receptionist_intelligence_token_budget_exceeded` (422): The approved spoken-knowledge bundle exceeds the profile's tokenizer-measured live budget. Distill the approved facts before publishing.
- `receptionist_intelligence_source_reference_unresolved` (422): An approved FAQ or business policy cites a source snapshot that does not belong to this receptionist. Review its citations before publishing the immutable intelligence version.
- `receptionist_intelligence_source_reference_not_approved` (422): A cited source snapshot is pending or rejected. Explicitly approve the snapshot after review, or remove its citation, before publishing the immutable intelligence version.
- `receptionist_intelligence_version_conflict` (409): A concurrent intelligence publish claimed the next version number. Refresh the draft and retry; no mutable facts were promoted by this request.
- `receptionist_intelligence_version_not_found` (404): The receptionist owns an intelligence document, but not the requested immutable version number.
- `receptionist_release_not_supported` (409): The release-readiness and rollback surface applies only to an inbound receptionist agent; use the normal agent-version controls for every other use case.
- `receptionist_release_input_changed` (409): A release gate's sealed prompt inputs no longer match the receptionist draft, pinned knowledge, or structured-intelligence version. Run a fresh exact preflight rather than publishing proof for a different request.
- `receptionist_release_qualification_required` (409): A receptionist publish has no passing exact live-model preflight or carrier-path canary for its sealed inputs. Complete both release gates before it can be promoted.
- `receptionist_release_not_qualified` (409): The selected immutable receptionist version has not been qualified for live calls (or is draining). The control plane refuses to arm an AI route, and carrier ingress uses its deterministic spoken fallback instead.
- `rollback_target_not_found` (409): No earlier published receptionist version with a still-passing release gate is available for restore. Publish and qualify a replacement instead.
- `rollback_target_not_prior` (409): A restore target must be an earlier immutable receptionist version; the active version is not its own rollback target.
- `receptionist_rollback_incompatible` (409): The chosen healthy version would make a current answering-policy location selector, runtime route, or direct-handoff target unsafe. Update the policy or choose another compatible version.
- `rollback_target_not_healthy` (409): The selected prior version no longer has a passing release-gate record, so it cannot be restored automatically.
- `receptionist_regression_requires_receptionist` (422): Customer-authored release regression cases belong only to a receptionist agent; change or create the agent with the `receptionist` use case first.
- `receptionist_regression_case_not_found` (404): No regression case of that id belongs to this receptionist.
- `receptionist_regression_case_version_not_found` (404): The named regression case has no immutable revision with that version number.
- `receptionist_regression_case_limit` (422): The receptionist has reached the bounded number of customer-authored regression cases. Archive or consolidate a case before adding another.
- `receptionist_regression_case_version_limit` (422): The regression case has reached its bounded append-only revision limit. Create a replacement case if its assertion needs further iteration.
- `receptionist_regression_case_name_conflict` (409): Another regression case for this receptionist already uses that name. Choose a distinct name or update the existing case.
- `receptionist_regression_case_version_conflict` (409): The case changed after the client read it. Refresh its current immutable revision and retry with that `expected_version`.
- `receptionist_regression_case_archived` (409): An archived regression case cannot be edited. It remains readable as evidence but is excluded from later release gates.
- `knowledge_pack_not_found` (404): No knowledge pack of that id belongs to this tenant.
- `knowledge_pack_version_not_found` (404): The pack exists; that version number does not.
- `knowledge_pack_token_cap_exceeded` (409): The compiled pack exceeds the hard token cap owned by `08-ai-quality.md`. One rule, one code: the hand-authored version endpoint and the document upload both raise exactly this.
- `knowledge_pack_version_conflict` (409): A concurrent create claimed the same version number. Safe to retry - the retry increments from the winner.
- `knowledge_pack_over_budget` (409): The upload compiles inside the hard cap but over the `budget_tokens` ceiling the caller asked for. Raise the budget (up to the cap) or distill the sources; `errors[]` reports each document's token cost.
- `knowledge_upload_unsupported_format` (415): The uploaded document is not one of the accepted formats (pdf, docx, csv, md, txt) - by declared format or by extension.
- `knowledge_upload_too_large` (413): A document (or the request's document set) exceeds the ingest size limits, before any parsing.
- `knowledge_upload_encrypted` (422): The document is password-protected. The platform never attempts to guess or strip a password; remove it and upload again.
- `knowledge_upload_corrupt` (422): The bytes are not a readable document of the declared format (invalid base64, a broken archive, non-UTF-8 text, a mislabelled file). `errors[]` names the file.
- `knowledge_upload_no_text` (422): The document parsed but carries no extractable text - an image-only scan is the usual cause, and OCR must run before it can become a pack. A refusal on purpose: letting it through would publish a pack silently missing exactly the facts the tenant uploaded.
- `knowledge_upload_extractor_unavailable` (501): This document format cannot be read on this deployment (no PDF extraction backend is installed). Upload the content as docx, csv, md, or txt instead.
- `knowledge_tokenizer_unavailable` (503): No tokenizer is configured for ingestion in this deployment, so no honest token count can be produced. Uploads are measured with the serving model's own tokenizer or refused - a pack is never admitted on an estimate.
- `voice_profile_not_found` (404): No voice profile of that id belongs to this tenant.
- `voice_profile_revoked` (409): A revoked profile can neither accept a new consent proof nor be re-approved; create a new profile and collect consent again.
- `voice_profile_not_approved` (409): An agent draft tried to pin a cloned voice whose consent review has not approved it. Self-serve upload makes pending profiles common; without this gate the failure would surface as the runtime's `unknown_voice` refusal at dial time. Pick a stock voice until the review completes.
- `voice_sample_invalid_audio` (422): `POST /v1/voice-profiles/{id}/sample-recording` (or consent upload): the file cannot be used - not decodable, wrong container, not 16-bit PCM mono WAV, under 6 s, over 90 s, or below 16 kHz. `detail` names the exact defect; nothing was stored.
- `voice_upload_too_large` (413): One voice upload exceeds the per-file cap (20 MB decoded). Nothing was stored.
- `voice_upload_storage_unavailable` (503): No voice-artifact storage is configured on this deployment, so sample/consent uploads are refused before any byte is accepted. The URI-string routes (`POST /v1/voice-profiles`, `…/consent-proof`) are unaffected. Also answers the admin stock-reference surface for the same absence.
- `stock_voice_not_found` (404): Admin console: no stock-voice catalog row has that id - neither in the deploy's seed YAML (`inference/tts/voices.yaml`) nor among admin-created voices (migration 0066's overlay).
- `stock_voice_reference_missing` (404/409): Admin console: no reference clip is stored for that voice. 404 auditioning it; 409 trying to flip a voice `available` before its reference exists - upload the WAV, let the voice node's sync stage it, then flip.
- `voice_audition_unavailable` (503): Admin console: the staff voice bench is not configured in this deployment. Synthesis lives on the voice runtime - the only process holding a `TtsProvider` - and this API proxies it, so both `VOCAPABLE_VOICE_RUNTIME_AUDITION_URL` and `VOCAPABLE_VOICE_RUNTIME_AUDITION_TOKEN` must be set. Empty means u
- `voice_audition_text_required` (422): Admin console: an audition was requested with no sentence to speak.
- `voice_audition_text_too_long` (422): Admin console: the audition text exceeded the 400-character ceiling. That bound is the abuse control a mutating route would have inherited from its reason string, and it bounds how much GPU work one console click commissions while live calls share the same synth pool.
- `voice_audition_unreachable` (502): Admin console: the voice runtime did not answer the audition request.
- `voice_audition_failed` (502): Admin console: the voice runtime refused the audition, or produced no audio for it.
- `stock_voice_roster_empty` (409): Admin console: the catalog edit would leave no available stock voice, and agents default onto the roster. Enable another voice before disabling this one.
- `stock_voice_not_deletable` (409): Admin console: `DELETE /admin/voices/{id}` refuses the effective default - elect another default first. Every other voice deletes: an unpinned `custom` row is erased outright (0067), everything else becomes `removed` (0069) - hidden everywhere, restorable from the console's Removed section, pinned v
- `voice_rights_attestation_stale` (409): `POST /v1/voice-profiles`: the echoed version/SHA-256 of the Voice Rights & Consent Attestation is not the one this deployment serves - the document changed between render and signature. Fetch `GET /v1/voice-rights-attestation` again and re-collect the signature.
- `voice_consent_proof_required` (409): Approval was attempted with no stored signed release from the voice owner (§4 review queue). The release is a precondition, never a follow-up.
- `voice_cloning_withdrawn` (410): Self-serve voice cloning is withdrawn (owner decision, 2026-08-18). The four write paths - `POST /v1/voice-profiles` and the three artifact uploads on a profile, `…/sample-recording`, `…/consent-recording`, `…/consent-proof` - answer this and nothing else; they stay mounted rather than being removed
## Contacts, lists, and imports
- `contact_not_found` (404): No contact of that id belongs to this tenant (soft-deleted rows are gone for this purpose).
- `contact_already_exists` (409): A live contact with that E.164 already exists for this tenant; numbers are unique per tenant among non-deleted rows.
- `contact_owner_not_found` (404): The requested `owner_user_id` is not a user in this tenant. A foreign user id is indistinguishable from an absent one, so assigning a contact can never create a cross-tenant owner link.
- `contact_phone_reconsent_required` (409): `PATCH /v1/contacts/{id}` refused a changed `phone_e164`. Consent proof is bound to one telephone identity and cannot move to another number; create a new contact with fresh evidence, then retire the old contact.
- `consent_captured_in_future` (422): `POST /v1/contacts` received a consent capture timestamp after the server's current time. Evidence must describe an event that already happened; correct the source record's timestamp and retry.
- `contact_list_not_found` (404): No contact list of that id belongs to this tenant.
- `contact_list_in_use` (409): `DELETE /v1/contact-lists/{id}` refused: a campaign still targets this list. A campaign's target is part of the record of what it dialed, so the list outlives the campaign; `detail` names the campaign. Delete the campaign first, or leave the list in place - its memberships cost nothing.
- `contact_note_not_found` (404): No note of that id belongs to that contact in this tenant. Scoped to the contact, not just the note: a note id that exists under a *different* contact is a miss here, so the pair in the path is the identity.
- `contact_call_not_found` (404): The optional `call_id` for a note does not name a call for that contact in this tenant. An absent call, a foreign call, and a different contact's call are deliberately indistinguishable.
- `import_job_not_found` (404): No import job of that id belongs to this tenant.
- `import_too_large` (413): The uploaded CSV exceeds the per-import size limit. Split the file.
- `import_mapping_invalid` (422): The column mapping does not fit the uploaded file. `errors[]` carries the mismatch against `column_mapping`.
- `contact_timezone_unresolved` (409): Dialability cannot be answered because the contact has no resolved timezone, and quiet hours are evaluated in the callee's local time. Fail-closed: no verdict is guessed.
- `jurisdiction_unresolved` (409): **No policy set governs this number at all**, so there is nothing to enforce and no window can be computed. `effective_policy` never falls back to a federal floor and never invents a window, because inventing permission on the input most likely to be missing was the original defect. **Corrected 2026
- `jurisdiction_residual_gate` (409): **A policy set governs this number and names its own hole.** Raised by `GET /v1/contacts/{id}/dialability` and by `POST /v1/calls` when the resolved policy carries a `residual_gate` nothing has cleared - a requirement the record cannot evaluate: a state registration, permit, bond or message filing (
## Campaigns, scrubbing, consent, and DNC
- `campaign_not_found` (404): No campaign of that id belongs to this tenant.
- `production_key_required` (403): Campaign creation, editing, launch, pause, resume, and cancellation, plus a BYO caller-ID/routing mutation, require a post-promotion `vcp_live_` key. A `vcp_test_` key is deliberately confined to mock test calls: it may stage a draft BYO account but cannot create/activate/pool/route its numbers, sel
- `compliance_profile_not_found` (404): The campaign named a compliance profile that does not belong to this tenant. `errors[]` names the field.
- `scrub_run_not_found` (404): No scrub run of that id belongs to this tenant.
- `telephony_account_required` (422): No `telephony_account_id` was supplied and the tenant has no default account to fall back to.
- `scrub_required` (409): Launch refused: the campaign has no scrub run covering its target, or the run has not finished.
- `scrub_expired` (409): Launch refused: the scrub run has gone stale past its `expires_at`. Re-run it.
- `scrub_unacknowledged` (409): Launch refused: the scrub report exists and is fresh but nobody has acknowledged it (§2 Step 4).
- `agent_not_published` (409): Launch, ad-hoc call, or test call refused: the agent has no published version to pin. Drafts are never DIALED. (Since migration 0065 the browser voice session no longer raises this - an unpublished agent mints a `browser_draft` session instead, because the workshop hearing its own draft dials no one
- `call_question_conflict` (409): `POST /v1/calls` refused: a per-call question reuses an id the pinned version's objectives already carry. Per-call questions are additive-only - overriding a gate-proven objective per call would bypass the publish gate - so there is no precedence rule, only this refusal. `errors[]` names each collid
- `call_questions_unsupported` (409): `POST /v1/calls` refused: the request carries `questions` but the pinned version does not grant `record_survey_answer`, so the answers would have no tool to record them - a silent no-op refused at launch rather than debugged after the call. Grant the tool and republish, or launch without questions.
- `draft_session_stale` (409): A `browser_draft` voice-session token was redeemed after the draft changed: the token seals the exact draft content hash minted against, redemption recomputes it, and a mismatch refuses rather than running text the operator did not authorize. Open a new session to hear the current draft.
- `callback_in_past` (409): A requested callback time is at or before now, so there is no future instant to schedule. `errors[]` names `callback_at`.
- `callback_too_far_ahead` (409): A requested callback time is beyond the booking horizon. `detail` names the horizon in days. Legacy callbacks use it as a `not_before` floor that calling rules may move later. Under `exact_action_v2`, a proposed time change is refused and requires fresh caller confirmation. Neither policy waives cal
- `callback_confirmation_conflict` (409): This call already has a durably confirmed callback. An equivalent request returns its original result; a changed time, confirming turn or other facts do not cancel or replace the existing attempt. Reconcile that attempt before promising a change.
- `callback_exact_time_unavailable` (409): Calling rules would move or dispose the caller-confirmed callback time. No new attempt was saved. Ask for a different explicit time and obtain fresh confirmation.
- `telephony_account_unverified` (409): Launch, ad-hoc call, or test call refused: the campaign's `TelephonyAccount` has not passed verification. An unverified account cannot dial (§2 Telephony accounts).
- `outbound_caller_id_required` (409): An ad-hoc live call's verified default BYO account has no active, tenant-assigned, carrier-confirmed voice number. The request cannot name a caller ID; configure one on the account and retry.
- `spend_cap_exceeded` (409): Launch refused: the tenant has spent its cap for the current period. The detail names the amount, the cap, the period start, and where the cap came from. Pacer governor #4 enforces the same fact per dial batch, and `POST /v1/calls` owes callers the identical check - there is no side door (`11-billin
- `spend_state_unknown` (409): Launch refused: the tenant has a spend cap and the period's ledger holds chargeable usage on a meter that reaches an invoice but that its `Plan` carries no `overage_rates` entry for, so period-to-date spend cannot be established. The detail names the meter, the plan, and the period start. Fail-close
- `payment_past_due` (409): A new campaign launch or ad-hoc live call is refused once a payment has been outstanding since day 7 of the delinquency ladder (`11-billing.md` §Dunning). **Campaigns already running finish normally** - a card failure never cuts a live conversation - and paying the outstanding invoice restores launc
- `managed_production_not_available` (409): A campaign launch or ad-hoc live call resolved to a `managed` (platform-owned) `TelephonyAccount`. Platform telephony carries paid test calls to verified numbers only until a written legal opinion clears managed production (CANON 120, `adr/0005`). Use a verified BYO subaccount. The dialability gate'
- `campaign_not_launchable` (409): The campaign's current status refuses a launch.
- `campaign_not_pausable` (409): Only a `running` campaign can be paused.
- `campaign_not_resumable` (409): Only a `paused` campaign can be resumed.
- `campaign_not_cancellable` (409): The campaign has already finished.
- `campaign_not_editable` (409): The campaign has finished; its configuration is now history.
- `voicemail_drop_policy_invalid` (409): The campaign's stored `voicemail_drop` document cannot be read as a policy, so launch cannot tell whether this program leaves messages. Refused rather than defaulted to "off": a hand-edited row must not silently disarm a gate (ADR-0023).
- `voicemail_callback_not_in_service` (409): The campaign elects a voicemail drop, but the callback number the composed message names is not an active voice number **on this campaign's own telephony account** with a receiving agent configured. Publish proved the tenant owns the number; this proves a callee who rings it reaches someone. It is n
- `agent_pin_immutable` (409): The campaign has already launched. Every call it placed carries the version pinned at launch, so the agent pin cannot be moved underneath that evidence.
- `campaign_target_immutable` (409): A campaign's target cannot be changed: the scrub run authorizing the launch covers one contact list. Targeting another means a new campaign.
- `scrub_run_already_acknowledged` (409): The run has already been acknowledged; acknowledgment records an actor and a moment and is not repeatable.
- `scrub_run_incomplete` (409): Acknowledgment refused: the run has not finished.
- `scrub_run_expired` (409): Acknowledgment refused: the run has gone stale.
- `scrub_totals_mismatch` (409): The acknowledged totals do not match the run's. The acknowledgment attests to what the customer saw, so a mismatch means they saw a different report.
- `dnc_scope_not_writable` (403): Only `internal`-scope DNC entries are tenant-writable; the federal and state datasets are platform reference data refreshed by ingestion.
- `dnc_expiry_not_supported` (422): An internal DNC entry may not carry `expires_at`. Recording one withdraws the consent behind the number and sets the contact's do-not-call flag, and neither is restorable when a suppression lapses - an expiry would destroy consent evidence permanently and then return the number to dialable. Internal
- `consent_record_not_found` (404): The revocation names a consent record that does not belong to this tenant.
- `consent_contact_mismatch` (409): The revocation names a different contact than the record it revokes.
- `consent_not_revocable` (409): The consent record is not `active`, so there is nothing to revoke.
- `disclosure_text_required` (422): A PEWC consent record requires the verbatim disclosure text shown to the consumer; PEWC without it is not PEWC.
- `contact_not_dialable` (409): The number is suppressed and this request would contact it anyway. Raised today by `POST /messages` on an internal-DNC hit, and specified in §1 and §3 as the refusal an ad-hoc `POST /calls` or a test call returns, with `blocked_reasons[]` in `errors[]`. **A refusal raised by a compliance-floor contr
## Calls, analytics, billing, and exports
- `call_not_found` (404): No call of that id belongs to this tenant.
- `receptionist_purchase_conflict` (409): A reviewed connection price or subscription source changed, a request key belongs to another purchase, or unfinished work requires reconciliation. Recover the saved purchase or review a fresh quote before confirming.
- `receptionist_purchase_unavailable` (503): New connection purchasing is disabled or billing could not verify the purchase. Existing purchase identities remain available for recovery.
- `receptionist_payment_method_required` (402): The verified uncovered connection amount is positive and the customer has no default payment method. Add one in Billing before confirming; no purchase is claimed or collected.
- `receptionist_purchase_not_found` (404): No saved receptionist connection purchase of that ID belongs to this tenant.
- `receptionist_purchase_pending` (503): Signed Stripe event recovery could not finish the original connection purchase. Ownership is retained and the event can be retried without creating another invoice or charge.
- `inbound_voicemail_not_found` (404): No inbound voicemail receipt of that id belongs to this tenant. It is intentionally distinct from `call_not_found`: a receipt exists before audio has been imported into a normal Call.
- `transcript_unavailable` (404): The call exists but has no transcript - it never reached conversation, or post-processing has not finalized.
- `recording_unavailable` (404): No safely playable recording exists for this call - the consent basis in force did not permit one, its retention window has closed, or its retained artifact is temporarily unavailable. **Nothing was lost**, and the code says so rather than leaving a generic 404 to read as a missing artifact (`07-com
- `recording_url_signing_unavailable` (501): The recording exists and is retained, but object-storage URL signing is not wired in this deployment, so the `302` cannot be issued. Export the call instead.
- `analytics_window_unbounded` (400): `start` and `end` are both required. A window is never defaulted: guessing one for a report that costs partition scans would hide the cost from the caller.
- `analytics_window_invalid` (400): `end` must be strictly after `start`; the window is half-open `[start, end)`.
- `analytics_window_too_wide` (400): The window exceeds the per-report ceiling (the partition ceiling, §2). For longer history, export `contact_attempts` and aggregate downstream.
- `analytics_grouping_too_granular` (400): The requested `group_by` would return more buckets than one report may emit. Narrow the window, drop a dimension, or filter to one campaign.
- `usage_window_too_wide` (400): The `occurred_at` window on the usage ledger exceeds the per-page ceiling. For bulk extraction, export `usage_events`.
- `subscription_not_found` (404): This tenant has no subscription.
- `subscription_inactive` (409): The subscription is not in a state that permits the request: on campaign launch, the mirrored Stripe subscription is `unpaid` or `canceled`, so new launches refuse immediately (`past_due` is different - it follows the documented seven-day dunning grace before `payment_past_due` blocks launches; exis
- `wallet_not_found` (404): This tenant has no wallet, so it has no budget to read or set. A wallet is created by provisioning when the sandbox fee lands; a tenant without one predates self-service checkout.
- `subscription_exists` (409): `POST /v1/subscription` on a tenant that already has one (`uq_subscriptions_tenant_id`). A plan change amends the existing subscription; it never opens a second. Also answered when a concurrent request won the constraint.
- `subscription_provisioning_pending` (409): A prior `POST /v1/subscription` has a durable operation but its first Stripe outcome is unknown. The API will not submit another order: wait for the signed `customer.subscription.created` webhook to reconcile it, then refresh billing. This refusal is what prevents a retry after Stripe's idempotency-
- `subscription_provisioning_reconciliation_required` (409): Durable local and Stripe evidence is incomplete or contradictory, so the operation is frozen for billing review. Do not retry with a different plan or assume a new order is safe; no automatic duplicate subscription is created.
- `plan_not_purchasable` (409): The published plan exists but carries no `stripe_price_id`, so nothing can bill through it. An operator wires the plan to a Stripe Price before it sells; no price is invented at checkout.
- `plan_metered_prices_unwired` (409): The plan's base-fee Price is wired but its **metered** Prices are not, so a subscription opened on it would invoice the base fee and never rate a single forwarded charge. Stripe puts usage on an invoice only through a subscription item whose Price carries `recurring.meter` (§11-billing §Rating), so
- `plan_catalog_unavailable` (503): The published plan catalog is not provisioned in this deployment (migration 0028 seeds it; 0055 reshapes it to the ADR-0022 card), so no subscription can resolve a plan.
- `extra_sessions_not_purchasable` (409): Extra concurrent sessions have no Stripe price in this deployment: `VOCAPABLE_STRIPE_EXTRA_SESSION_PRICE_ID` is unset, so the $200/mo add-on cannot bill. Raised by `POST /v1/subscription` with `extra_sessions > 0` and by `PUT /v1/subscription/extra-sessions` needing a Stripe change. An operator wire
- `extra_sessions_not_applicable` (409): The plan includes no concurrent outbound sessions, so the $200/mo add-on has nothing to extend: raised by `POST /v1/subscription` and `PUT /v1/subscription/extra-sessions` for a positive target on a plan whose `concurrency_limit` is 0 - the `receptionist` class today. Inbound answering capacity is b
- `extra_sessions_update_unconfirmed` (502): The capacity operation is saved, but its provider outcome is not fully confirmed. Retry the requested quantity to reconcile it from a fresh provider read; a signed subscription webhook can also complete it. A matching quantity alone does not prove which operation applied. Only the frozen request may
- `extra_sessions_update_failed` (502): The current provider subscription could not be safely verified before dispatch, or Stripe definitively refused the original session change. The detail distinguishes an unsent request from a provider refusal. Uncertain or malformed successful responses use `extra_sessions_update_unconfirmed` and pres
- `receptionist_not_purchasable` (409): The AI receptionist's two licensed Prices are not both wired in this deployment: `VOCAPABLE_STRIPE_RECEPTIONIST_BASE_PRICE_ID` (the first connection) or `VOCAPABLE_STRIPE_RECEPTIONIST_CONNECTION_PRICE_ID` (each additional one) is unset, so inbound answering connections cannot bill. Raised by `POST /
- `receptionist_connections_update_unconfirmed` (502): Stripe did not answer the subscription-item update, so whether the connection change applied is unknown. The local count is unchanged; the signed `customer.subscription.updated` webhook reconciles it. Refresh billing shortly rather than assuming the change failed - the request is safely retryable be
- `receptionist_connections_update_failed` (502): Stripe definitively refused the connection change. The local count is unchanged, the reserved receptionist lane admits exactly what it admitted before, and the detail carries Stripe's refusal. Also answered when the tenant's saved payment method cannot be **read** from Stripe before the prepaid mont
- `receptionist_connections_charge_declined` (402): The prepaid first month of the connections being added could not be collected, definitely: the tenant has no saved payment method on the Stripe customer, the instrument refused, or the PaymentIntent came back in a terminal non-success state. Raised by `PUT /v1/subscription/receptionist-connections`
- `receptionist_connections_charge_authentication_required` (402): The prepaid first month could not be collected because the card's issuer demands Strong Customer Authentication (3-D Secure and its equivalents): the instrument was **not** refused and there is nothing to replace, and no off-session request can perform the challenge. Raised by `PUT /v1/subscription/
- `receptionist_connections_charge_unconfirmed` (502): The prepaid month's payment may or may not have been taken: Stripe did not answer, answered a success the adapter cannot parse, answered about a charge whose customer/amount/currency this request does not recognize, or reported the PaymentIntent as `processing` - a delayed instrument such as an ACH
- `receptionist_connections_raced` (409): Another writer moved `subscriptions.receptionist_connections` while this request was deciding, so the compare-and-set in `set_receptionist_connections` refused and nothing local changed. Raised by `PUT /v1/subscription/receptionist-connections`. Losing this race is recoverable - refresh billing and
- `commercial_status_collection_method_unsafe` (409): Raised by `PATCH /admin/tenants/{id}/commercial-status` (action `tenant.commercial_status.set`) on a transition **to** `comped` while the tenant's Stripe subscription is still `charge_automatically` and/or its Stripe customer still carries a default payment method. Recording the intent before the co
- `collection_method_update_failed` (502): `PUT /admin/tenants/{id}/collection-method` saved the authorized change, but fresh Stripe reads do not yet prove completion. A provider write may already have applied. The response includes the saved operation ID and request key; retry the same target and key to recover that operation. Dispatch owne
- `collection_method_change_conflict` (409): Another capacity purchase, upgrade, or collection-method change owns this subscription; the request key names a different target; or the saved source no longer matches. Recover the existing operation before starting a new change. No new provider write is dispatched by the refused request.
- `credit_note_payment_pending` (409): Raised by `POST /admin/invoices/{id}/credit-note` (action `billing.credit_note.issue`) when Stripe refuses credit-note creation because a `payment_intent` is pending on the open invoice. Reachable exactly when a comped tenant was left on `charge_automatically`, which is what the code above exists to
- `credit_note_already_issued` (409): The same route against an invoice that already carries a credit note in the `credit_notes` mirror. The route is a **re-attempt, not an issuer** - the `invoice.finalized` webhook handler is the issuer - and Stripe permits credit notes summing to the invoice total, so an unguarded second press is repr
- `credit_note_not_comped` (422): The same route against an invoice whose tenant's `commercial_status` is not `comped` (nor an `internal`/`trial` arrangement that credits). A credit note against a paying tenant is a different accounting event - a service-fault credit, which is a `WalletEntry` under `11-billing.md` §Refunds, credits,
- `credit_note_issue_unconfirmed` (502): The same route when Stripe did not answer the credit-note creation - a transient or timeout class, not a definite refusal. Money may or may not have been credited and the local mirror has no row. Deliberately **not retried in-band**, on the same posture as `receptionist_connections_charge_unconfirme
- `prepaid_deposit_unsigned` (422): Raised by `POST /admin/tenants/{id}/prepaid-deposit` (action `billing.prepaid.deposit.record`) when the deposit's `order_acceptance_id` names no `agreement_acceptances` row belonging to this tenant - a deposit that would cite paper this counterparty never signed. The deposit's FK tests existence and
- `invoice_not_prepaid` (422): Raised by `POST /admin/invoices/{id}/settle-out-of-band` (action `billing.invoice.settle_out_of_band`) against an invoice whose tenant's **in-force** `subscription_terms.deal_shape` is not `prepaid`. The out-of-band settlement recognizes cash held as a deferred-revenue deposit, and it keys on the in
- `deferred_balance_insufficient` (409): The same route when the tenant's remaining deferred-revenue balance (`sum(deposits) - sum(settlements)`) does not cover the invoice's `amount_remaining` in full. Settlement is **all-or-nothing** (ADR-0035 d4): `paid_out_of_band` pays the whole invoice and cannot part-pay, so the ledger never recogni
- `settle_out_of_band_unconfirmed` (502): The same route when Stripe did not answer the invoice re-read or the `pay_invoice_out_of_band` call - a transient or timeout class, not a definite refusal. Whether the pay applied is unknown. The drawdown reservation is durable and the pay is idempotent on the invoice id (paying an already-paid invo
- `subscription_terms_overlap` (409): Raised by `POST /admin/tenants/{id}/subscription-terms` (action `billing.subscription.terms.write`) when the requested `valid_from` - or a back-dated correction - lands inside a validity range that already exists for this tenant. It is the exclusion constraint `EXCLUDE USING GIST (tenant_id WITH =,
- `subscription_terms_unsigned` (422): The same route with a `custom`, `comped`, or `prepaid` terms row carrying no `order_acceptance_id` (ADR-0035 d1 put `prepaid` in the acceptance-required branch - a prepayment is a negotiated commitment with a counterparty). It is the CHECK that makes a paperless deal of those shapes **unrepresentabl
- `subscription_terms_below_floor` (422): The same route with a `minute_rate_minor` below the $0.05/min publishable floor (CANON 124) on a row carrying neither an explicit owner approval reference (the `floor_approval_ref` column) nor a `pricing_memo_ref`. It is the custom-path floor CHECK - the second enforcement point beneath `rate_card._
- `related_party_consent_missing` (409): A `subscription_terms` row carrying `related_party = true` was written, signed or activated with no recorded reference to an executed Conflict Disclosure and Consent Letter. This is the related-party half of the no-custom-price-without-paper rule, and it fails closed for the same reason `pricing_mem
- `design_partner_conversion_notice_missing` (409): An operator tried to end a Comped Period - stop the Design Partner Credit and convert the tenant to the Conversion Price - and either no §6.4 sixty-day notice is recorded for that tenant or the recorded send date is fewer than sixty days before the requested Conversion Date. Under `legal/design-part
- `invoice_write_off_not_permitted` (409): `billing.invoice.write_off` invoked against an invoice whose tenant's `commercial_status` is `comped` or `internal`. `mark_uncollectible` books to **Bad debt**, which says cash was expected and the customer failed us; a comp says no cash was ever expected and the customer is in good standing. Coded
- `invoice_not_open_for_out_of_band_payment` (409): `billing.invoice.mark_paid_out_of_band` invoked against an invoice that is not `open` - draft, void, uncollectible, or already paid. **What an operator does:** read the invoice's current status. A draft has not finalized and cannot be paid; a void invoice is terminal and Stripe's own reference says
- `promotional_grant_no_metered_items` (409): `billing.promotional_credit.grant` requested for a subscription carrying no metered items for the grant to apply to. A Stripe billing credit grant reaches **metered subscription items only**, so it cannot touch the class base fees, the $200 extra session, the $250 first receptionist connection or th
- `portal_subscription_update_unavailable` (409): `POST /v1/billing/customer-portal` with `flow="subscription_update"` now directs all plan and connection changes to Vocapable Billing for quote review and confirmed purchase. A legacy dedicated editor configuration does not bypass purchase ownership. The request shape remains accepted, but no hosted
- `subscription_unmanaged` (409): `PUT /v1/subscription/extra-sessions` or `PUT /v1/subscription/receptionist-connections` on a subscription that carries no Stripe reference (a dev-seeded or externally provisioned row), so there is no subscription item to change. Same family as `stripe_subscription_missing`: the API refuses rather t
- `stripe_customer_missing` (409): The tenant has no Stripe customer to subscribe or to credit. It is recorded when the sandbox fee is paid; a tenant provisioned outside self-service checkout has none, and a second customer is never created for one that may already exist.
- `stripe_subscription_missing` (409): The tenant has a local subscription but it has no Stripe subscription id (for example, a dev-seeded or externally provisioned row). Stripe customer-portal cancellation and plan-change deep links refuse rather than guessing which Stripe subscription to mutate.
- `subscription_cancellation_review_required` (409): Hosted cancellation requires an identified Stripe subscription and effective `mtm` terms. Missing, custom or committed terms direct the customer to support for order/end-date review. This does not assert that an expired commitment renews; a recorded current conversion restores self-service access.
- `billing_portal_unavailable` (502 / 503): Stripe Billing Portal is unavailable. `503` means gateway/return-origin configuration is missing, the configuration is inactive or permits subscription editing, or required capabilities differ. Protected sessions also require explicit cancellation-disabled evidence and an explicit configuration ID;
- `stripe_subscription_failed` (502): Stripe definitively refused the subscription. The durable operation records that refusal but no local subscription is created; a later corrected request reuses the same operation and provider correlation rather than creating a second one.
- `stripe_subscription_incomplete` (502): Stripe answered with a subscription carrying no item billing period, which the pinned API version places on the items. Confirm the account's pinned version matches the adapter's - a locally guessed period would disagree with the invoice.
- `stripe_credit_failed` (502): Stripe refused the $49 sandbox credit (ADR-0008). The local claim rolls back with the request, so the credit stays redeemable - retry rather than granting it by hand.
- `stripe_read_failed` (503): Raised on `POST /stripe/webhook` only, and its audience is Stripe rather than a customer: an inbound invoice or subscription event could not be re-read from Stripe because Stripe did not answer. **The 503 is the point** - a non-2xx is how Stripe is asked to redeliver, and the alternative is acknowle
- `export_job_not_found` (404): No export job of that id belongs to this tenant.
- `invalid_export_columns` (422): One or more requested columns are not exportable for that resource; `errors[]` names each.
- `export_too_large` (422): The extraction would exceed the per-job row cap. **422, not 413** - the request entity is tiny; it is the *result* that will not fit. Split it with filters.
- `api_key_not_found` (404): No API key of that id belongs to this tenant.
- `key_mode_escalation` (403): `POST /v1/api-keys` with `mode: "live"` from a **test** key. A live key branches telephony onto the live provider, so minting one from a sandbox credential would walk around the sandbox/production boundary CANON 122 rests on - the mode a key may issue is the mode it holds, never a field the caller s
- `unknown_scope` (422): `POST /v1/api-keys` naming a scope outside the catalog (`03-domain-model.md` §Authorization). Refused rather than stored: a key minted with an unknown scope appears to grant something, grants nothing, and surfaces much later as a confusing 403. `errors[]` names the field.
- `voice_session_unavailable` (503): `POST /v1/agents/{id}/voice-session`: no browser voice-runtime endpoint is configured in this deployment (`VOCAPABLE_VOICE_RUNTIME_BROWSER_URL`), so there is no socket to connect to. The carrier-facing Media Streams endpoint is a **different network path** and never stands in for it - a session mint
- `voice_profile_missing` (409): **Promoted from §9.9 (2026-08-11):** the pinned agent version carries no voice, so there is nothing to speak with. Raised by `POST /v1/agents/{id}/voice-session` before any socket opens; §9.9 keeps the identical code for the runtime's own bootstrap. Answering it in HTTP is the point - the alternativ
- `disclosure_opener_missing` (409): **Promoted from §9.9 (2026-08-11):** the pinned agent version carries no AI-disclosure opener. A floor control (CANON 71), so its absence refuses the session rather than degrading it - on the browser transport exactly as on the carrier one (CANON 103).
- `llm_chain_unresolvable` (503): **Promoted from §9.9 (2026-08-11):** no link in the agent's LLM routing chain is reachable from this deployment, so the session would connect to a pipeline with no model. A deployment fault, not the caller's.
- `runtime_plane_unavailable` (503): **Promoted from §9.9 (2026-08-11):** the session-token keyring and LLM provider catalog are not wired, so no session token can be minted or redeemed. Same code, same meaning, now reachable by a customer request.
- `sandbox_session_in_progress` (409): `POST /v1/agents/{id}/voice-session`: this account already holds its allowed number of open in-browser sessions. `Retry-After` carries the session ceiling in seconds, which is when the one in progress frees at the latest. A concurrency fact about the caller, so 409 - nothing is broken and no quota i
- `sandbox_session_limit_reached` (429): The per-account in-browser session budget for the current window is spent. `errors[]` carries `limit`, `window` and `resets_at`, and `Retry-After` carries the wait, because waiting is the entire remedy.
- `sandbox_ip_limit_reached` (429): The same budget, counted per source address rather than per account - the ceiling that stops one network minting sessions through many accounts. Same body shape and same `Retry-After` as `sandbox_session_limit_reached`; a distinct code because the remedy differs (wait, versus wait *and* stop fanning
- `sandbox_capacity_unavailable` (503): No sandbox capacity right now. In-browser sessions run on a fixed pool deliberately smaller than the platform's live-call capacity, so a test never takes the capacity a live call needs. **503 and not 429**: nothing the caller did causes it and nothing they change fixes it. `Retry-After` is the sessi
- `sandbox_admission_unavailable` (503): The conversation-slot registry this deployment started with cannot hold lane reservations, so sandbox sessions have neither a per-account concurrency ceiling nor a platform one. Fail-closed, in the same family as `slot_registry_unavailable` (§9.9): admitting unbounded sessions is the state admission
- `agent_chat_unavailable` (503): `POST /v1/agents/{id}/chat`: no configured LLM provider can serve a chat turn - the provider catalog (`VOCAPABLE_LLM_PROVIDERS_JSON`) is absent, or it resolves none of the draft's route and carries no reachable default. A deployment fault, not the caller's.
- `assisted_setup_unavailable` (503): `POST /v1/agents/{id}/setup-chat`: `VOCAPABLE_ASSISTED_SETUP_PROVIDER` is unset, so the interviewer has no model to run on. Deliberately separate from `agent_chat_unavailable` - the draft-chat surface can be live while the setup assistant is not.
- `chat_daily_limit_reached` (429): The workspace's daily chat-turn budget is spent. `Retry-After` carries the wait; chat turns are budgeted-free, and the budget is the bound that keeps them free.
- `chat_ip_limit_reached` (429): The hourly per-address chat budget is spent - the ceiling that stops one network chatting through many keys. Same remedy shape as `chat_daily_limit_reached`.
- `chat_turn_limit_reached` (409): The posted conversation exceeds the draft's `guardrails.max_turns` (or the platform default). Start a new conversation; the server keeps no transcript to trim.
- `chat_guardrails_unenforceable` (422): The draft's `blocklist_patterns` cannot be compiled into the Layer-2 screen (an invalid or oversized regex), so a chat turn cannot be screened the way a call would be. Fix the draft's guardrails first - the same defect publish lint refuses.
- `chat_upstream_failed` (502): Every link in the resolved chain refused before streaming began. The chain is the draft's routed providers only - deliberately no hosted fallback: chat runs the platform's own model, reached from the control plane through the chat-plane base_url override (`VOCAPABLE_CHAT_PROVIDER_BASE_URL_OVERRIDES_
- `human_rep_not_found` (404): No human-rep destination of that id belongs to this tenant.
- `rep_group_not_found` (404): No human-rep group of that id belongs to this tenant.
- `rep_destination_in_use` (409): The tenant already configured that E.164 handoff destination. One destination has one owner so a future carrier callback cannot resolve an ambiguous target.
- `rep_group_name_in_use` (409): The tenant already configured a group with that name.
- `rep_user_not_found` (422): The optional linked portal user is not active in this tenant. A foreign user is never linkable to a handoff destination.
- `human_rep_not_available` (409): The rep is inactive or has transfers disabled, so it cannot publish a manual availability advertisement.
- `handoff_targets_unconfigured` (409): Publish refused because the draft grants `transfer_to_human` but has no immutable direct-rep target snapshot. Configure one or more active, transfer-enabled tenant reps first, then request a fresh release evaluation.
- `handoff_targets_unavailable` (409): Publish refused because at least one rep in the draft's frozen direct-rep target list is no longer active or transfer-enabled. Reconfigure the list, request a fresh release evaluation, then publish.
- `funding_amount_out_of_range` (422): `POST /v1/billing/prepaid/funding-intents` named an amount below or above this deployment's declared manual-funding range. No Stripe Checkout Session or wallet credit was created.
- `prepaid_calling_requires_active_subscription` (409): Manual prepaid credit is available only after this tenant has an active or trialing production subscription. It funds platform-managed, tenant-verified **test calls only**; it is neither sandbox credit nor BYO carrier credit.
- `funding_idempotency_conflict` (409): The supplied manual-funding idempotency key already names a different amount or currency. Retry the original request, or choose a new key for a new Checkout Session.
- `funding_intent_expired` (409): The hosted Checkout page for this local manual-funding intent expired. Start a new top-up with a fresh key; an expired browser page is never reopened. A signed paid completion can still settle its exact intent if Stripe delivers it after expiry.
- `checkout_failed` (502): Stripe refused creation of the hosted manual-funding Checkout Session. The local intent remains recoverable by its idempotency key, but no credit is settled until a signed completion arrives.
- `checkout_response_invalid` (502): Stripe answered Checkout creation without a usable hosted URL or with a customer that does not match the tenant's local Stripe customer. The browser is not redirected to an unrecorded payment page.
- `funding_intent_conflict` (409): A Stripe Checkout Session could not be durably attached to its local manual-funding intent. Retry the same key so Stripe idempotency can recover the one safe page; no credit was settled.
- `auto_fund_unavailable` (503): Automatic prepaid funding is feature-disabled, Stripe is not wired, or the deployment lacks a safe hosted return origin. A customer may still send `enabled:false`, which is a local mandate revocation and does not require Stripe.
- `auto_fund_amount_out_of_range` (422): The requested automatic amount or calendar-month cap falls outside the authoritative bounds returned by `GET /v1/billing/prepaid/auto-fund`; no SetupIntent page or PaymentIntent was opened.
- `auto_fund_requires_active_subscription` (409): Automatic prepaid funding is available only to an active or trialing production subscription. It does not make sandbox credit, BYO traffic, or production campaigns chargeable.
- `auto_fund_idempotency_conflict` (409): The supplied setup idempotency key already records different customer-approved threshold, amount, cap, or consent terms. Retry the original setup or use a fresh key for newly displayed terms.
- `auto_fund_setup_pending` (409): The previous setup operation cannot safely be reused or attached. The customer must refresh its authoritative state and, where instructed, begin a new hosted setup; no prepaid credit was settled.
- `auto_fund_setup_failed` (502): Stripe refused or returned an unusable hosted SetupIntent Checkout page. The configuration remains disabled and no off-session debit is attempted.
## Telephony accounts, phone numbers, and pools
- `telephony_account_not_found` (404): No telephony account of that id belongs to this tenant.
- `credentials_not_accepted` (422): `credentials` were supplied on a `managed` election. A managed account dials on the platform's own carrier relationship and carries no tenant credentials.
- `telephony_credentials_missing` (409): Verification was requested on an account with nothing stored to verify.
- `telephony_verification_unsupported` (503): The account's carrier has a registered adapter that can place calls but cannot answer the two onboarding reads - prove a credential pair, prove a number is the account's. Raised at `TelephonyResolver._build_provisioning_adapter`, which narrows a built adapter through `supports_provisioning` (all thr
- `telephony_account_not_active` (409): The named telephony account exists and is not `active` - it was suspended or removed from service - so no call may resolve a carrier through it.
- `telephony_credentials_rejected` (400): The carrier refused the stored credentials. 400 rather than 503: nothing about this deployment is missing, the secret itself is wrong or revoked, and the cure is re-supplying it.
- `telephony_credentials_unavailable` (503): The token vault that holds this account's carrier credentials is not configured in this deployment, so they cannot be read.
- `telephony_provider_unsupported` (503): The account names a carrier this build registers no adapter for. Raised by `adapter_factory_for` (`apps/control-api/src/vocapable_control/telephony_resolution.py`), the `ADAPTER_FACTORIES` lookup that **is** the dispatch since 2026-08-20 - it replaced the single `account.provider != TWILIO_PROVIDER`
- `telephony_provider_not_conformant` (503): The account names a carrier whose **adapter exists but whose conformance gate is not green on the current build** - offline Tiers 0–4 red in CI, or the Tier 5 live observation older than a quarter (`22-carrier-integration.md` §1.1). Split from `telephony_provider_unsupported` deliberately: *we have
- `telephony_capability_unsupported` (409): A tenant elected a lane the resolved carrier's adapter declares it cannot perform - principally the ADR-0023 voicemail-drop lane on an adapter declaring `amd` below the `message_end` rung. Raised at campaign launch and at the answering/originate election, **never mid-call**. The adapter refuses rath
- `telephony_verification_unavailable` (503): No verification transport is wired in this deployment, so a carrier account cannot be proven.
- `telephony_number_lookup_unavailable` (503): The carrier's number-inventory lookup is not reachable from this deployment, so ownership of the caller ID cannot be confirmed.
- `telephony_number_not_owned` (409): The caller ID is not in the carrier account's own inventory. A number the tenant does not hold at the carrier may not be presented, whatever this platform's records say.
- `telephony_vault_unavailable` (503): The token vault is not configured, so carrier credentials can neither be stored nor read on `/v1/telephony-accounts`.
- `telephony_resolver_unavailable` (503): No carrier resolution plane is composed in this deployment, so a telephony account cannot be turned into a live provider.
- `inbound_voice_webhook_not_ready` (409): This account is not a verified BYO Twilio account with vaulted credentials, so no inbound-Voice webhook URL is published for it. Managed and unverified accounts cannot become a voice ingress.
- `inbound_voice_callbacks_unavailable` (503): The signed inbound-Voice callback plane is not fully composed: it needs the credential vault, stable public base URL, session-token signing plane, LLM catalog, and Media Streams URL before a customer can copy a webhook URL.
- `inbound_voicemail_callbacks_unavailable` (503): The number cannot be switched to inbound voicemail because this deployment lacks its signed callback/import plane: credential vault, stable public origin, private recording store, and enabled importer. It neither publishes a speculative recording route nor accepts its callbacks.
- `phone_number_not_found` (404): No phone number of that id (or E.164) is visible to this tenant.
- `phone_number_exists` (409): That E.164 is already on the platform. Numbers are globally unique.
- `phone_number_not_assignable` (409): The number cannot be put into service: it is already in service, or it is being released and must rest first.
- `phone_number_not_assigned` (409): The operation needs a number in service under this tenant: releasing one that is not in service, or adding platform inventory to a pool.
- `inbound_voice_number_not_eligible` (409): A non-null inbound route requires an active, assigned, carrier-confirmed voice number on this tenant's verified BYO Twilio account. Clear the route with `inbound_agent_id: null` to remove stale configuration after release or verification loss.
- `inbound_voice_agent_not_ready` (409): The selected receiving agent does not belong to this tenant or has no published immutable version. Publish it before enabling inbound routing.
- `inbound_voicemail_policy_not_eligible` (409): The selected published receiving agent does not carry the one-party recording policy required for inbound voicemail. Choose an eligible agent or retain the live-agent route; an all-party disclosure cannot be converted into recording consent here.
- `answering_policy_not_found` (404): The number is visible to this tenant but carries no answering policy. Raised by `GET /v1/phone-numbers/{number_id}/answering-policy` and by `DELETE` of the same path - a delete that removed nothing says so rather than answering `204`, because "already gone" and "never configured" are otherwise indis
- `answering_policy_agent_not_ready` (409): `PUT /v1/phone-numbers/{number_id}/answering-policy` names an agent in an `ai` step that does not belong to this tenant or has no published immutable version. Publish it first. Proved once per distinct agent the document names, at write time rather than at answer time: a draft must not become callab
- `answering_policy_location_not_ready` (409): An `ai` step names a stable location reference that the agent's current immutable receptionist-intelligence version cannot resolve. Publish compatible business facts first, or remove the location selector; a raw address or prompt fragment is never accepted in an answering policy.
- `answering_policy_rep_not_ready` (409): `PUT /v1/phone-numbers/{number_id}/answering-policy` names a rep in a `ring_humans` step that is not an active, transfer-enabled rep of this tenant. **Every** named rep must resolve, not merely one of them: the step renders as an ordered `` over the reps in the order the document gives, so a s
- `receptionist_location_unavailable` (409): Carrier ingress found that a policy-sealed location selector no longer compiles against the exact intelligence version the selected agent pins. The policy engine records the refusal and advances to its next safe step rather than returning a Media Streams URL that would fail during bootstrap.
- `number_pool_not_found` (404): No number pool of that id belongs to this tenant.
- `number_pool_member_exists` (409): That number is already a member of this pool.
- `verified_number_not_found` (404): No verified-number registration of that id belongs to this tenant.
- `verified_number_exists` (409): The number already has a live (`pending` or `verified`) registration for this tenant. Re-registration restarts only an expired one; a revoked number registers as a fresh row.
- `verified_number_not_pending` (409): The check named a registration with no code awaiting confirmation - it is already `verified`, or `expired`/`revoked` and must start over.
- `verified_number_code_invalid` (409): The presented code does not match. Attempts are bounded; exhausting them expires the registration.
- `verified_number_code_expired` (409): The one-time code's window has closed; the registration is now `expired`. Re-register for a fresh code.
- `verified_number_attempts_exhausted` (409): Too many wrong codes; the registration is now `expired`. Re-register for a fresh code. The invalid/expired/exhausted split is deliberate - unlike an API-key refusal there is no existence oracle to protect (the caller already proved tenancy), and each names a different next step.
- `verified_number_delivery_unavailable` (503): No messaging provider is composed in this deployment to deliver the one-time code to the number, so registration refuses rather than minting a code nobody can receive. Delivery rides the platform Twilio account (ADR-0005) and needs both the vaulted platform credential and a sending number. Landlines
- `test_call_destination_not_verified` (409): The test call's `to` is not a currently `verified` VerifiedNumber of this tenant - `pending` is a registration, not proof, and `expired`/`revoked` no longer count. Register the number (`POST /v1/verified-numbers` → `POST /v1/verified-numbers/{id}/check`) first; a test call may only dial a number the
- `test_call_cap_exceeded` (409): The per-tenant daily Platform Test Call volume cap is spent (CANON 120): counted per UTC day over campaign-less non-mock attempts. Mock test calls (a `vcp_test_` key) never raise it. The detail names the count, the cap, and the UTC reset instant.
## Messaging, calendar, and webhooks
- `message_template_not_found` (404): No message template of that id belongs to this tenant.
- `message_template_exists` (409): A template of that name and locale already exists.
- `message_template_not_active` (409): The template exists but is not `active`, so it cannot be rendered into a send.
- `message_template_body_invalid` (422): The template body carries a malformed placeholder.
- `template_variables_invalid` (422): The supplied variables do not match the template's declared set. `errors[]` names each one `missing` or `not_declared`, and the detail lists what the template declares.
- `phone_number_not_sms_capable` (409): The sending number does not carry the `sms` capability.
- `sms_delivery_failed` (502): The carrier refused this specific message and retrying changes nothing - an unreachable landline, a handset that replied STOP, a number that cannot receive SMS. Nothing was delivered, and the caller must fix the destination rather than try again.
- `sms_delivery_unavailable` (503): The message could not be sent: the platform's vaulted messaging credential is missing or incomplete, or a carrier fault interrupted the send. An interrupted send is **not** retried here - the Messages endpoint takes no idempotency key, so a resend could deliver a second copy - so whether it went out
- `a2p_registration_missing` (409): A2P 10DLC registration is incomplete on the sending number's `TelephonyAccount`. Weeks of lead time, surfaced at onboarding rather than discovered here (`01-roadmap.md`).
- `webhook_endpoint_not_configured` (404): This tenant has no webhook endpoint yet (`PUT /v1/webhook-endpoint` creates it).
- `webhook_target_not_found` (404): No custom-webhook target of that id belongs to this tenant.
- `webhook_target_exists` (409): A target with that `webhook_id` is already registered. The id is the spoken name the model calls it by, so two targets may not answer to one; PATCH the existing target or choose another id.
- `webhook_target_name_reserved` (422): The `webhook_id` names a platform tool. A target may not shadow the registry (CANON facts 11 and 83); the same refusal the runtime makes at bind time.
- `webhook_target_schema_invalid` (422): The target's argument schema uses a construct the shared validator (`vocapable_contracts.toolschema`) does not enforce, or renders over the per-target prompt budget. Refused at registration because bind time would refuse it identically - a stored schema is always a bindable one. The detail names the
- `webhook_target_url_invalid` (422): The target URL is outside the delivery policy: not absolute https (plain http is allowed only for `localhost`/`127.0.0.1` outside production), carries userinfo, or - in production - is a private, link-local, loopback, or otherwise non-global IP literal. A registered target must not be a bridge into
- `calendar_integration_unavailable` (503): Direct Google Calendar OAuth is not configured in this deployment.
- `calendar_not_connected` (409): The tenant has no active calendar connection, or its stored grant is gone from the vault. Re-run the OAuth consent.
- `calendar_reconnect_required` (409): Google no longer accepts the stored grant or required calendar access. Reconnect the calendar; no availability or booking result was returned.
- `calendar_slot_unavailable` (409): The slot changed after availability was checked. No meeting or invite was created; check availability again.
- `booking_outside_bookable_hours` (409): `POST /v1/calendar/events` named a time the tenant's bookable-hours policy does not admit - outside its open weekday windows, inside its minimum lead time, or past its booking horizon (migration 0079). Enforced at booking and not only at `check_calendar`, because `book_meeting` takes an arbitrary `s
- `calendar_provider_unavailable` (503): Google timed out, rate-limited the request, or returned a 5xx. No meeting was created. Retry after the provider recovers.
- `calendar_provider_error` (502): Google refused or malformed this calendar request. No meeting was created; check the connection and request.
- `invalid_oauth_state` (400): The OAuth callback's `state` did not verify - forged, expired, or minted for a different provider family. The callback carries no API key, so the signed, tenant-bound, 10-minute `state` **is** its admission control (§7); a state that fails for any reason activates nothing.
- `oauth_consent_denied` (400): The customer's admin declined consent at the provider.
- `appointment_conflict` (409): That external calendar event is already recorded against a different call, so recording it again would double-book one booking across two calls.
## Agreements
- `checkout_unavailable` (503): This deployment has no Stripe configuration, so the sandbox fee cannot be collected. Answers on `POST /v1/onboarding/checkout`. Signup still collects and stores answers - the portal says so rather than rendering a button that fails at Stripe.
- `onboarding_incomplete` (409): `POST /v1/onboarding/activate` was called before the compliance screening admitted the applicant or before the plan step was saved. The wizard's earlier steps are the gate the retired sandbox fee used to be (ADR-0021); finish them and activate again.
- `validation_failed` (422): `PATCH /v1/onboarding` named a `step` and supplied no answers object of that name. Distinct from `invalid_request` (§9.2) because the body is individually well-formed - it is the cross-field agreement between `step` and its payload that failed, which no field-level schema can express.
- `onboarding_not_found` (404): The signed-in account has no signup application. `GET`/`PATCH /v1/onboarding` answer with it before the wizard has been started, which is what tells the portal to start one rather than resume.
- `tenant_not_provisioned` (409): `POST /v1/onboarding/api-key` was called by a signed-in human whose account carries no tenant yet - the fee is unpaid, or Stripe's `checkout.session.completed` has not landed. 409 rather than 404 because the cure is time, not a different request: retry once the receipt arrives.
- `api_key_already_issued` (409): `POST /v1/onboarding/api-key` has already handed this tenant its first key, and that secret is shown exactly once. The route is deliberately first-key-only - a general session-authenticated mint would duplicate `POST /v1/api-keys` under a browser cookie, which is a weaker credential than the API key
- `console_access_refused` (403): `POST /v1/auth/console-key` was called by a session whose membership or whose workspace is not active. Fails closed for a suspended workspace, a deactivated member, and any legacy session shape that cannot prove either. Deliberately not role-gated - a member is entitled to the console, and what they
- `production_key_owner_required` (403): `POST /v1/onboarding/production-api-key` is not being called by the active owner of an active workspace. The owner/session check runs before activation lookup, so a member does not learn whether the workspace is live.
- `production_request_owner_required` (403): `GET /v1/onboarding/production-readiness`, `POST /v1/onboarding/production-readiness/request`, or `POST /v1/onboarding/production-activation` is not being called by the active owner of an active workspace. The check runs before preflight reads, so another member cannot use this route as an oracle fo
- `production_not_ready` (409): The owner tried to activate or request review before every persisted preflight requirement was complete. `errors[]` names each unfinished requirement; complete it, then retry. This refusal does not change activation state.
- `production_already_live` (409): The owner requested review after the onboarding application was already `live`. No duplicate request is created; use the post-activation live-key handoff if a production credential is still needed.
- `production_application_moved` (409): `POST /v1/onboarding/production-activation` began a clean-pass self-serve activation, but the onboarding application left `paid` between the readiness read and the guarded transition (a concurrent activation or staff promotion). Nothing was changed; re-read state and retry.
- `production_no_compliance_profile` (409): Self-serve activation reached the scrub-waiver write but the tenant carries no `compliance_profiles` row to attest against (a corrupted tenant - provisioning writes exactly one and nothing deletes it). Activation is refused rather than completing into an unscreenable state; contact support.
- `production_not_activated` (409): The active owner has a provisioned workspace but production activation has not durably put that same onboarding application in `live` at the `production` step. No live secret was minted; complete the readiness path and retry.
- `production_api_key_already_issued` (409): This tenant's one-time post-activation live-key handoff already succeeded. The original secret cannot be recovered because only its hash is stored; use the live key already held by the workspace or rotate it through the normal API-key surface.
- `webhook_signature_invalid` (400): `POST /stripe/webhook` could not verify the `Stripe-Signature` header against the configured signing secret - unsigned, wrongly signed, or outside the 300-second replay tolerance. 400 rather than 401 because there is no credential to re-present. A deployment with no signing secret refuses every webh
- `workload_class_below_floor` (422): The plan selected at `PATCH /v1/onboarding` sits below the class the compliance screen established for this applicant. A class is a ceiling (CANON 123), so buying up is allowed and buying below the screen's verdict is not. The wizard disables those options; this is what makes it a control rather tha
- `onboarding_closed` (409): The compliance screening for this signup application returned `blocked` or `needs_review`, and the application is closed. Every write to `/v1/onboarding` answers with it thereafter - deliberately terminal, so a refused applicant cannot vary one answer at a time until something passes. Cure is a huma
- `agreements_required` (403): The tenant has not accepted the CURRENT version of every document needed by the surface. Account surfaces require MSA, Telecom Addendum, Platform Test Calls Addendum, AUP, and DPA; production surfaces additionally require the Production Services Order and scrub attestation; `POST /contacts` addition
- `agreement_version_mismatch` (409): The acceptance names a version or content SHA-256 that is not what `GET /v1/agreements` currently serves. An acceptance binds to the exact text presented (the jurisdiction-hash pattern); re-fetch and accept what is served.
- `agreement_draft_unacceptable` (503): The named document still carries the `DRAFT - FOR COUNSEL REVIEW` banner and this is production: no tenant may accept a draft (CANON 121). Dev/test deployments accept drafts so the flow stays testable. 503 because nothing the caller sends can succeed until the deployment's document set clears counse
- `device_authorization_owner_required` (403): `POST /v1/onboarding/device-authorizations/approve` lacks an email-verified active owner session for an active workspace. This check runs before the pairing lookup, so a member or stale owner cannot use a code to learn about another device request.
- `device_authorization_unavailable` (503): Device bootstrap cannot safely construct the configured browser approval URL, or its narrow pairing store could not create a usable request. Use ordinary browser onboarding or correct the deployment configuration; no partially returned credential is valid.
- `device_authorization_invalid` (400): The submitted human code is malformed, absent, expired, consumed, or bound to a different owner/workspace. Those cases intentionally share one response so the approval endpoint is not a pairing-code oracle.
- `device_authorization_grant_refused` (400): The device code or route-limited grant is malformed, unknown, expired, unapproved, or already spent. Start a new pairing; this response deliberately does not reveal which fact failed.
- `device_first_key_unavailable` (409): Device bootstrap can claim only the never-delivered provisioning `Default` key. If a browser/device handoff already delivered it, this route never mints another key; a customer who lost every delivered key needs staff-assisted recovery.
## First-party accounts and browser sessions (ADR-0017)
- `invalid_credentials` (401): Sign-in did not succeed. Unknown email, wrong password, an account with no usable password, and a session cookie carrying no synchronizer are one indistinguishable refusal on purpose (see above).
- `google_signin_unavailable` (503): `POST /v1/auth/google` in a deployment with no `VOCAPABLE_GOOGLE_SIGNIN_CLIENT_ID` configured. The surface is unavailable, not open - the portal discovers this through `GET /v1/auth/providers` and shows no button.
- `google_credential_invalid` (401): The Google ID token did not verify - expired, wrong audience, bad signature, and unknown issuer are one indistinguishable refusal on purpose; the reason goes to the security log.
- `google_email_unverified` (403): The Google account's own email is unverified, so it cannot key a Vocapable account. Cure is verifying the address with Google, not a different request.
- `email_verification_required` (403): The account exists and authenticated, but has not proved its email address, which onboarding requires before it can be started or paid for. Cure is the verification link, or `POST /v1/auth/email-verifications` for another one - not a different request.
- `verification_link_invalid` (400): The email-verification token is unknown, already consumed, or past its window. 400 rather than 401 because there is no credential to re-present: request a new link.
- `password_reset_link_invalid` (400): The password-reset token is unknown, already consumed, or past its window. Same shape and same reason as `verification_link_invalid`, and a distinct code because the cure is a different flow.
- `csrf_token_refused` (403): An unsafe request carrying the session cookie presented no `X-CSRF-Token`, or one that is not this session's synchronizer. Raised on `/admin` too since ADR-0017 decision 5, where it is declared on the mount and covers every console mutation. The token is served by `GET /v1/auth/session` and rotates
- `csrf_origin_refused` (403): An unsafe request carrying the session cookie declared an `Origin`/`Referer` that is not a configured portal origin. Checked **before** the synchronizer so a cross-site attempt is refused without the response revealing whether the token would have matched.
- `dashboard_auth_unavailable` (503): This deployment cannot verify browser sessions - no account-security secret, or no session verifier composed. API-key access is unaffected, which is why this is not `unauthenticated`: the caller's credential was never examined.
- `auth_email_unavailable` (503): No email provider, template set, or portal base URL is configured, so a verification or reset link cannot be minted or delivered. The account state is unchanged; nothing was half-issued.
- `auth_rate_limiter_unavailable` (503): The limiter that bounds sign-in, verification, and reset attempts is not composed, so those endpoints refuse rather than run unbounded. Fail closed: an unprotected password surface is worse than an unavailable one.
## Account deletion
- `account_deletion_unavailable` (409): The session's platform account is missing or not `active`, so no account request can be resolved for it. Sign in again from a current session.
- `account_deletion_email_confirmation_required` (409): `POST /v1/account-deletion/requests` was called on an account that signs in without a password, so a password cannot be the proof. Send a one-time confirmation link (`POST …/email-confirmations`) instead.
- `account_deletion_password_confirmation_required` (409): `POST /v1/account-deletion/email-confirmations` was called on an account that has a password, so the email link is not its proof. Re-enter the password on the account-deletion page.
- `account_deletion_reauthentication_failed` (403): The password supplied to confirm the request did not verify. Enter the current Vocapable password and try again.
- `account_deletion_request_settled` (409): This account-deletion request already reached a terminal outcome, so it cannot be re-confirmed. Open the status page to review the recorded outcome.
- `account_deletion_confirmation_invalid` (400): The one-time email confirmation token is unknown, already spent, or expired (or its account is no longer active). 400 because there is no credential to re-present: request a new confirmation email.
- `account_deletion_email_unavailable` (503): The confirmation email cannot be minted or sent - no portal base URL, no email provider, or the send failed. No request was confirmed; retry later or contact support with the account email.
## Mobile companion app (receptionist)
- `answering_policy_closed_steps_required` (422): A mobile schedule edit gave a number's answering policy business hours or holiday dates, but the policy carries no closed-step route to answer with when closed. Add a closed-step route before scheduling hours.
- `answering_policy_version_conflict` (409): The answering policy changed after this mobile schedule edit was based on it (optimistic-concurrency mismatch on the policy version). Refresh the current policy and reapply.
- `appointment_not_found` (404): No appointment of that id belongs to this tenant.
- `cellular_handoff_unavailable` (409): Emergency forwarding remains unavailable until the cellular-handoff feature and a verified destination are in place.
- `emergency_forward_destination_invalid` (422): The named human rep is not an active, transfer-enabled destination in this tenant, so it cannot be an emergency forward target. Choose an active, transfer-enabled rep.
- `emergency_override_expiry_invalid` (422): The emergency-override expiry must be in the future and no more than 24 hours from now.
- `emergency_override_version_conflict` (409): The emergency override changed before it could be cleared - the confirmation was for an earlier route. Refresh the current emergency route and retry.
- `handoff_booking_in_progress` (409): A calendar booking for this handoff is already running. Wait a moment, then reopen the handoff to see the booking receipt.
- `handoff_booking_not_found` (404): No handoff booking is available for this handoff and mobile role.
- `handoff_request_not_found` (404): No transfer (handoff) request of that id is available to this tenant or mobile role.
- `idempotency_key_conflict` (409): The mobile `Idempotency-Key` was already used for a different task action. Generate a new key before submitting a different update. Distinct from the `/v1` idempotency guard's codes (§9.2).
- `mobile_auth_rate_limiter_unavailable` (503): The limiter that bounds mobile sign-in is not composed, so mobile auth refuses rather than run unbounded. Fail closed.
- `mobile_auth_unavailable` (503): Mobile sign-in is not configured in this deployment - no mobile access-token signer, or no safe claimed-HTTPS/iOS-compatibility return URI.
- `mobile_availability_window_invalid` (422): The availability duration must be in the future and no more than 12 hours from now.
- `mobile_billing_budget_unavailable` (409): This workspace has no safely editable billable wallet, or its existing guardrail uses a legacy period the app must not relabel. Use the web portal or contact support.
- `mobile_billing_budget_version_conflict` (409): The spend guardrail changed after the app loaded it. Reload the live budget, then start a new browser confirmation for the new revision.
- `mobile_call_ai_agent_unavailable` (409): The agent named for a mobile AI call is not active or has no current published version. Activate and publish it first.
- `mobile_call_ai_contact_search_too_short` (422): The Mobile Call AI contact search needs at least two characters; a phone number or email address is not a searchable label.
- `mobile_call_ai_transport_invalid` (503): Mobile Call AI could not start on the verified business carrier route. Retry once that route is available.
- `mobile_capability_forbidden` (403): The signed-in mobile role does not carry the mobile capability this operation declares. The `detail` names the required capability.
- `mobile_feature_disabled` (403): The requested mobile feature is not enabled for this workspace's staged rollout. The browser portal remains the safe fallback.
- `mobile_handoff_booking_unavailable` (409): This handoff cannot book a calendar event now: it needs an active carrier-confirmed handoff, a linked live contact, and a connected calendar.
- `mobile_invitation_delivery_failed` (503): The team-invitation email could not be sent; no invitation was issued. Check email delivery and retry.
- `mobile_invitation_delivery_unavailable` (503): Team-invitation delivery is unavailable - this deployment has no safe portal URL and transactional email sender.
- `mobile_invitation_expiry_invalid` (422): The invitation expiry must be in the future and no more than 30 days away.
- `mobile_member_not_found` (404): No editable team member of that id belongs to this workspace.
- `mobile_metrics_window_invalid` (400): The receptionist-metrics `end` must be strictly after `start`; the window is half-open [start, end).
- `mobile_metrics_window_too_wide` (400): The requested receptionist-metrics window exceeds the maximum analytics span. Bound it to at most the allowed number of days.
- `mobile_metrics_window_unbounded` (400): Receptionist metrics require a bounded window: provide both `start` and `end`.
- `mobile_redirect_uri_refused` (400): The mobile return URI is not the exact claimed HTTPS or fixed iOS compatibility URI registered for this deployment.
- `mobile_rep_link_invalid` (422): The selected transfer representative is not an existing rep owned by this workspace and unlinked elsewhere.
- `mobile_rep_link_required` (403): This mobile membership is not linked to a transfer rep. Ask a workspace administrator to link it to a human rep.
- `mobile_self_membership_change_refused` (409): An admin cannot suspend, revoke, or change their own role from the mobile app. Use another active admin for the change.
- `mobile_step_up_callback_refused` (400): The mobile step-up return address is not registered. Return to the app and start the confirmation again.
- `mobile_step_up_email_confirmation_invalid` (400): The step-up email confirmation link is invalid or expired. Request another confirmation email.
- `mobile_step_up_email_proof_required` (409): This account has no password, so the sensitive change must be confirmed from its verified email. Use the email confirmation option.
- `mobile_step_up_email_unavailable` (503): The step-up email confirmation cannot be minted or sent in this deployment. Retry later or use an account with a current password.
- `mobile_step_up_grant_refused` (403): The browser step-up confirmation can no longer be used. Return to the mobile app and confirm the change again.
- `mobile_step_up_native_context_refused` (401): This mobile session can no longer start a confirmation. Sign in again to continue in the app.
- `mobile_step_up_password_proof_required` (409): This account has a password, so the sensitive change must be confirmed by re-entering it - not the email option.
- `mobile_step_up_payload_invalid` (422): The protected step-up command is malformed. Return to the app and review the change before confirming it.
- `mobile_step_up_portal_unavailable` (503): Browser step-up confirmation is not configured in this deployment. Retry later or use the web portal.
- `mobile_step_up_reauthentication_failed` (403): The step-up password could not be confirmed. Enter the current Vocapable password and try again.
- `mobile_step_up_request_unavailable` (404): This mobile step-up confirmation is no longer available. Return to the app and start the change again.
- `mobile_step_up_required` (403): A fresh browser step-up confirmation is required for this sensitive change. Confirm it in the browser, then return to the mobile app.
- `mobile_token_refused` (400): The mobile authorization (native token exchange) is not valid. Return to the app and start sign-in again.
- `mobile_transfer_destination_conflict` (409): That transfer-destination phone number is already assigned to another representative. Choose a different number.
- `mobile_transfer_destination_delivery_failed` (503): The transfer-destination verification code could not be sent; no change is pending. Check SMS delivery and retry.
- `mobile_transfer_destination_delivery_unavailable` (503): Transfer-destination verification delivery is unavailable - this deployment has no platform SMS sender.
- `mobile_transfer_destination_not_pending` (409): This transfer-destination verification is not pending. Start a new verification before submitting another code.
- `mobile_transfer_destination_rep_link_required` (409): Link the active team member to a human transfer representative before setting a transfer destination for it.
- `mobile_transfer_destination_unavailable` (409): Availability cannot be enabled: no active transfer destination is linked to this team member. Have an administrator link one first.
- `mobile_transfer_destination_verification_not_found` (404): No transfer-destination verification exists for this team member. Start a new one.
- `mobile_transfer_destination_verification_replaced` (409): This verification code was superseded by a newer one. Request and confirm the newest code for this destination.
- `mobile_unauthenticated` (401): The mobile session is invalid or expired. Sign in again to continue in Vocapable Mobile.
- `mobile_workspace_selection_refused` (403): The chosen workspace is not one of the active workspaces available to the signed-in account.
- `mobile_workspace_unavailable` (409): No active mobile workspace is available to this account. Ask a workspace admin to invite it or restore its membership.
- `operations_task_assignee_invalid` (422): The chosen assignee is not an active member of this workspace. Choose an active member or clear the assignment.
- `operations_task_not_found` (404): No operations task of that id is available to this mobile role.
- `operations_task_state_conflict` (409): That task action is no longer available in the task's current state. Refresh the task to see its current state.
- `operations_task_version_conflict` (409): The task changed before this update (optimistic-concurrency version mismatch). Refresh the task and apply the change again.
- `mobile_change_feed_cursor_invalid` (400): The change-feed position (cursor) is malformed or no longer valid. Restart the feed from the beginning, or from the last checkpoint you still hold.
- `mobile_coach_telemetry_role_invalid` (422): The Coach telemetry role does not match the role currently granted to this mobile workspace session. Send the role this session actually holds.
- `mobile_notification_test_unavailable` (409): This device is not ready for a test notification - it has no registered push token or is not fully enrolled. Complete device enrollment, then retry.
- `mobile_receptionist_not_published` (409): The receptionist has not been published yet, so it cannot be test-called from the Studio. Publish it first.
- `mobile_receptionist_studio_number_not_found` (404): No active receptionist line with that identifier belongs to this workspace.
- `mobile_receptionist_studio_intelligence_agent_invalid` (422): The line-owned target is not a receptionist belonging to this workspace. Choose a receptionist target in the web control center first.
- `mobile_receptionist_studio_intelligence_agent_unavailable` (409): This line has no unambiguous, line-owned receptionist intelligence target, or the requested target is not part of this line's policy. Resolve the target in the web control center; Mobile Studio never imports facts into an unrelated receptionist.
- `mobile_receptionist_studio_status_expiry_invalid` (422): The temporary-status expiry must be in the future and no more than seven days from now.
- `mobile_receptionist_studio_version_conflict` (409): The Receptionist Studio record changed elsewhere (optimistic-concurrency version mismatch). Refresh the receptionist details, compare the changes, and try again.
## Caller requests and the inbox (ADR-0037)
- `caller_request_not_found` (404): No caller request of that id belongs to this tenant.
- `qualification_not_found` (404): No sales or recruiting qualification response of that id belongs to this tenant.
- `qualification_review_conflict` (409): Caller answers or the team review changed. Refresh the current response and review before submitting a new decision. A retry of an already saved idempotent request returns its original receipt.
- `qualification_review_unavailable` (409): The source answers or pinned company question set cannot be verified. Review the source conversation and configuration; no fit decision was saved.
- `contact_correction_not_found` (404): No proposed caller identity correction of that id belongs to this tenant.
- `contact_correction_conflict` (409): The contact changed, a newer caller statement superseded the proposal, or it already has a final decision. Refresh before deciding. An idempotent retry returns its original saved decision.
- `contact_correction_unavailable` (409): The live contact, allowed identity field, or source evidence cannot support a correction review. No contact change or review was saved.
- `caller_request_version_conflict` (409): The request changed since it was read (optimistic-concurrency version mismatch). The detail names the current version; re-read the request and retry with it. Distinct from `caller_request_invalid_transition` so a portal can tell "refresh and retry" from "someone already resolved this".
- `caller_request_invalid_transition` (409): The requested move is not one the state machine allows: acknowledge applies to `open` requests only, and `resolved` is terminal for every edit, assignment included (no reopen in v1 - ADR-0037 records the deferral).
- `caller_request_assignee_not_found` (404): The named `assignee_user_id` is not a user of this tenant. The `users.id` FK is global, so the tenant-scoped read is the control; pick an assignee from `GET /v1/users`.