Calls
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.
Live-call control verbs (hold/unhold/mute/unmute/hangup), evidence-first.
Endpoints
| Method | Path | Summary |
|---|---|---|
GET | /v1/calls | Call state for the CTI surface (keyset-paginated, ?live=true for in-flight) |
GET | /v1/calls/{call_id} | One call by its call_uuid |
POST | /v1/calls/{call_id}/hold | Place a live call on hold |
POST | /v1/calls/{call_id}/unhold | Release a held call (re-checks GATE_MID_CALL) |
POST | /v1/calls/{call_id}/mute | Mute the agent microphone on a live call |
POST | /v1/calls/{call_id}/unmute | Unmute the agent microphone on a live call |
POST | /v1/calls/{call_id}/hangup | Hang up a live call |
Call state for the CTI surface (keyset-paginated, ?live=true for in-flight)
GET /v1/callsThe read half of the CTI surface (WidgetAllowlist pre-authorizes it for ddw_): the same append-only CDR rows as /v1/call_attempts, keyed by call_uuid for the control verbs, with ?live=true restricting to calls still in flight (ended_at null, originals only).
SCOPE: a tenant (dd_/ddu_) principal reads the whole floor and may filter by any agent_id; an agent (ddw_) principal reads ONLY its own calls — the agent_id filter is FORCED to the token's bound agent, and naming any other agent is 403, never silently rewritten.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
campaign_id | query | string (uuid) | no | |
debt_id | query | string (uuid) | no | |
agent_id | query | string (uuid) | no | |
disposition | query | string | no | Exact match against an OPEN vocabulary: call_attempts.disposition is written VERBATIM (20260725000003_create_disposition_engine.exs) and the seven engine values — answered_human, abandoned, busy, no_answer, canceled, rejected, failed — are a NAMED SUBSET of it, not its boundary: typed agent/AI outcomes (promise_to_pay, wrong_number) and platform markers (not_placed) are equally valid. An unknown code answers 200 with zero rows, never 400. What IS 400 is a malformed SHAPE: the empty string, and the parameter repeated or sent in array form. |
live | query | boolean | no | true → only calls still in flight (ended_at null; originals only). Strict boolean: anything else is 400. |
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
One page of calls.
| Field | Type | Required | Description |
|---|---|---|---|
calls | array of CallAttempt | yes | |
calls[].id | string (uuid) | no | |
calls[].tenant_id | string (uuid) | no | |
calls[].debt_id | string (uuid) | no | |
calls[].campaign_id | string (uuid) | null | no | |
calls[].agent_id | string (uuid) | null | no | |
calls[].consumer_ref | string | no | |
calls[].debt_key | string | no | |
calls[].call_uuid | string | null | no | |
calls[].from_number | string | null | no | |
calls[].to_number | string | no | |
calls[].started_at | string (date-time) | null | no | |
calls[].answered_at | string (date-time) | null | no | |
calls[].ended_at | string (date-time) | null | no | |
calls[].disposition | string | null | no | Engine dispositions: answered_human|abandoned|busy|no_answer|canceled|rejected|failed; typed agent/AI outcomes are free-form (e.g. promise_to_pay). |
calls[].hangup_cause | string | null | no | |
calls[].sip_response_code | integer | null | no | |
calls[].compliance_snapshot | object | null | no | Frozen decision evidence at originate time. |
calls[].amd_verdict | string | null | no | |
calls[].recording_uri | string | null | no | Where the call recording landed (object path/URI); null when the call was not recorded. |
calls[].ai_used | boolean | no | Whether the voice-AI bot ran on this call (the AI add-on meter key). |
calls[].note | string | null | no | The agent's free-text note about the call, entered at disposition time on ANY disposition. It rides on the correction record that carries the disposition, so the ORIGINAL row is normally null. Encrypted at rest (per-tenant envelope) and never queried by content. null = no note, or a record written before the column existed. |
calls[].corrects_id | string (uuid) | null | no | |
calls[].inserted_at | string (date-time) | no | |
next_cursor | string | null | yes | Opaque keyset cursor; null on the last page. |
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"
}
}One call by its call_uuid
GET /v1/calls/{call_id}The ORIGINAL CDR row of the call (corrects_id null — the row the control verbs key on). Ownership mirrors the verbs: a ddw_ principal may read ONLY a call it owns (403 otherwise — the verbs already disclose ownership, so a read gains nothing by masking it as 404); a foreign or unknown call_id is 404.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
call_id | path | string (uuid) | yes | The live call's FreeSWITCH uuid (= call_attempts.call_uuid, also the call_id of the call.* floor events). |
Responses
200
The call.
| Field | Type | Required | Description |
|---|---|---|---|
call | object (CallAttempt) | yes | Append-only CDR row. Originals have corrects_id: null; corrections point at the original — the LATEST correction is the final disposition. |
call.id | string (uuid) | no | |
call.tenant_id | string (uuid) | no | |
call.debt_id | string (uuid) | no | |
call.campaign_id | string (uuid) | null | no | |
call.agent_id | string (uuid) | null | no | |
call.consumer_ref | string | no | |
call.debt_key | string | no | |
call.call_uuid | string | null | no | |
call.from_number | string | null | no | |
call.to_number | string | no | |
call.started_at | string (date-time) | null | no | |
call.answered_at | string (date-time) | null | no | |
call.ended_at | string (date-time) | null | no | |
call.disposition | string | null | no | Engine dispositions: answered_human|abandoned|busy|no_answer|canceled|rejected|failed; typed agent/AI outcomes are free-form (e.g. promise_to_pay). |
call.hangup_cause | string | null | no | |
call.sip_response_code | integer | null | no | |
call.compliance_snapshot | object | null | no | Frozen decision evidence at originate time. |
call.amd_verdict | string | null | no | |
call.recording_uri | string | null | no | Where the call recording landed (object path/URI); null when the call was not recorded. |
call.ai_used | boolean | no | Whether the voice-AI bot ran on this call (the AI add-on meter key). |
call.note | string | null | no | The agent's free-text note about the call, entered at disposition time on ANY disposition. It rides on the correction record that carries the disposition, so the ORIGINAL row is normally null. Encrypted at rest (per-tenant envelope) and never queried by content. null = no note, or a record written before the column existed. |
call.corrects_id | string (uuid) | null | no | |
call.inserted_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"
}
}Place a live call on hold
POST /v1/calls/{call_id}/holdHolds the AGENT leg so the consumer hears hold-music (evidence-first: a call_control_actions row is written before the ESL command). 409 when the call has no bridged agent leg.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
call_id | path | string (uuid) | yes | The live call's FreeSWITCH uuid (= call_attempts.call_uuid, also the call_id of the call.* floor events). |
Responses
200
Held (audit row durable).
| Field | Type | Required | Description |
|---|---|---|---|
call | object (CallControlResult) | yes | |
call.verb | hold · unhold · mute · unmute · hangup | no | |
call.call_id | string | no | |
call.audit_id | string (uuid) | no | The durable call_control_actions row. |
call.to_number | string | no | Masked last-4 (***0184) — the full E.164 never crosses this surface. |
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"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}429 — CallControlThrottled
The NR-18 request-rate brake refused this request (retryable next window). code rate_limited. Bounded per MOST SPECIFIC DURABLE principal (agent > user > tenant), never per tenant when a narrower one exists, so one hostile page cannot throttle its sibling agents. Durable means the budget survives re-authentication: logging back in mints a new token but does not buy a new budget. The four toggles share ONE budget; hangup is exempt and never answers 429, because refusing a teardown would leave a call alive that its operator asked to end. Refused BEFORE the call is looked up, so this answer is identical whether or not the call_id exists. No Retry-After header.
{
"error": {
"code": "rate_limited",
"message": "call control rate limit exceeded; retry later"
}
}502 — CallControlFailed
The switch rejected the control command (call already ended / leg gone). The audit row settles failed.
{
"error": {
"code": "call_control_failed",
"message": "the switch rejected the control command"
}
}503 — CallControlUnavailable
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 / timed out. Both retryable.
audit_unavailable
{
"error": {
"code": "audit_unavailable",
"message": "call-control audit unavailable, retry"
}
}esl_unavailable
{
"error": {
"code": "esl_unavailable",
"message": "telephony control unavailable, retry"
}
}Release a held call (re-checks GATE_MID_CALL)
POST /v1/calls/{call_id}/unholdReleases the hold. Re-evaluates the mid-call trio — consent revocation, written cease-and-desist (live reads) and quiet hours against the consumer timezone(s) frozen in the call's compliance snapshot. Blocked -> 422: the call stays on hold and an automatic hangup is scheduled (auto_hangup_at in the body, C9) so the consumer is never kept held indefinitely. 409 when the call has no bridged agent leg.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
call_id | path | string (uuid) | yes | The live call's FreeSWITCH uuid (= call_attempts.call_uuid, also the call_id of the call.* floor events). |
Responses
200
Released (audit row durable).
| Field | Type | Required | Description |
|---|---|---|---|
call | object (CallControlResult) | yes | |
call.verb | hold · unhold · mute · unmute · hangup | no | |
call.call_id | string | no | |
call.audit_id | string (uuid) | no | The durable call_control_actions row. |
call.to_number | string | no | Masked last-4 (***0184) — the full E.164 never crosses this surface. |
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"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}422 — MidCallComplianceBlocked
The GATE_MID_CALL re-check refused the unhold: the consumer's consent was revoked, a written cease-and-desist is on file, or the frozen consumer timezone(s) are outside the quiet-hours window. The call STAYS on hold and an automatic hangup is scheduled (auto_hangup_at, C9) — a consumer is never kept on hold indefinitely past a compliance block. No decision_id: this is a cheap mid-call re-check, not an engine decision; the durable call_control_actions row is the evidence.
{
"error": {
"code": "compliance_blocked",
"message": "the mid-call compliance re-check refused this unhold (quiet_hours)",
"gate": "mid_call",
"reason_code": "quiet_hours",
"rule_id": "fdcpa.quiet_hours.v1",
"consumer_timezone": "America/Chicago",
"consumer_local_time": "2026-07-06T21:12:09-05:00",
"auto_hangup_at": "2026-07-07T02:17:09Z"
}
}429 — CallControlThrottled
The NR-18 request-rate brake refused this request (retryable next window). code rate_limited. Bounded per MOST SPECIFIC DURABLE principal (agent > user > tenant), never per tenant when a narrower one exists, so one hostile page cannot throttle its sibling agents. Durable means the budget survives re-authentication: logging back in mints a new token but does not buy a new budget. The four toggles share ONE budget; hangup is exempt and never answers 429, because refusing a teardown would leave a call alive that its operator asked to end. Refused BEFORE the call is looked up, so this answer is identical whether or not the call_id exists. No Retry-After header.
{
"error": {
"code": "rate_limited",
"message": "call control rate limit exceeded; retry later"
}
}502 — CallControlFailed
The switch rejected the control command (call already ended / leg gone). The audit row settles failed.
{
"error": {
"code": "call_control_failed",
"message": "the switch rejected the control command"
}
}503 — CallControlUnavailable
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 / timed out. Both retryable.
audit_unavailable
{
"error": {
"code": "audit_unavailable",
"message": "call-control audit unavailable, retry"
}
}esl_unavailable
{
"error": {
"code": "esl_unavailable",
"message": "telephony control unavailable, retry"
}
}Mute the agent microphone on a live call
POST /v1/calls/{call_id}/muteMutes the audio the agent reads into the bridge (their microphone), evidence-first. 409 when the call has no bridged agent leg.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
call_id | path | string (uuid) | yes | The live call's FreeSWITCH uuid (= call_attempts.call_uuid, also the call_id of the call.* floor events). |
Responses
200
Muted (audit row durable).
| Field | Type | Required | Description |
|---|---|---|---|
call | object (CallControlResult) | yes | |
call.verb | hold · unhold · mute · unmute · hangup | no | |
call.call_id | string | no | |
call.audit_id | string (uuid) | no | The durable call_control_actions row. |
call.to_number | string | no | Masked last-4 (***0184) — the full E.164 never crosses this surface. |
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"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}429 — CallControlThrottled
The NR-18 request-rate brake refused this request (retryable next window). code rate_limited. Bounded per MOST SPECIFIC DURABLE principal (agent > user > tenant), never per tenant when a narrower one exists, so one hostile page cannot throttle its sibling agents. Durable means the budget survives re-authentication: logging back in mints a new token but does not buy a new budget. The four toggles share ONE budget; hangup is exempt and never answers 429, because refusing a teardown would leave a call alive that its operator asked to end. Refused BEFORE the call is looked up, so this answer is identical whether or not the call_id exists. No Retry-After header.
{
"error": {
"code": "rate_limited",
"message": "call control rate limit exceeded; retry later"
}
}502 — CallControlFailed
The switch rejected the control command (call already ended / leg gone). The audit row settles failed.
{
"error": {
"code": "call_control_failed",
"message": "the switch rejected the control command"
}
}503 — CallControlUnavailable
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 / timed out. Both retryable.
audit_unavailable
{
"error": {
"code": "audit_unavailable",
"message": "call-control audit unavailable, retry"
}
}esl_unavailable
{
"error": {
"code": "esl_unavailable",
"message": "telephony control unavailable, retry"
}
}Unmute the agent microphone on a live call
POST /v1/calls/{call_id}/unmuteRestores the agent's microphone audio, evidence-first.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
call_id | path | string (uuid) | yes | The live call's FreeSWITCH uuid (= call_attempts.call_uuid, also the call_id of the call.* floor events). |
Responses
200
Unmuted (audit row durable).
| Field | Type | Required | Description |
|---|---|---|---|
call | object (CallControlResult) | yes | |
call.verb | hold · unhold · mute · unmute · hangup | no | |
call.call_id | string | no | |
call.audit_id | string (uuid) | no | The durable call_control_actions row. |
call.to_number | string | no | Masked last-4 (***0184) — the full E.164 never crosses this surface. |
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"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}429 — CallControlThrottled
The NR-18 request-rate brake refused this request (retryable next window). code rate_limited. Bounded per MOST SPECIFIC DURABLE principal (agent > user > tenant), never per tenant when a narrower one exists, so one hostile page cannot throttle its sibling agents. Durable means the budget survives re-authentication: logging back in mints a new token but does not buy a new budget. The four toggles share ONE budget; hangup is exempt and never answers 429, because refusing a teardown would leave a call alive that its operator asked to end. Refused BEFORE the call is looked up, so this answer is identical whether or not the call_id exists. No Retry-After header.
{
"error": {
"code": "rate_limited",
"message": "call control rate limit exceeded; retry later"
}
}502 — CallControlFailed
The switch rejected the control command (call already ended / leg gone). The audit row settles failed.
{
"error": {
"code": "call_control_failed",
"message": "the switch rejected the control command"
}
}503 — CallControlUnavailable
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 / timed out. Both retryable.
audit_unavailable
{
"error": {
"code": "audit_unavailable",
"message": "call-control audit unavailable, retry"
}
}esl_unavailable
{
"error": {
"code": "esl_unavailable",
"message": "telephony control unavailable, retry"
}
}Hang up a live call
POST /v1/calls/{call_id}/hangupTears the call down (kills the consumer leg), evidence-first. The CDR pipeline emits call.ended when the leg dies.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
call_id | path | string (uuid) | yes | The live call's FreeSWITCH uuid (= call_attempts.call_uuid, also the call_id of the call.* floor events). |
Responses
200
Hangup issued (audit row durable).
| Field | Type | Required | Description |
|---|---|---|---|
call | object (CallControlResult) | yes | |
call.verb | hold · unhold · mute · unmute · hangup | no | |
call.call_id | string | no | |
call.audit_id | string (uuid) | no | The durable call_control_actions row. |
call.to_number | string | no | Masked last-4 (***0184) — the full E.164 never crosses this surface. |
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"
}
}502 — CallControlFailed
The switch rejected the control command (call already ended / leg gone). The audit row settles failed.
{
"error": {
"code": "call_control_failed",
"message": "the switch rejected the control command"
}
}503 — CallControlUnavailable
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 / timed out. Both retryable.
audit_unavailable
{
"error": {
"code": "audit_unavailable",
"message": "call-control audit unavailable, retry"
}
}esl_unavailable
{
"error": {
"code": "esl_unavailable",
"message": "telephony control unavailable, retry"
}
}