Voice ai
Generated from
DialerDigital/corecommit6e9f026f0a72— specopenapi/dialer-v1.yamlv0.36.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.
AI voice agents: builder + versioned policy, live conversations, transcripts/outcomes, supervisor takeover, the per-tenant AI-dialing kill-switch and today's metrics.
Endpoints
| Method | Path | Summary |
|---|---|---|
GET | /v1/ai/agents | List AI voice agents |
POST | /v1/ai/agents | Create an AI agent (status draft) |
GET | /v1/ai/agents/{id} | Fetch an AI agent |
PATCH | /v1/ai/agents/{id} | Update an AI agent (bumps policy_version) |
DELETE | /v1/ai/agents/{id} | Retire an AI agent (never deletes) |
POST | /v1/ai/agents/{id}/test | Sandbox conversation (no live call) |
GET | /v1/ai/conversations | List AI conversations (live feed or history) |
GET | /v1/ai/conversations/{id} | Fetch a conversation with transcript and outcome |
POST | /v1/ai/conversations/{id}/takeover | Supervisor takeover of a live AI conversation |
POST | /v1/ai/pause | Kill-switch — pause all AI dialing (tenant-wide) |
POST | /v1/ai/resume | Kill-switch — resume AI dialing (tenant-wide) |
GET | /v1/ai/status | AI-dialing kill-switch state |
GET | /v1/ai/metrics/today | Today's AI operating metrics (UTC day) |
GET | /v1/ai/eligibility | Per-campaign artificial-voice eligibility |
List AI voice agents
GET /v1/ai/agentsResponses
200
All AI agents of the tenant.
| Field | Type | Required | Description |
|---|---|---|---|
ai_agents | array of AiAgent | yes | |
ai_agents[].id | string (uuid) | yes | |
ai_agents[].name | string | yes | |
ai_agents[].status | draft · live · paused · retired | yes | |
ai_agents[].voice | object (AiVoice) | no | |
ai_agents[].voice.language | string | yes | BCP-47 (e.g. en-US). |
ai_agents[].voice.voice_id | string | yes | Provider voice identifier. |
ai_agents[].voice.pace | number | yes | 0 = slow … 1 = fast (0.5 ≈ natural). |
ai_agents[].disclosure | object (AiDisclosure) | no | |
ai_agents[].disclosure.text | string | yes | The opening artificial/prerecorded-voice disclosure (content-validated, EN/ES). |
ai_agents[].disclosure.mandated | true | yes | Always true — the disclosure cannot be disabled. |
ai_agents[].disclosure.locked | boolean | no | True once the agent has been live; text is then immutable. |
ai_agents[].policy | object (AiPolicy) | no | Negotiation guardrails the AI is bounded by. |
ai_agents[].policy.max_settlement_pct | integer | no | Max settlement as a percent of balance. |
ai_agents[].policy.min_payment_cents | integer | no | |
ai_agents[].policy.max_plan_months | integer | no | |
ai_agents[].policy.take_payments | none · card_on_file | no | |
ai_agents[].policy.prohibited | array of string | no | Disallowed conduct/topics (free-form tags). |
ai_agents[].handoff_rules | array of AiHandoffRule | no | |
ai_agents[].handoff_rules[].id | string (uuid) | null | no | |
ai_agents[].handoff_rules[].trigger | keywords · negative_sentiment · payment_over · human_requested | yes | |
ai_agents[].handoff_rules[].params | object | no | Trigger config, e.g. {keywords:[...]}, {seconds:20}, {cents:50000}. |
ai_agents[].handoff_rules[].action | transfer · offer_human · confirm_human | yes | |
ai_agents[].handoff_rules[].severity | hard · soft | yes | |
ai_agents[].handoff_rules[].locked | boolean | no | human_requested is ALWAYS locked (never removable/editable). |
ai_agents[].policy_version | integer | yes | Monotonic; increments on every versioned change. Transcripts cite it. |
ai_agents[].created_at | string (date-time) | no | |
ai_agents[].updated_at | string (date-time) | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}Create an AI agent (status draft)
POST /v1/ai/agentsCreates a draft agent at policy_version: 1. The opening disclosure.text is mandated, content-validated (must self-identify the voice as automated, EN/ES) and becomes IMMUTABLE the first time the agent transitions to live. A human_requested handoff rule is always present and locked. Duplicate name → 409. Management scope: a ddw_ browser token is 403.
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | no | |
status | draft · live · paused · retired | no | |
voice | object (AiVoice) | no | |
voice.language | string | yes | BCP-47 (e.g. en-US). |
voice.voice_id | string | yes | Provider voice identifier. |
voice.pace | number | yes | 0 = slow … 1 = fast (0.5 ≈ natural). |
disclosure | object | no | |
disclosure.text | string | no | |
policy | object (AiPolicy) | no | Negotiation guardrails the AI is bounded by. |
policy.max_settlement_pct | integer | no | Max settlement as a percent of balance. |
policy.min_payment_cents | integer | no | |
policy.max_plan_months | integer | no | |
policy.take_payments | none · card_on_file | no | |
policy.prohibited | array of string | no | Disallowed conduct/topics (free-form tags). |
handoff_rules | array of AiHandoffRule | no | |
handoff_rules[].id | string (uuid) | null | no | |
handoff_rules[].trigger | keywords · negative_sentiment · payment_over · human_requested | yes | |
handoff_rules[].params | object | no | Trigger config, e.g. {keywords:[...]}, {seconds:20}, {cents:50000}. |
handoff_rules[].action | transfer · offer_human · confirm_human | yes | |
handoff_rules[].severity | hard · soft | yes | |
handoff_rules[].locked | boolean | no | human_requested is ALWAYS locked (never removable/editable). |
{
"name": "Ava — Collections Assistant",
"voice": {
"language": "en-US",
"voice_id": "neural-3-female",
"pace": 0.5
},
"disclosure": {
"text": "This call is from Ava, an automated assistant calling on behalf of Brightpath Recovery Group regarding an account. This call is recorded."
},
"policy": {
"max_settlement_pct": 70,
"min_payment_cents": 5000,
"max_plan_months": 12,
"take_payments": "card_on_file",
"prohibited": [
"legal threats",
"third-party disclosure",
"other debts"
]
},
"handoff_rules": [
{
"trigger": "keywords",
"params": {
"keywords": [
"attorney",
"lawyer",
"dispute",
"cease",
"bankruptcy"
]
},
"action": "transfer",
"severity": "hard"
}
]
}Responses
201
Created (status draft).
| Field | Type | Required | Description |
|---|---|---|---|
ai_agent | object (AiAgent) | yes | |
ai_agent.id | string (uuid) | yes | |
ai_agent.name | string | yes | |
ai_agent.status | draft · live · paused · retired | yes | |
ai_agent.voice | object (AiVoice) | no | |
ai_agent.voice.language | string | yes | BCP-47 (e.g. en-US). |
ai_agent.voice.voice_id | string | yes | Provider voice identifier. |
ai_agent.voice.pace | number | yes | 0 = slow … 1 = fast (0.5 ≈ natural). |
ai_agent.disclosure | object (AiDisclosure) | no | |
ai_agent.disclosure.text | string | yes | The opening artificial/prerecorded-voice disclosure (content-validated, EN/ES). |
ai_agent.disclosure.mandated | true | yes | Always true — the disclosure cannot be disabled. |
ai_agent.disclosure.locked | boolean | no | True once the agent has been live; text is then immutable. |
ai_agent.policy | object (AiPolicy) | no | Negotiation guardrails the AI is bounded by. |
ai_agent.policy.max_settlement_pct | integer | no | Max settlement as a percent of balance. |
ai_agent.policy.min_payment_cents | integer | no | |
ai_agent.policy.max_plan_months | integer | no | |
ai_agent.policy.take_payments | none · card_on_file | no | |
ai_agent.policy.prohibited | array of string | no | Disallowed conduct/topics (free-form tags). |
ai_agent.handoff_rules | array of AiHandoffRule | no | |
ai_agent.handoff_rules[].id | string (uuid) | null | no | |
ai_agent.handoff_rules[].trigger | keywords · negative_sentiment · payment_over · human_requested | yes | |
ai_agent.handoff_rules[].params | object | no | Trigger config, e.g. {keywords:[...]}, {seconds:20}, {cents:50000}. |
ai_agent.handoff_rules[].action | transfer · offer_human · confirm_human | yes | |
ai_agent.handoff_rules[].severity | hard · soft | yes | |
ai_agent.handoff_rules[].locked | boolean | no | human_requested is ALWAYS locked (never removable/editable). |
ai_agent.policy_version | integer | yes | Monotonic; increments on every versioned change. Transcripts cite it. |
ai_agent.created_at | string (date-time) | no | |
ai_agent.updated_at | string (date-time) | no |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended.
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}Fetch an AI agent
GET /v1/ai/agents/{id}Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The AI agent.
| Field | Type | Required | Description |
|---|---|---|---|
ai_agent | object (AiAgent) | yes | |
ai_agent.id | string (uuid) | yes | |
ai_agent.name | string | yes | |
ai_agent.status | draft · live · paused · retired | yes | |
ai_agent.voice | object (AiVoice) | no | |
ai_agent.voice.language | string | yes | BCP-47 (e.g. en-US). |
ai_agent.voice.voice_id | string | yes | Provider voice identifier. |
ai_agent.voice.pace | number | yes | 0 = slow … 1 = fast (0.5 ≈ natural). |
ai_agent.disclosure | object (AiDisclosure) | no | |
ai_agent.disclosure.text | string | yes | The opening artificial/prerecorded-voice disclosure (content-validated, EN/ES). |
ai_agent.disclosure.mandated | true | yes | Always true — the disclosure cannot be disabled. |
ai_agent.disclosure.locked | boolean | no | True once the agent has been live; text is then immutable. |
ai_agent.policy | object (AiPolicy) | no | Negotiation guardrails the AI is bounded by. |
ai_agent.policy.max_settlement_pct | integer | no | Max settlement as a percent of balance. |
ai_agent.policy.min_payment_cents | integer | no | |
ai_agent.policy.max_plan_months | integer | no | |
ai_agent.policy.take_payments | none · card_on_file | no | |
ai_agent.policy.prohibited | array of string | no | Disallowed conduct/topics (free-form tags). |
ai_agent.handoff_rules | array of AiHandoffRule | no | |
ai_agent.handoff_rules[].id | string (uuid) | null | no | |
ai_agent.handoff_rules[].trigger | keywords · negative_sentiment · payment_over · human_requested | yes | |
ai_agent.handoff_rules[].params | object | no | Trigger config, e.g. {keywords:[...]}, {seconds:20}, {cents:50000}. |
ai_agent.handoff_rules[].action | transfer · offer_human · confirm_human | yes | |
ai_agent.handoff_rules[].severity | hard · soft | yes | |
ai_agent.handoff_rules[].locked | boolean | no | human_requested is ALWAYS locked (never removable/editable). |
ai_agent.policy_version | integer | yes | Monotonic; increments on every versioned change. Transcripts cite it. |
ai_agent.created_at | string (date-time) | no | |
ai_agent.updated_at | string (date-time) | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Update an AI agent (bumps policy_version)
PATCH /v1/ai/agents/{id}Any change to voice / disclosure / policy / handoff_rules / status INCREMENTS policy_version and appends an immutable snapshot (transcripts cite the version in force); a name-only edit does not bump. Constraints: once the agent has been live, disclosure.text is immutable (422); policy_version is monotonic; the human_requested handoff rule stays locked. status moves within draft|live|paused|retired. Management scope: a ddw_ browser token is 403.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | no | |
status | draft · live · paused · retired | no | |
voice | object (AiVoice) | no | |
voice.language | string | yes | BCP-47 (e.g. en-US). |
voice.voice_id | string | yes | Provider voice identifier. |
voice.pace | number | yes | 0 = slow … 1 = fast (0.5 ≈ natural). |
disclosure | object | no | |
disclosure.text | string | no | |
policy | object (AiPolicy) | no | Negotiation guardrails the AI is bounded by. |
policy.max_settlement_pct | integer | no | Max settlement as a percent of balance. |
policy.min_payment_cents | integer | no | |
policy.max_plan_months | integer | no | |
policy.take_payments | none · card_on_file | no | |
policy.prohibited | array of string | no | Disallowed conduct/topics (free-form tags). |
handoff_rules | array of AiHandoffRule | no | |
handoff_rules[].id | string (uuid) | null | no | |
handoff_rules[].trigger | keywords · negative_sentiment · payment_over · human_requested | yes | |
handoff_rules[].params | object | no | Trigger config, e.g. {keywords:[...]}, {seconds:20}, {cents:50000}. |
handoff_rules[].action | transfer · offer_human · confirm_human | yes | |
handoff_rules[].severity | hard · soft | yes | |
handoff_rules[].locked | boolean | no | human_requested is ALWAYS locked (never removable/editable). |
{
"status": "live",
"policy": {
"max_settlement_pct": 65
}
}Responses
200
The updated agent (new policy_version).
| Field | Type | Required | Description |
|---|---|---|---|
ai_agent | object (AiAgent) | yes | |
ai_agent.id | string (uuid) | yes | |
ai_agent.name | string | yes | |
ai_agent.status | draft · live · paused · retired | yes | |
ai_agent.voice | object (AiVoice) | no | |
ai_agent.voice.language | string | yes | BCP-47 (e.g. en-US). |
ai_agent.voice.voice_id | string | yes | Provider voice identifier. |
ai_agent.voice.pace | number | yes | 0 = slow … 1 = fast (0.5 ≈ natural). |
ai_agent.disclosure | object (AiDisclosure) | no | |
ai_agent.disclosure.text | string | yes | The opening artificial/prerecorded-voice disclosure (content-validated, EN/ES). |
ai_agent.disclosure.mandated | true | yes | Always true — the disclosure cannot be disabled. |
ai_agent.disclosure.locked | boolean | no | True once the agent has been live; text is then immutable. |
ai_agent.policy | object (AiPolicy) | no | Negotiation guardrails the AI is bounded by. |
ai_agent.policy.max_settlement_pct | integer | no | Max settlement as a percent of balance. |
ai_agent.policy.min_payment_cents | integer | no | |
ai_agent.policy.max_plan_months | integer | no | |
ai_agent.policy.take_payments | none · card_on_file | no | |
ai_agent.policy.prohibited | array of string | no | Disallowed conduct/topics (free-form tags). |
ai_agent.handoff_rules | array of AiHandoffRule | no | |
ai_agent.handoff_rules[].id | string (uuid) | null | no | |
ai_agent.handoff_rules[].trigger | keywords · negative_sentiment · payment_over · human_requested | yes | |
ai_agent.handoff_rules[].params | object | no | Trigger config, e.g. {keywords:[...]}, {seconds:20}, {cents:50000}. |
ai_agent.handoff_rules[].action | transfer · offer_human · confirm_human | yes | |
ai_agent.handoff_rules[].severity | hard · soft | yes | |
ai_agent.handoff_rules[].locked | boolean | no | human_requested is ALWAYS locked (never removable/editable). |
ai_agent.policy_version | integer | yes | Monotonic; increments on every versioned change. Transcripts cite it. |
ai_agent.created_at | string (date-time) | no | |
ai_agent.updated_at | string (date-time) | no |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended.
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}422 — Unprocessable
Shape is fine, semantics are not (broken domain rule).
{
"error": {
"code": "unprocessable",
"message": "scheduled_at must be at least 10 minutes out"
}
}Retire an AI agent (never deletes)
DELETE /v1/ai/agents/{id}Soft-retire (status retired): the row and all its version snapshots survive because live/historical conversations and transcripts reference them. A retired agent cannot dial. Management scope: a ddw_ browser token is 403.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The retired agent.
| Field | Type | Required | Description |
|---|---|---|---|
ai_agent | object (AiAgent) | yes | |
ai_agent.id | string (uuid) | yes | |
ai_agent.name | string | yes | |
ai_agent.status | draft · live · paused · retired | yes | |
ai_agent.voice | object (AiVoice) | no | |
ai_agent.voice.language | string | yes | BCP-47 (e.g. en-US). |
ai_agent.voice.voice_id | string | yes | Provider voice identifier. |
ai_agent.voice.pace | number | yes | 0 = slow … 1 = fast (0.5 ≈ natural). |
ai_agent.disclosure | object (AiDisclosure) | no | |
ai_agent.disclosure.text | string | yes | The opening artificial/prerecorded-voice disclosure (content-validated, EN/ES). |
ai_agent.disclosure.mandated | true | yes | Always true — the disclosure cannot be disabled. |
ai_agent.disclosure.locked | boolean | no | True once the agent has been live; text is then immutable. |
ai_agent.policy | object (AiPolicy) | no | Negotiation guardrails the AI is bounded by. |
ai_agent.policy.max_settlement_pct | integer | no | Max settlement as a percent of balance. |
ai_agent.policy.min_payment_cents | integer | no | |
ai_agent.policy.max_plan_months | integer | no | |
ai_agent.policy.take_payments | none · card_on_file | no | |
ai_agent.policy.prohibited | array of string | no | Disallowed conduct/topics (free-form tags). |
ai_agent.handoff_rules | array of AiHandoffRule | no | |
ai_agent.handoff_rules[].id | string (uuid) | null | no | |
ai_agent.handoff_rules[].trigger | keywords · negative_sentiment · payment_over · human_requested | yes | |
ai_agent.handoff_rules[].params | object | no | Trigger config, e.g. {keywords:[...]}, {seconds:20}, {cents:50000}. |
ai_agent.handoff_rules[].action | transfer · offer_human · confirm_human | yes | |
ai_agent.handoff_rules[].severity | hard · soft | yes | |
ai_agent.handoff_rules[].locked | boolean | no | human_requested is ALWAYS locked (never removable/editable). |
ai_agent.policy_version | integer | yes | Monotonic; increments on every versioned change. Transcripts cite it. |
ai_agent.created_at | string (date-time) | no | |
ai_agent.updated_at | string (date-time) | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended.
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Sandbox conversation (no live call)
POST /v1/ai/agents/{id}/testDrives the agent through a stateless SANDBOX exchange (proxied to the AI runtime) — NO real call, NO ai_conversations row, NO dial. Omit session_id to start; echo it back to continue the same sandbox thread. 503 when the runtime sandbox is unconfigured/unreachable. Management scope: a ddw_ browser token is 403.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
session_id | string (uuid) | no | Omit to start a new sandbox thread; echo to continue one. |
message | string | yes |
{
"message": "Yes — I can pay $150 on the 27th."
}Responses
200
The agent's sandbox reply.
| Field | Type | Required | Description |
|---|---|---|---|
session_id | string (uuid) | yes | |
reply | string | yes |
{
"session_id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
"reply": "I can set that up: $150 on June 27 using the card on file ending 4417. Shall I confirm?"
}400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended.
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}502 — BadGateway
An upstream dependency (the AI runtime sandbox) returned an unexpected/failed response.
{
"error": {
"code": "bad_gateway",
"message": "the AI runtime sandbox could not be reached"
}
}503 — SandboxUnavailable
The AI runtime sandbox is unconfigured or unreachable (fail-closed — the runtime may not exist yet).
{
"error": {
"code": "sandbox_unavailable",
"message": "the AI runtime sandbox is not configured or unreachable"
}
}List AI conversations (live feed or history)
GET /v1/ai/conversationsactive=true returns the LIVE floor (conversations still in progress), newest first, UNPAGINATED. Without it, returns closed conversations, keyset-paginated (from/to bound started_at).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
active | query | boolean | no | true → only in-progress conversations (the live feed). |
agent_id | query | string (uuid) | no | Restrict to conversations handled by this AI agent. |
from | query | string (date-time) | no | RFC 3339 lower bound (endpoint-specific field; default trailing 30 UTC days where noted). |
to | query | string (date-time) | no | RFC 3339 upper bound. |
limit | query | integer | no | Page size (default 100, max 1000). |
cursor | query | string | no | Opaque keyset cursor from the previous page's next_cursor. |
Responses
200
A page/feed of conversations.
| Field | Type | Required | Description |
|---|---|---|---|
ai_conversations | array of AiConversationListItem | yes | |
ai_conversations[].id | string (uuid) | yes | |
ai_conversations[].call_id | string | null | no | The call's FreeSWITCH uuid. |
ai_conversations[].ai_agent_id | string (uuid) | null | no | |
ai_conversations[].account_ref | string | null | no | |
ai_conversations[].us_state | string | null | no | |
ai_conversations[].phase | disclosure · verification · negotiation · payment_setup · wrapup | yes | |
ai_conversations[].duration_secs | integer | null | no | |
ai_conversations[].sentiment | calm · positive · frustrated · negative | null | no | |
ai_conversations[].handoff_suggested | boolean | no | |
ai_conversations[].started_at | string (date-time) | no | |
ai_conversations[].ended_at | string (date-time) | null | no | |
next_cursor | string | null | yes | Opaque keyset cursor; null on the last page and for the active=true feed. |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}Fetch a conversation with transcript and outcome
GET /v1/ai/conversations/{id}Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The conversation, its full transcript and its outcome.
| Field | Type | Required | Description |
|---|---|---|---|
ai_conversation | any | yes |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Supervisor takeover of a live AI conversation
POST /v1/ai/conversations/{id}/takeoverTransfers the debtor leg from the AI to the supervisor's REGISTERED endpoint and tears down the orphaned AI leg — the SAME evidence-first mechanic as POST /v1/supervision/calls/{call_id}/takeover (a durable supervision_actions row is written BEFORE any switch command). The {id} is the CONVERSATION id; the server resolves its call_id and drives Dialer.Supervision. A conversation with no live call (sandbox/never-dialed) is 409. session_id in the result is null (no eavesdrop leg). Same WHO gate as the supervision operations (ADR #116): supervisor_ext must carry role supervisor|admin, else 403 role_forbidden (evidenced as refused).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
supervisor_ext | string | yes | Must be an ACTIVE roster extension of THIS tenant with role supervisor or admin (ADR |
{
"supervisor_ext": "2000"
}Responses
200
Debtor leg transferred to the supervisor (audit row durable).
| Field | Type | Required | Description |
|---|---|---|---|
supervision | object (SupervisionResult) | yes | |
supervision.action | listen · whisper · barge · takeover | no | |
supervision.call_id | string | no | |
supervision.supervisor_ext | string | no | |
supervision.session_id | string | null | no | Supervisor leg uuid; null for takeover (no eavesdrop leg). |
supervision.audit_id | string (uuid) | no | The durable supervision_actions row. |
supervision.to_number | string | no | Masked last-4 (***0184) — the full E.164 never crosses this surface. |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — SupervisionRoleForbidden
The caller may not supervise with that supervisor_ext (ADR #116, core#928). code role_forbidden: the extension resolves to an active agent whose role is not supervisor or admin. code forbidden: an agent (ddw_) principal named an extension that is not its own. Either way the attempt IS evidenced — a supervision_actions row with result: refused — and ZERO switch commands ran. Per tenant, supervision_role_enforced = false turns the role check off (the identity binding stays).
role_forbidden
{
"error": {
"code": "role_forbidden",
"message": "supervisor_ext must belong to an agent with role supervisor or admin"
}
}forbidden
{
"error": {
"code": "forbidden",
"message": "supervisor_ext must be the agent bound to this token"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}502 — SupervisionFailed
The switch rejected the supervision command (call already ended / supervisor device not registered).
{
"error": {
"code": "supervision_failed",
"message": "the switch rejected the supervision command"
}
}503 — SupervisionUnavailable
code audit_unavailable: the evidence row could not be written so NO switch command was sent (evidence-first, fail-closed). codeesl_unavailable: switch control link down. Both retryable.
audit_unavailable
{
"error": {
"code": "audit_unavailable",
"message": "supervision audit unavailable, retry"
}
}esl_unavailable
{
"error": {
"code": "esl_unavailable",
"message": "telephony control unavailable, retry"
}
}Kill-switch — pause all AI dialing (tenant-wide)
POST /v1/ai/pauseIdempotent. Immediately stops the tenant's AI agents from originating new calls (the "Pause all AI dialing" control; the AIDisclosure gate then blocks every AI origination fail-closed). In-flight conversations continue; use takeover to intervene. The toggle is stamped (paused_at, paused_by). Management scope: a ddw_ browser token is 403.
Responses
200
AI dialing is paused.
| Field | Type | Required | Description |
|---|---|---|---|
ai_dialing | active · paused | yes | |
paused_at | string (date-time) | null | no | |
paused_by | string | null | no |
{
"ai_dialing": "paused",
"paused_at": "2026-07-09T17:40:00Z"
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended.
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}Kill-switch — resume AI dialing (tenant-wide)
POST /v1/ai/resumeIdempotent inverse of POST /v1/ai/pause. Management scope (403 for ddw_).
Responses
200
AI dialing is active.
| Field | Type | Required | Description |
|---|---|---|---|
ai_dialing | active · paused | yes | |
paused_at | string (date-time) | null | no | |
paused_by | string | null | no |
{
"ai_dialing": "active",
"paused_at": null
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended.
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}AI-dialing kill-switch state
GET /v1/ai/statusThe current kill-switch state (same shape as pause/resume).
Responses
200
The kill-switch state.
| Field | Type | Required | Description |
|---|---|---|---|
ai_dialing | active · paused | yes | |
paused_at | string (date-time) | null | no | |
paused_by | string | null | no |
{
"ai_dialing": "active",
"paused_at": null
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}Today's AI operating metrics (UTC day)
GET /v1/ai/metrics/todayAggregate for the current UTC day off ai_conversations + outcomes + the durable ai_usage meter (the same billable feed as /v1/stats/ai-usage).
Responses
200
Today's metrics.
| Field | Type | Required | Description |
|---|---|---|---|
date | string (date) | no | |
handled | integer | no | AI-handled conversations today. |
containment_rate | number | no | Share resolved without human handoff (0..1). |
avg_handle_secs | integer | no | |
handled_delta_pct | number | null | no | Percent change in handled vs. yesterday up to the SAME time-of-day; null when yesterday had none. |
handoffs | object | no | |
handoffs.count | integer | no | |
handoffs.pct | number | no | |
ai_minutes | number | no | Billable AI minutes today (from the ai_usage meter). |
billed_cents | integer | no | |
ptp | object | no | |
ptp.count | integer | no | |
ptp.amount_cents | integer | no | |
payments | object | no | |
payments.count | integer | no | |
payments.amount_cents | integer | no | |
voicemail_drops | integer | no | Conversations the runtime classified voicemail (its AMD verdict) today. |
compliance_violations | integer | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}Per-campaign artificial-voice eligibility
GET /v1/ai/eligibilityOne entry per campaign that PLAYS artificial voice (AI or TTS/IVR/survey), each with whether it may originate now given the tenant's AI-disclosure capability and the AI-dialing kill-switch. Per-recipient consent (the other half of the artificial-voice gate) is enforced at dial time — not summarized here.
Responses
200
The eligibility list.
| Field | Type | Required | Description |
|---|---|---|---|
eligibility | array of AiEligibilityItem | yes | |
eligibility[].campaign_id | string (uuid) | yes | |
eligibility[].name | string | yes | |
eligibility[].eligible | boolean | yes | |
eligibility[].reason | string | null | no | Why it cannot originate (ai_dialing_paused / disclosure_missing); null when eligible. |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}