Skip to content

Voice ai

Generated from DialerDigital/core commit 6e9f026f0a72 — spec openapi/dialer-v1.yaml v0.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

MethodPathSummary
GET/v1/ai/agentsList AI voice agents
POST/v1/ai/agentsCreate 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}/testSandbox conversation (no live call)
GET/v1/ai/conversationsList AI conversations (live feed or history)
GET/v1/ai/conversations/{id}Fetch a conversation with transcript and outcome
POST/v1/ai/conversations/{id}/takeoverSupervisor takeover of a live AI conversation
POST/v1/ai/pauseKill-switch — pause all AI dialing (tenant-wide)
POST/v1/ai/resumeKill-switch — resume AI dialing (tenant-wide)
GET/v1/ai/statusAI-dialing kill-switch state
GET/v1/ai/metrics/todayToday's AI operating metrics (UTC day)
GET/v1/ai/eligibilityPer-campaign artificial-voice eligibility

List AI voice agents

http
GET /v1/ai/agents

Responses

200

All AI agents of the tenant.

FieldTypeRequiredDescription
ai_agentsarray of AiAgentyes
ai_agents[].idstring (uuid)yes
ai_agents[].namestringyes
ai_agents[].statusdraft · live · paused · retiredyes
ai_agents[].voiceobject (AiVoice)no
ai_agents[].voice.languagestringyesBCP-47 (e.g. en-US).
ai_agents[].voice.voice_idstringyesProvider voice identifier.
ai_agents[].voice.pacenumberyes0 = slow … 1 = fast (0.5 ≈ natural).
ai_agents[].disclosureobject (AiDisclosure)no
ai_agents[].disclosure.textstringyesThe opening artificial/prerecorded-voice disclosure (content-validated, EN/ES).
ai_agents[].disclosure.mandatedtrueyesAlways true — the disclosure cannot be disabled.
ai_agents[].disclosure.lockedbooleannoTrue once the agent has been live; text is then immutable.
ai_agents[].policyobject (AiPolicy)noNegotiation guardrails the AI is bounded by.
ai_agents[].policy.max_settlement_pctintegernoMax settlement as a percent of balance.
ai_agents[].policy.min_payment_centsintegerno
ai_agents[].policy.max_plan_monthsintegerno
ai_agents[].policy.take_paymentsnone · card_on_fileno
ai_agents[].policy.prohibitedarray of stringnoDisallowed conduct/topics (free-form tags).
ai_agents[].handoff_rulesarray of AiHandoffRuleno
ai_agents[].handoff_rules[].idstring (uuid) | nullno
ai_agents[].handoff_rules[].triggerkeywords · negative_sentiment · payment_over · human_requestedyes
ai_agents[].handoff_rules[].paramsobjectnoTrigger config, e.g. {keywords:[...]}, {seconds:20}, {cents:50000}.
ai_agents[].handoff_rules[].actiontransfer · offer_human · confirm_humanyes
ai_agents[].handoff_rules[].severityhard · softyes
ai_agents[].handoff_rules[].lockedbooleannohuman_requested is ALWAYS locked (never removable/editable).
ai_agents[].policy_versionintegeryesMonotonic; increments on every versioned change. Transcripts cite it.
ai_agents[].created_atstring (date-time)no
ai_agents[].updated_atstring (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).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

Create an AI agent (status draft)

http
POST /v1/ai/agents

Creates 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 name409. Management scope: a ddw_ browser token is 403.

Request body (JSON, required)

FieldTypeRequiredDescription
namestringno
statusdraft · live · paused · retiredno
voiceobject (AiVoice)no
voice.languagestringyesBCP-47 (e.g. en-US).
voice.voice_idstringyesProvider voice identifier.
voice.pacenumberyes0 = slow … 1 = fast (0.5 ≈ natural).
disclosureobjectno
disclosure.textstringno
policyobject (AiPolicy)noNegotiation guardrails the AI is bounded by.
policy.max_settlement_pctintegernoMax settlement as a percent of balance.
policy.min_payment_centsintegerno
policy.max_plan_monthsintegerno
policy.take_paymentsnone · card_on_fileno
policy.prohibitedarray of stringnoDisallowed conduct/topics (free-form tags).
handoff_rulesarray of AiHandoffRuleno
handoff_rules[].idstring (uuid) | nullno
handoff_rules[].triggerkeywords · negative_sentiment · payment_over · human_requestedyes
handoff_rules[].paramsobjectnoTrigger config, e.g. {keywords:[...]}, {seconds:20}, {cents:50000}.
handoff_rules[].actiontransfer · offer_human · confirm_humanyes
handoff_rules[].severityhard · softyes
handoff_rules[].lockedbooleannohuman_requested is ALWAYS locked (never removable/editable).
json
{
  "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).

FieldTypeRequiredDescription
ai_agentobject (AiAgent)yes
ai_agent.idstring (uuid)yes
ai_agent.namestringyes
ai_agent.statusdraft · live · paused · retiredyes
ai_agent.voiceobject (AiVoice)no
ai_agent.voice.languagestringyesBCP-47 (e.g. en-US).
ai_agent.voice.voice_idstringyesProvider voice identifier.
ai_agent.voice.pacenumberyes0 = slow … 1 = fast (0.5 ≈ natural).
ai_agent.disclosureobject (AiDisclosure)no
ai_agent.disclosure.textstringyesThe opening artificial/prerecorded-voice disclosure (content-validated, EN/ES).
ai_agent.disclosure.mandatedtrueyesAlways true — the disclosure cannot be disabled.
ai_agent.disclosure.lockedbooleannoTrue once the agent has been live; text is then immutable.
ai_agent.policyobject (AiPolicy)noNegotiation guardrails the AI is bounded by.
ai_agent.policy.max_settlement_pctintegernoMax settlement as a percent of balance.
ai_agent.policy.min_payment_centsintegerno
ai_agent.policy.max_plan_monthsintegerno
ai_agent.policy.take_paymentsnone · card_on_fileno
ai_agent.policy.prohibitedarray of stringnoDisallowed conduct/topics (free-form tags).
ai_agent.handoff_rulesarray of AiHandoffRuleno
ai_agent.handoff_rules[].idstring (uuid) | nullno
ai_agent.handoff_rules[].triggerkeywords · negative_sentiment · payment_over · human_requestedyes
ai_agent.handoff_rules[].paramsobjectnoTrigger config, e.g. {keywords:[...]}, {seconds:20}, {cents:50000}.
ai_agent.handoff_rules[].actiontransfer · offer_human · confirm_humanyes
ai_agent.handoff_rules[].severityhard · softyes
ai_agent.handoff_rules[].lockedbooleannohuman_requested is ALWAYS locked (never removable/editable).
ai_agent.policy_versionintegeryesMonotonic; increments on every versioned change. Transcripts cite it.
ai_agent.created_atstring (date-time)no
ai_agent.updated_atstring (date-time)no

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "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).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

403 — Forbidden

The tenant is suspended.

json
{
  "error": {
    "code": "forbidden",
    "message": "tenant is suspended"
  }
}

409 — Conflict

Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.

json
{
  "error": {
    "code": "conflict",
    "message": "invalid state transition"
  }
}

Fetch an AI agent

http
GET /v1/ai/agents/{id}

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200

The AI agent.

FieldTypeRequiredDescription
ai_agentobject (AiAgent)yes
ai_agent.idstring (uuid)yes
ai_agent.namestringyes
ai_agent.statusdraft · live · paused · retiredyes
ai_agent.voiceobject (AiVoice)no
ai_agent.voice.languagestringyesBCP-47 (e.g. en-US).
ai_agent.voice.voice_idstringyesProvider voice identifier.
ai_agent.voice.pacenumberyes0 = slow … 1 = fast (0.5 ≈ natural).
ai_agent.disclosureobject (AiDisclosure)no
ai_agent.disclosure.textstringyesThe opening artificial/prerecorded-voice disclosure (content-validated, EN/ES).
ai_agent.disclosure.mandatedtrueyesAlways true — the disclosure cannot be disabled.
ai_agent.disclosure.lockedbooleannoTrue once the agent has been live; text is then immutable.
ai_agent.policyobject (AiPolicy)noNegotiation guardrails the AI is bounded by.
ai_agent.policy.max_settlement_pctintegernoMax settlement as a percent of balance.
ai_agent.policy.min_payment_centsintegerno
ai_agent.policy.max_plan_monthsintegerno
ai_agent.policy.take_paymentsnone · card_on_fileno
ai_agent.policy.prohibitedarray of stringnoDisallowed conduct/topics (free-form tags).
ai_agent.handoff_rulesarray of AiHandoffRuleno
ai_agent.handoff_rules[].idstring (uuid) | nullno
ai_agent.handoff_rules[].triggerkeywords · negative_sentiment · payment_over · human_requestedyes
ai_agent.handoff_rules[].paramsobjectnoTrigger config, e.g. {keywords:[...]}, {seconds:20}, {cents:50000}.
ai_agent.handoff_rules[].actiontransfer · offer_human · confirm_humanyes
ai_agent.handoff_rules[].severityhard · softyes
ai_agent.handoff_rules[].lockedbooleannohuman_requested is ALWAYS locked (never removable/editable).
ai_agent.policy_versionintegeryesMonotonic; increments on every versioned change. Transcripts cite it.
ai_agent.created_atstring (date-time)no
ai_agent.updated_atstring (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).

json
{
  "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).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

Update an AI agent (bumps policy_version)

http
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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
namestringno
statusdraft · live · paused · retiredno
voiceobject (AiVoice)no
voice.languagestringyesBCP-47 (e.g. en-US).
voice.voice_idstringyesProvider voice identifier.
voice.pacenumberyes0 = slow … 1 = fast (0.5 ≈ natural).
disclosureobjectno
disclosure.textstringno
policyobject (AiPolicy)noNegotiation guardrails the AI is bounded by.
policy.max_settlement_pctintegernoMax settlement as a percent of balance.
policy.min_payment_centsintegerno
policy.max_plan_monthsintegerno
policy.take_paymentsnone · card_on_fileno
policy.prohibitedarray of stringnoDisallowed conduct/topics (free-form tags).
handoff_rulesarray of AiHandoffRuleno
handoff_rules[].idstring (uuid) | nullno
handoff_rules[].triggerkeywords · negative_sentiment · payment_over · human_requestedyes
handoff_rules[].paramsobjectnoTrigger config, e.g. {keywords:[...]}, {seconds:20}, {cents:50000}.
handoff_rules[].actiontransfer · offer_human · confirm_humanyes
handoff_rules[].severityhard · softyes
handoff_rules[].lockedbooleannohuman_requested is ALWAYS locked (never removable/editable).
json
{
  "status": "live",
  "policy": {
    "max_settlement_pct": 65
  }
}

Responses

200

The updated agent (new policy_version).

FieldTypeRequiredDescription
ai_agentobject (AiAgent)yes
ai_agent.idstring (uuid)yes
ai_agent.namestringyes
ai_agent.statusdraft · live · paused · retiredyes
ai_agent.voiceobject (AiVoice)no
ai_agent.voice.languagestringyesBCP-47 (e.g. en-US).
ai_agent.voice.voice_idstringyesProvider voice identifier.
ai_agent.voice.pacenumberyes0 = slow … 1 = fast (0.5 ≈ natural).
ai_agent.disclosureobject (AiDisclosure)no
ai_agent.disclosure.textstringyesThe opening artificial/prerecorded-voice disclosure (content-validated, EN/ES).
ai_agent.disclosure.mandatedtrueyesAlways true — the disclosure cannot be disabled.
ai_agent.disclosure.lockedbooleannoTrue once the agent has been live; text is then immutable.
ai_agent.policyobject (AiPolicy)noNegotiation guardrails the AI is bounded by.
ai_agent.policy.max_settlement_pctintegernoMax settlement as a percent of balance.
ai_agent.policy.min_payment_centsintegerno
ai_agent.policy.max_plan_monthsintegerno
ai_agent.policy.take_paymentsnone · card_on_fileno
ai_agent.policy.prohibitedarray of stringnoDisallowed conduct/topics (free-form tags).
ai_agent.handoff_rulesarray of AiHandoffRuleno
ai_agent.handoff_rules[].idstring (uuid) | nullno
ai_agent.handoff_rules[].triggerkeywords · negative_sentiment · payment_over · human_requestedyes
ai_agent.handoff_rules[].paramsobjectnoTrigger config, e.g. {keywords:[...]}, {seconds:20}, {cents:50000}.
ai_agent.handoff_rules[].actiontransfer · offer_human · confirm_humanyes
ai_agent.handoff_rules[].severityhard · softyes
ai_agent.handoff_rules[].lockedbooleannohuman_requested is ALWAYS locked (never removable/editable).
ai_agent.policy_versionintegeryesMonotonic; increments on every versioned change. Transcripts cite it.
ai_agent.created_atstring (date-time)no
ai_agent.updated_atstring (date-time)no

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "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).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

403 — Forbidden

The tenant is suspended.

json
{
  "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).

json
{
  "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.

json
{
  "error": {
    "code": "conflict",
    "message": "invalid state transition"
  }
}

422 — Unprocessable

Shape is fine, semantics are not (broken domain rule).

json
{
  "error": {
    "code": "unprocessable",
    "message": "scheduled_at must be at least 10 minutes out"
  }
}

Retire an AI agent (never deletes)

http
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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200

The retired agent.

FieldTypeRequiredDescription
ai_agentobject (AiAgent)yes
ai_agent.idstring (uuid)yes
ai_agent.namestringyes
ai_agent.statusdraft · live · paused · retiredyes
ai_agent.voiceobject (AiVoice)no
ai_agent.voice.languagestringyesBCP-47 (e.g. en-US).
ai_agent.voice.voice_idstringyesProvider voice identifier.
ai_agent.voice.pacenumberyes0 = slow … 1 = fast (0.5 ≈ natural).
ai_agent.disclosureobject (AiDisclosure)no
ai_agent.disclosure.textstringyesThe opening artificial/prerecorded-voice disclosure (content-validated, EN/ES).
ai_agent.disclosure.mandatedtrueyesAlways true — the disclosure cannot be disabled.
ai_agent.disclosure.lockedbooleannoTrue once the agent has been live; text is then immutable.
ai_agent.policyobject (AiPolicy)noNegotiation guardrails the AI is bounded by.
ai_agent.policy.max_settlement_pctintegernoMax settlement as a percent of balance.
ai_agent.policy.min_payment_centsintegerno
ai_agent.policy.max_plan_monthsintegerno
ai_agent.policy.take_paymentsnone · card_on_fileno
ai_agent.policy.prohibitedarray of stringnoDisallowed conduct/topics (free-form tags).
ai_agent.handoff_rulesarray of AiHandoffRuleno
ai_agent.handoff_rules[].idstring (uuid) | nullno
ai_agent.handoff_rules[].triggerkeywords · negative_sentiment · payment_over · human_requestedyes
ai_agent.handoff_rules[].paramsobjectnoTrigger config, e.g. {keywords:[...]}, {seconds:20}, {cents:50000}.
ai_agent.handoff_rules[].actiontransfer · offer_human · confirm_humanyes
ai_agent.handoff_rules[].severityhard · softyes
ai_agent.handoff_rules[].lockedbooleannohuman_requested is ALWAYS locked (never removable/editable).
ai_agent.policy_versionintegeryesMonotonic; increments on every versioned change. Transcripts cite it.
ai_agent.created_atstring (date-time)no
ai_agent.updated_atstring (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).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

403 — Forbidden

The tenant is suspended.

json
{
  "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).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

Sandbox conversation (no live call)

http
POST /v1/ai/agents/{id}/test

Drives 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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
session_idstring (uuid)noOmit to start a new sandbox thread; echo to continue one.
messagestringyes
json
{
  "message": "Yes — I can pay $150 on the 27th."
}

Responses

200

The agent's sandbox reply.

FieldTypeRequiredDescription
session_idstring (uuid)yes
replystringyes
json
{
  "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).

json
{
  "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).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

403 — Forbidden

The tenant is suspended.

json
{
  "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).

json
{
  "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.

json
{
  "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).

json
{
  "error": {
    "code": "sandbox_unavailable",
    "message": "the AI runtime sandbox is not configured or unreachable"
  }
}

List AI conversations (live feed or history)

http
GET /v1/ai/conversations

active=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

NameInTypeRequiredDescription
activequerybooleannotrue → only in-progress conversations (the live feed).
agent_idquerystring (uuid)noRestrict to conversations handled by this AI agent.
fromquerystring (date-time)noRFC 3339 lower bound (endpoint-specific field; default trailing 30 UTC days where noted).
toquerystring (date-time)noRFC 3339 upper bound.
limitqueryintegernoPage size (default 100, max 1000).
cursorquerystringnoOpaque keyset cursor from the previous page's next_cursor.

Responses

200

A page/feed of conversations.

FieldTypeRequiredDescription
ai_conversationsarray of AiConversationListItemyes
ai_conversations[].idstring (uuid)yes
ai_conversations[].call_idstring | nullnoThe call's FreeSWITCH uuid.
ai_conversations[].ai_agent_idstring (uuid) | nullno
ai_conversations[].account_refstring | nullno
ai_conversations[].us_statestring | nullno
ai_conversations[].phasedisclosure · verification · negotiation · payment_setup · wrapupyes
ai_conversations[].duration_secsinteger | nullno
ai_conversations[].sentimentcalm · positive · frustrated · negative | nullno
ai_conversations[].handoff_suggestedbooleanno
ai_conversations[].started_atstring (date-time)no
ai_conversations[].ended_atstring (date-time) | nullno
next_cursorstring | nullyesOpaque keyset cursor; null on the last page and for the active=true feed.

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "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).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

Fetch a conversation with transcript and outcome

http
GET /v1/ai/conversations/{id}

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200

The conversation, its full transcript and its outcome.

FieldTypeRequiredDescription
ai_conversationanyyes

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "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).

json
{
  "error": {
    "code": "not_found",
    "message": "resource not found for this tenant"
  }
}

Supervisor takeover of a live AI conversation

http
POST /v1/ai/conversations/{id}/takeover

Transfers 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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
supervisor_extstringyesMust be an ACTIVE roster extension of THIS tenant with role supervisor or admin (ADR
json
{
  "supervisor_ext": "2000"
}

Responses

200

Debtor leg transferred to the supervisor (audit row durable).

FieldTypeRequiredDescription
supervisionobject (SupervisionResult)yes
supervision.actionlisten · whisper · barge · takeoverno
supervision.call_idstringno
supervision.supervisor_extstringno
supervision.session_idstring | nullnoSupervisor leg uuid; null for takeover (no eavesdrop leg).
supervision.audit_idstring (uuid)noThe durable supervision_actions row.
supervision.to_numberstringnoMasked last-4 (***0184) — the full E.164 never crosses this surface.

400 — BadRequest

Malformed request (shape/type errors, invalid filters, bad timestamps).

json
{
  "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).

json
{
  "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

json
{
  "error": {
    "code": "role_forbidden",
    "message": "supervisor_ext must belong to an agent with role supervisor or admin"
  }
}

forbidden

json
{
  "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).

json
{
  "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.

json
{
  "error": {
    "code": "conflict",
    "message": "invalid state transition"
  }
}

502 — SupervisionFailed

The switch rejected the supervision command (call already ended / supervisor device not registered).

json
{
  "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

json
{
  "error": {
    "code": "audit_unavailable",
    "message": "supervision audit unavailable, retry"
  }
}

esl_unavailable

json
{
  "error": {
    "code": "esl_unavailable",
    "message": "telephony control unavailable, retry"
  }
}

Kill-switch — pause all AI dialing (tenant-wide)

http
POST /v1/ai/pause

Idempotent. 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.

FieldTypeRequiredDescription
ai_dialingactive · pausedyes
paused_atstring (date-time) | nullno
paused_bystring | nullno
json
{
  "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).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

403 — Forbidden

The tenant is suspended.

json
{
  "error": {
    "code": "forbidden",
    "message": "tenant is suspended"
  }
}

Kill-switch — resume AI dialing (tenant-wide)

http
POST /v1/ai/resume

Idempotent inverse of POST /v1/ai/pause. Management scope (403 for ddw_).

Responses

200

AI dialing is active.

FieldTypeRequiredDescription
ai_dialingactive · pausedyes
paused_atstring (date-time) | nullno
paused_bystring | nullno
json
{
  "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).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

403 — Forbidden

The tenant is suspended.

json
{
  "error": {
    "code": "forbidden",
    "message": "tenant is suspended"
  }
}

AI-dialing kill-switch state

http
GET /v1/ai/status

The current kill-switch state (same shape as pause/resume).

Responses

200

The kill-switch state.

FieldTypeRequiredDescription
ai_dialingactive · pausedyes
paused_atstring (date-time) | nullno
paused_bystring | nullno
json
{
  "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).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

Today's AI operating metrics (UTC day)

http
GET /v1/ai/metrics/today

Aggregate 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.

FieldTypeRequiredDescription
datestring (date)no
handledintegernoAI-handled conversations today.
containment_ratenumbernoShare resolved without human handoff (0..1).
avg_handle_secsintegerno
handled_delta_pctnumber | nullnoPercent change in handled vs. yesterday up to the SAME time-of-day; null when yesterday had none.
handoffsobjectno
handoffs.countintegerno
handoffs.pctnumberno
ai_minutesnumbernoBillable AI minutes today (from the ai_usage meter).
billed_centsintegerno
ptpobjectno
ptp.countintegerno
ptp.amount_centsintegerno
paymentsobjectno
payments.countintegerno
payments.amount_centsintegerno
voicemail_dropsintegernoConversations the runtime classified voicemail (its AMD verdict) today.
compliance_violationsintegerno

401 — Unauthorized

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

Per-campaign artificial-voice eligibility

http
GET /v1/ai/eligibility

One 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.

FieldTypeRequiredDescription
eligibilityarray of AiEligibilityItemyes
eligibility[].campaign_idstring (uuid)yes
eligibility[].namestringyes
eligibility[].eligiblebooleanyes
eligibility[].reasonstring | nullnoWhy 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).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

Nothing in these docs is legal advice — always confirm compliance posture with your own counsel.