Skip to content

Calls

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.

Live-call control verbs (hold/unhold/mute/unmute/hangup), evidence-first.

Endpoints

MethodPathSummary
GET/v1/callsCall 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}/holdPlace a live call on hold
POST/v1/calls/{call_id}/unholdRelease a held call (re-checks GATE_MID_CALL)
POST/v1/calls/{call_id}/muteMute the agent microphone on a live call
POST/v1/calls/{call_id}/unmuteUnmute the agent microphone on a live call
POST/v1/calls/{call_id}/hangupHang up a live call

Call state for the CTI surface (keyset-paginated, ?live=true for in-flight)

http
GET /v1/calls

The 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

NameInTypeRequiredDescription
campaign_idquerystring (uuid)no
debt_idquerystring (uuid)no
agent_idquerystring (uuid)no
dispositionquerystringnoExact 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.
livequerybooleannotrue → only calls still in flight (ended_at null; originals only). Strict boolean: anything else is 400.
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

One page of calls.

FieldTypeRequiredDescription
callsarray of CallAttemptyes
calls[].idstring (uuid)no
calls[].tenant_idstring (uuid)no
calls[].debt_idstring (uuid)no
calls[].campaign_idstring (uuid) | nullno
calls[].agent_idstring (uuid) | nullno
calls[].consumer_refstringno
calls[].debt_keystringno
calls[].call_uuidstring | nullno
calls[].from_numberstring | nullno
calls[].to_numberstringno
calls[].started_atstring (date-time) | nullno
calls[].answered_atstring (date-time) | nullno
calls[].ended_atstring (date-time) | nullno
calls[].dispositionstring | nullnoEngine dispositions: answered_human|abandoned|busy|no_answer|canceled|rejected|failed; typed agent/AI outcomes are free-form (e.g. promise_to_pay).
calls[].hangup_causestring | nullno
calls[].sip_response_codeinteger | nullno
calls[].compliance_snapshotobject | nullnoFrozen decision evidence at originate time.
calls[].amd_verdictstring | nullno
calls[].recording_uristring | nullnoWhere the call recording landed (object path/URI); null when the call was not recorded.
calls[].ai_usedbooleannoWhether the voice-AI bot ran on this call (the AI add-on meter key).
calls[].notestring | nullnoThe 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_idstring (uuid) | nullno
calls[].inserted_atstring (date-time)no
next_cursorstring | nullyesOpaque keyset cursor; null on the last page.

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"
  }
}

One call by its call_uuid

http
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

NameInTypeRequiredDescription
call_idpathstring (uuid)yesThe live call's FreeSWITCH uuid (= call_attempts.call_uuid, also the call_id of the call.* floor events).

Responses

200

The call.

FieldTypeRequiredDescription
callobject (CallAttempt)yesAppend-only CDR row. Originals have corrects_id: null; corrections point at the original — the LATEST correction is the final disposition.
call.idstring (uuid)no
call.tenant_idstring (uuid)no
call.debt_idstring (uuid)no
call.campaign_idstring (uuid) | nullno
call.agent_idstring (uuid) | nullno
call.consumer_refstringno
call.debt_keystringno
call.call_uuidstring | nullno
call.from_numberstring | nullno
call.to_numberstringno
call.started_atstring (date-time) | nullno
call.answered_atstring (date-time) | nullno
call.ended_atstring (date-time) | nullno
call.dispositionstring | nullnoEngine dispositions: answered_human|abandoned|busy|no_answer|canceled|rejected|failed; typed agent/AI outcomes are free-form (e.g. promise_to_pay).
call.hangup_causestring | nullno
call.sip_response_codeinteger | nullno
call.compliance_snapshotobject | nullnoFrozen decision evidence at originate time.
call.amd_verdictstring | nullno
call.recording_uristring | nullnoWhere the call recording landed (object path/URI); null when the call was not recorded.
call.ai_usedbooleannoWhether the voice-AI bot ran on this call (the AI add-on meter key).
call.notestring | nullnoThe 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_idstring (uuid) | nullno
call.inserted_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"
  }
}

Place a live call on hold

http
POST /v1/calls/{call_id}/hold

Holds 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

NameInTypeRequiredDescription
call_idpathstring (uuid)yesThe live call's FreeSWITCH uuid (= call_attempts.call_uuid, also the call_id of the call.* floor events).

Responses

200

Held (audit row durable).

FieldTypeRequiredDescription
callobject (CallControlResult)yes
call.verbhold · unhold · mute · unmute · hangupno
call.call_idstringno
call.audit_idstring (uuid)noThe durable call_control_actions row.
call.to_numberstringnoMasked 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).

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"
  }
}

409 — Conflict

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

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

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

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

json
{
  "error": {
    "code": "audit_unavailable",
    "message": "call-control audit unavailable, retry"
  }
}

esl_unavailable

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

Release a held call (re-checks GATE_MID_CALL)

http
POST /v1/calls/{call_id}/unhold

Releases 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

NameInTypeRequiredDescription
call_idpathstring (uuid)yesThe live call's FreeSWITCH uuid (= call_attempts.call_uuid, also the call_id of the call.* floor events).

Responses

200

Released (audit row durable).

FieldTypeRequiredDescription
callobject (CallControlResult)yes
call.verbhold · unhold · mute · unmute · hangupno
call.call_idstringno
call.audit_idstring (uuid)noThe durable call_control_actions row.
call.to_numberstringnoMasked 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).

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"
  }
}

409 — Conflict

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

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

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

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

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

json
{
  "error": {
    "code": "audit_unavailable",
    "message": "call-control audit unavailable, retry"
  }
}

esl_unavailable

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

Mute the agent microphone on a live call

http
POST /v1/calls/{call_id}/mute

Mutes the audio the agent reads into the bridge (their microphone), evidence-first. 409 when the call has no bridged agent leg.

Parameters

NameInTypeRequiredDescription
call_idpathstring (uuid)yesThe live call's FreeSWITCH uuid (= call_attempts.call_uuid, also the call_id of the call.* floor events).

Responses

200

Muted (audit row durable).

FieldTypeRequiredDescription
callobject (CallControlResult)yes
call.verbhold · unhold · mute · unmute · hangupno
call.call_idstringno
call.audit_idstring (uuid)noThe durable call_control_actions row.
call.to_numberstringnoMasked 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).

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"
  }
}

409 — Conflict

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

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

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

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

json
{
  "error": {
    "code": "audit_unavailable",
    "message": "call-control audit unavailable, retry"
  }
}

esl_unavailable

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

Unmute the agent microphone on a live call

http
POST /v1/calls/{call_id}/unmute

Restores the agent's microphone audio, evidence-first.

Parameters

NameInTypeRequiredDescription
call_idpathstring (uuid)yesThe live call's FreeSWITCH uuid (= call_attempts.call_uuid, also the call_id of the call.* floor events).

Responses

200

Unmuted (audit row durable).

FieldTypeRequiredDescription
callobject (CallControlResult)yes
call.verbhold · unhold · mute · unmute · hangupno
call.call_idstringno
call.audit_idstring (uuid)noThe durable call_control_actions row.
call.to_numberstringnoMasked 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).

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"
  }
}

409 — Conflict

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

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

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

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

json
{
  "error": {
    "code": "audit_unavailable",
    "message": "call-control audit unavailable, retry"
  }
}

esl_unavailable

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

Hang up a live call

http
POST /v1/calls/{call_id}/hangup

Tears the call down (kills the consumer leg), evidence-first. The CDR pipeline emits call.ended when the leg dies.

Parameters

NameInTypeRequiredDescription
call_idpathstring (uuid)yesThe 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).

FieldTypeRequiredDescription
callobject (CallControlResult)yes
call.verbhold · unhold · mute · unmute · hangupno
call.call_idstringno
call.audit_idstring (uuid)noThe durable call_control_actions row.
call.to_numberstringnoMasked 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).

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"
  }
}

502 — CallControlFailed

The switch rejected the control command (call already ended / leg gone). The audit row settles failed.

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

json
{
  "error": {
    "code": "audit_unavailable",
    "message": "call-control audit unavailable, retry"
  }
}

esl_unavailable

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

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