Skip to content

Calls ​

Generated from DialerDigital/core commit 0fc18008b3c8 — spec openapi/dialer-v1.yaml v0.134.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.

Español

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
POST/v1/calls/{call_id}/transferBlind-transfer a live call to an internal or external destination
POST/v1/calls/{call_id}/transfer/attended/startOpen an attended-transfer consultation
POST/v1/calls/{call_id}/transfer/attended/completeComplete the attended transfer — hand the call to the destination
POST/v1/calls/{call_id}/transfer/attended/cancelCancel the consultation — the call stays with its agent
POST/v1/calls/{call_id}/conference/addBring a colleague, or a registered external number, into the call (three-way conference)
POST/v1/calls/{call_id}/conference/removeTake the colleague out of the 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. Only the agent principal also gets debtor_name on each call (the widget's call card); see the Call schema.

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 engine values — answered_human, abandoned, delivered, ai_conversation, answered_machine, ai_stream_lost, 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 Callyes
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). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.

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

403 — Forbidden ​

The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.

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

429 — ApiKeyRateLimited ​

This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.

json
{
  "error": {
    "code": "rate_limited",
    "message": "API key request quota exceeded; retry after the interval in Retry-After"
  }
}

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. It also carries transfer_chain, the legs of the call in order (core#2403) — [] when nobody transferred it; a chain that cannot be read is 503, never [].

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
callanyyesCall plus transfer_chain (core#2403): the legs of the call in order, [] when nobody transferred it.

401 — Unauthorized ​

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.

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

403 — Forbidden ​

The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.

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

429 — ApiKeyRateLimited ​

This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.

json
{
  "error": {
    "code": "rate_limited",
    "message": "API key request quota exceeded; retry after the interval in Retry-After"
  }
}

503 ​

The evidence could not be read; retry. Fail-closed — never an empty answer.

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 · hangup · blind_transfer · attended_transfer_start · attended_transfer_complete · attended_transfer_cancelno
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.
call.bridge_confirm_bystring,null (date-time)noattended_transfer_complete only. When core checks that the consumer is bridged to the destination; a consumer still unbridged then is hung up with cause NORMAL_UNSPECIFIED. null when that check could not be armed.

401 — Unauthorized ​

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.

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. Every braked verb — the four toggles and the blind transfer — shares 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. Retry-After is the time left in the fixed window.

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 · hangup · blind_transfer · attended_transfer_start · attended_transfer_complete · attended_transfer_cancelno
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.
call.bridge_confirm_bystring,null (date-time)noattended_transfer_complete only. When core checks that the consumer is bridged to the destination; a consumer still unbridged then is hung up with cause NORMAL_UNSPECIFIED. null when that check could not be armed.

401 — Unauthorized ​

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.

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. Every braked verb — the four toggles and the blind transfer — shares 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. Retry-After is the time left in the fixed window.

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 · hangup · blind_transfer · attended_transfer_start · attended_transfer_complete · attended_transfer_cancelno
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.
call.bridge_confirm_bystring,null (date-time)noattended_transfer_complete only. When core checks that the consumer is bridged to the destination; a consumer still unbridged then is hung up with cause NORMAL_UNSPECIFIED. null when that check could not be armed.

401 — Unauthorized ​

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.

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. Every braked verb — the four toggles and the blind transfer — shares 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. Retry-After is the time left in the fixed window.

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 · hangup · blind_transfer · attended_transfer_start · attended_transfer_complete · attended_transfer_cancelno
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.
call.bridge_confirm_bystring,null (date-time)noattended_transfer_complete only. When core checks that the consumer is bridged to the destination; a consumer still unbridged then is hung up with cause NORMAL_UNSPECIFIED. null when that check could not be armed.

401 — Unauthorized ​

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.

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. Every braked verb — the four toggles and the blind transfer — shares 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. Retry-After is the time left in the fixed window.

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 · hangup · blind_transfer · attended_transfer_start · attended_transfer_complete · attended_transfer_cancelno
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.
call.bridge_confirm_bystring,null (date-time)noattended_transfer_complete only. When core checks that the consumer is bridged to the destination; a consumer still unbridged then is hung up with cause NORMAL_UNSPECIFIED. null when that check could not be armed.

401 — Unauthorized ​

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.

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

Blind-transfer a live call to an internal or external destination ​

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

Moves the CONSUMER leg to another destination (uuid_transfer), evidence-first; the origin agent's leg dies with the broken bridge. The call is handed over immediately — there is no consultation leg and no way back — when the agent needs to speak to the destination first, that is the attended transfer below.

destination is one of THREE classes, each gated on its own terms:

  • a roster sip_extension or an agent id (UUID) of an ACTIVE agent of the caller's tenant whose seat is FREE right now;
  • a roster extension with no agent behind it;
  • an EXTERNAL PSTN number in strict E.164 (+<country><number>), or the id of one of the tenant's CURRENT registered external destinations — core resolves it to its E.164 and it is gated exactly as that number (GET /v1/transfer/targets lists them WITHOUT the number, which is what a widget picker sends).

The roster is consulted FIRST, so a tenant whose extensions happen to be 10 digits long still transfers internally. A number-shaped string that is not E.164 is refused 422 destination_not_supported: the jurisdiction that decides whether the new leg may be placed is resolved FROM the destination number, and a national string with no country cannot answer it.

An external destination must be REGISTERED first (POST /v1/transfer-destinations). Any other E.164 is refused 422compliance_blocked, gate: pre_dial, rule_idtransfer.destination_unregistered.v1, before the chain below runs and with no decision_id (the engine never ran). A registry that cannot be read also blocks (transfer.destination_registry_unavailable.v1). A registered destination still passes the whole chain below.

An external transfer ORIGINATES A NEW LEG, so it passes the same GATE_PRE_DIAL chain a dial does (consent, DNC, quiet hours, Reg F, state matrix, disclosure). It is evaluated as what it is — a call to the DESTINATION: DNC reads the destination's suppression status and quiet hours resolve the destination's own NPA. A block answers 422compliance_blocked with gate: pre_dial and the engine decision_id; nothing is dialled. A non-NANP E.164 blocks: a USA cell has no quiet-hours window for it.

What each gate of that chain asks of the destination, and what lets it through:

  • Consent (tcpa.consent.v1). A consent revocation on file for the number blocks (consent_revoked). Otherwise prior express consent is required, as for a mobile, unless one of three things holds: consent on file for the number; the line_type the tenant DECLARED for the registered destination is landline; or the tenant's human-initiated posture (human_initiated_enabled, OFF by default, admin-only) is on. unknown — the default of a registration — mobile and voip require it (tcpa_no_consent).
  • DNC (dnc.lists.v1 and the Reassigned Numbers Database). The destination must be on no suppression list, and the RND must answer that it was not reassigned since the registration instant. The declared line_type changes nothing here.
  • Quiet hours — the destination's own window, from its NPA.
  • Reg F and the state matrix read the attempt history of the request's consumer and debt, which this leg leaves EMPTY: the party at the other end is not the debtor.
  • Disclosure and artificial voice — nothing prerecorded or synthetic plays on a transfer, so they have nothing to require.

Caller-ID on an external leg is the DID this call already presents — never chosen at transfer time, because caller-ID is round-robin over the pool the operator configured. A call with no tenant DID to present is refused 422 caller_id_unavailable rather than dialled with the carrier gateway's default number.

The external leg is its own call attempt, billed only if it bridges. Before the switch is touched the leg gets its own call_attempts row (no debtor on it: it never counts against the debtor's Reg F attempts) and its own listener. What is charged is outbound_minutes from the leg's CHANNEL_BRIDGE to its hangup, rated in Decimal like any dial — never from the uuid_transfer acknowledgement, which comes before the far end rings. A leg that never bridged closes with no answered_at and costs nothing.

A house leg leaves by a carrier the LCR chose, metered against that carrier's contracted call rate. The same route selection a dial uses picks the leg's carrier; the route is recorded with the leg's attempt and travels to the edge as the carrier to use. A house leg with no route (routing switched off, no candidate carrier) is refused 503 no_route, and a carrier already at its contracted calls-per-second refuses it 429carrier_cps. Both happen before anything is written or dialled, so the consumer stays with their agent. A BYOC tenant's leg goes out through its own trunk, with no route and no carrier meter.

Gated like unhold on the CONSUMER's side too: the call comes back to live audio, so the GATE_MID_CALL trio (consent revocation, cease-and-desist, quiet hours) is re-checked BEFORE the switch is touched and refuses with 422 compliance_blocked, gate: mid_call. And because this phase does NOT move the agent seat, an INTERNAL destination whose seat is busy is refused 422destination_unavailable rather than double-booked.

On success a control_handoff row is stamped: it names the DESTINATION agent for an internal transfer (from then on the ownership gate authorizes that agent and refuses the origin), and carries no agent for an external one — control left the agent population, so no agent-scoped principal may drive the call afterwards. The call's agent_id is never touched; it keeps saying where the call started.

Parameters

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

Request body (JSON, required)

FieldTypeRequiredDescription
destinationstringyesInternal sip_extension / agent id, or an external destination in strict E.164 (+<country><number>, 8–15 digits) or by its registration id. The server TRIMS the value before validating it and requires it to be non-blank and at most 64 BYTES once trimmed — minLength/ maxLength here count CHARACTERS on the raw value, so a blank-only string satisfies them and still earns a 400. A number-shaped value that is NOT E.164 (bare national digits, a malformed + string) is refused 422 destination_not_supported.

Responses

200 ​

Transfer issued (audit row durable, handoff stamped).

FieldTypeRequiredDescription
callobject (CallControlResult)yes
call.verbhold · unhold · mute · unmute · hangup · blind_transfer · attended_transfer_start · attended_transfer_complete · attended_transfer_cancelno
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.
call.bridge_confirm_bystring,null (date-time)noattended_transfer_complete only. When core checks that the consumer is bridged to the destination; a consumer still unbridged then is hung up with cause NORMAL_UNSPECIFIED. null when that check could not be armed.

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). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.

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

402 ​

The tenant's plan is not active (or could not be read — fail-closed). An EXTERNAL transfer buys carrier minutes exactly like a dial, so it passes the same plan traffic gate a dial does (ADR #39). An INTERNAL transfer never earns this: moving a call between two agents of the same tenant buys nothing from a carrier.

json
{
  "error": {
    "code": "plan_inactive",
    "message": "tenant plan is not active; traffic is gated"
  }
}

403 — Forbidden ​

The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.

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

422 — TransferRefused ​

The blind transfer never reached the switch. Three families of refusal share this status, and gate (when present) says which checkpoint refused.

The GATE_MID_CALL re-check refused it — code compliance_blocked, gate mid_call, with the same envelope unhold returns (reason_code, rule_id, auto_hangup_at): consent revoked, a written cease-and-desist on file, or the frozen consumer timezone(s) outside the quiet-hours window. A transfer puts the consumer back on live audio just like an unhold, so it is gated like one; otherwise a blocked unhold could be walked around by transferring the same call. The C9 auto-hangup is armed.

The GATE_PRE_DIAL chain refused the NEW LEG — codecompliance_blocked, gate pre_dial, with the engine decision_id and rule_id. Only an EXTERNAL destination can earn this: the leg it would create is an origination and passes the same chain a dial does, evaluated against the DESTINATION number (its DNC status, its NPA's quiet-hours window). NOTHING was dialled, NOTHING is armed and the consumer's own call is untouched — they are still talking to their agent. reason_code engine_unavailable is the fail-closed answer when the gate could not be evaluated at all: a gate that cannot decide blocks. reason_code destination_unregistered (rule_idtransfer.destination_unregistered.v1) is the refusal of an E.164 the tenant never registered in /v1/transfer-destinations, and destination_registry_unavailable the fail-closed answer when that registry could not be read; both come before the chain, with no decision_id. reason_code destination_not_allowed (rule_idegress.destination_not_allowed.v1) is the refusal of an E.164 outside the destination allowlist of a BYOC trunk of the sede (core#1281), and destination_allowlist_unavailable (egress.destination_allowlist_unavailable.v1) the fail-closed answer when that allowlist could not be read; both come before the registry, with decision_id when their gate_blocks row was written.

Or the destination itself was refused — envelope ErrorEnvelope. code destination_not_supported: the destination is number-shaped but not strict E.164 (bare national digits, a malformed + string). External destinations are dialled from E.164 only, because jurisdiction is resolved from that number. code destination_not_found: anything else that does not resolve to an active roster agent of THIS tenant. The split is decided by the SHAPE of what was sent, never by a second lookup, so neither answer says whether the destination exists in another tenant. code destination_unavailable: the destination IS an active agent of this tenant, but their seat is not :available right now (ringing, on a call, in wrap-up, paused, or not checked in). This phase does not move the seat, so transferring onto a busy agent would double-book them; this answer only ever names an agent of the caller's OWN roster, so it is not a cross-tenant oracle either. codecaller_id_unavailable: an EXTERNAL transfer on a call that has no tenant DID to present — refused rather than dialled with the carrier gateway's default ANI, which the tenant may not own. code did_unverified (core#2493): the DID the external leg would present (on an inbound call, the number the consumer dialled) is a customer DID whose ownership was never verified, and the tenant key did_ownership_proof is enforce. With observe the leg goes out and dialer_dids_unverified_use_total{surface="voice_transfer"} counts it; with off nothing changes. Applies to an external blind transfer and to an external conference add.

In every case the call_control_actions row settles failed with the requested destination recorded; ZERO ESL commands ran and the call is untouched.

Or the consultation was never taken (attended transfer only) — code consultation_unanswered: the destination is not registered, is busy, or did not answer the consultation leg. It is a 422 and not a 502 because the switch worked and a person did not pick up. THE HOLD HAS ALREADY BEEN RELEASED: the consumer is back with their agent before this response is written, and no consultation is left open.

Or the conference leg was never taken (conference add only) — codeconference_unanswered: the colleague is not registered, is busy, or did not answer. A 422 and not a 502 for the same reason; the call continues exactly as it was and no conference is left open.

destination_not_found

json
{
  "error": {
    "code": "destination_not_found",
    "message": "no active agent with that destination in this tenant"
  }
}

destination_not_supported

json
{
  "error": {
    "code": "destination_not_supported",
    "message": "an external transfer destination must be E.164 (+<country><number>)"
  }
}

destination_unavailable

json
{
  "error": {
    "code": "destination_unavailable",
    "message": "that agent is not available to take a transfer right now"
  }
}

caller_id_unavailable

json
{
  "error": {
    "code": "caller_id_unavailable",
    "message": "this call has no tenant caller-ID to present on a transferred leg"
  }
}

did_unverified

json
{
  "error": {
    "code": "did_unverified",
    "message": "the caller-ID this transferred leg would present is not a verified number of the tenant"
  }
}

compliance_blocked_mid_call

json
{
  "error": {
    "code": "compliance_blocked",
    "message": "the mid-call compliance re-check refused this blind_transfer (consent_revoked)",
    "gate": "mid_call",
    "reason_code": "consent_revoked",
    "rule_id": "regf.revocation.v1"
  }
}

compliance_blocked_pre_dial

json
{
  "error": {
    "code": "compliance_blocked",
    "message": "the origination gate refused the leg this blind_transfer would create (dnc_listed)",
    "gate": "pre_dial",
    "reason_code": "dnc_listed",
    "rule_id": "dnc.lists.v1",
    "decision_id": "018f3f2a-0000-7000-8000-000000000000"
  }
}

429 — TransferThrottled ​

code rate_limited: the NR-18 request-rate brake, exactly as CallControlThrottled describes it (refused before the call is looked up). code carrier_cps: an EXTERNAL transfer whose leg would leave by a carrier that is already at its contracted calls-per-second this second. Nothing was written or dialled and the consumer is still with their agent. Retryable within a second. rate_limited carries Retry-After (time left in the fixed window); carrier_cps does not. code tenant_channels / tenant_cps (conference add to an EXTERNAL number only): the third party is one more channel of the tenant, which is at its channel ceiling or call-rate window; nothing was dialled and the call goes on with the people already on it.

rate_limited

json
{
  "error": {
    "code": "rate_limited",
    "message": "call control rate limit exceeded; retry later"
  }
}

carrier_cps

json
{
  "error": {
    "code": "carrier_cps",
    "message": "the carrier for the transferred leg is at its contracted call rate; retry"
  }
}

tenant_channels

json
{
  "error": {
    "code": "tenant_channels",
    "message": "the tenant is at its channel or call-rate ceiling; the third party was not dialled"
  }
}

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 — TransferUnavailable ​

The four 503 codes of a blind transfer. audit_unavailable and esl_unavailable are the ones CallControlUnavailable describes. Two more apply to an EXTERNAL destination only. no_route: the house leg has no directed carrier route (routing switched off, no candidate carrier), so it is not placed, because house traffic is always directed. capacity_unavailable: the capacity gate could not admit the leg's carrier, and it fails closed. In both cases nothing was written or dialled.

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

no_route

json
{
  "error": {
    "code": "no_route",
    "message": "no directed carrier route for the transferred leg; nothing was dialled"
  }
}

capacity_unavailable

json
{
  "error": {
    "code": "capacity_unavailable",
    "message": "the capacity gate could not admit the transferred leg, retry"
  }
}

Open an attended-transfer consultation ​

http
POST /v1/calls/{call_id}/transfer/attended/start

Puts the CONSUMER on HOLD MUSIC and opens a private consultation leg to the destination so the agent can speak to them BEFORE handing the call over. Nothing is transferred yet and no bridge is broken: the consumer stays bridged to their agent for the whole consultation, which is what makes .../cancel a restoration rather than a repair.

WHAT EACH PARTY HEARS, stated rather than implied. The consumer hears hold music — the same MOH the hold verb streams, not silence. The destination is audible to the AGENT ONLY (the consultation leg is the supervision whisper leg), so nothing said during the consultation reaches the consumer. The destination, being on a leg that hears the call, can hear a consumer who keeps talking: that is the consultation working as intended, and it is stated here so it is not discovered.

destination is validated exactly like the blind transfer's: an ACTIVE agent of THIS tenant, by roster extension or agent id, whose seat is free right now. External/PSTN-shaped destinations are refused 422destination_not_supported (this phase is internal-only).

NOT gated by GATE_MID_CALL, deliberately: this verb only ever takes the consumer's audio AWAY, exactly like hold. The gate runs on .../complete and .../cancel, which are the verbs that put them back on live audio.

ONE CONSULTATION PER CALL: a second start while one is open is 409consultation_in_progress. If the destination does not take the consultation leg, the hold is RELEASED before the 422consultation_unanswered is returned — the consumer is back with their agent before the caller reads the error.

Parameters

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

Request body (JSON, required)

FieldTypeRequiredDescription
destinationstringyesInternal sip_extension / agent id, or an external destination in strict E.164 (+<country><number>, 8–15 digits) or by its registration id. The server TRIMS the value before validating it and requires it to be non-blank and at most 64 BYTES once trimmed — minLength/ maxLength here count CHARACTERS on the raw value, so a blank-only string satisfies them and still earns a 400. A number-shaped value that is NOT E.164 (bare national digits, a malformed + string) is refused 422 destination_not_supported.

Responses

200 ​

Consultation open (audit row durable, consultation leg up).

FieldTypeRequiredDescription
callobject (CallControlResult)yes
call.verbhold · unhold · mute · unmute · hangup · blind_transfer · attended_transfer_start · attended_transfer_complete · attended_transfer_cancelno
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.
call.bridge_confirm_bystring,null (date-time)noattended_transfer_complete only. When core checks that the consumer is bridged to the destination; a consumer still unbridged then is hung up with cause NORMAL_UNSPECIFIED. null when that check could not be armed.

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). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.

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

403 — Forbidden ​

The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.

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 — TransferRefused ​

The blind transfer never reached the switch. Three families of refusal share this status, and gate (when present) says which checkpoint refused.

The GATE_MID_CALL re-check refused it — code compliance_blocked, gate mid_call, with the same envelope unhold returns (reason_code, rule_id, auto_hangup_at): consent revoked, a written cease-and-desist on file, or the frozen consumer timezone(s) outside the quiet-hours window. A transfer puts the consumer back on live audio just like an unhold, so it is gated like one; otherwise a blocked unhold could be walked around by transferring the same call. The C9 auto-hangup is armed.

The GATE_PRE_DIAL chain refused the NEW LEG — codecompliance_blocked, gate pre_dial, with the engine decision_id and rule_id. Only an EXTERNAL destination can earn this: the leg it would create is an origination and passes the same chain a dial does, evaluated against the DESTINATION number (its DNC status, its NPA's quiet-hours window). NOTHING was dialled, NOTHING is armed and the consumer's own call is untouched — they are still talking to their agent. reason_code engine_unavailable is the fail-closed answer when the gate could not be evaluated at all: a gate that cannot decide blocks. reason_code destination_unregistered (rule_idtransfer.destination_unregistered.v1) is the refusal of an E.164 the tenant never registered in /v1/transfer-destinations, and destination_registry_unavailable the fail-closed answer when that registry could not be read; both come before the chain, with no decision_id. reason_code destination_not_allowed (rule_idegress.destination_not_allowed.v1) is the refusal of an E.164 outside the destination allowlist of a BYOC trunk of the sede (core#1281), and destination_allowlist_unavailable (egress.destination_allowlist_unavailable.v1) the fail-closed answer when that allowlist could not be read; both come before the registry, with decision_id when their gate_blocks row was written.

Or the destination itself was refused — envelope ErrorEnvelope. code destination_not_supported: the destination is number-shaped but not strict E.164 (bare national digits, a malformed + string). External destinations are dialled from E.164 only, because jurisdiction is resolved from that number. code destination_not_found: anything else that does not resolve to an active roster agent of THIS tenant. The split is decided by the SHAPE of what was sent, never by a second lookup, so neither answer says whether the destination exists in another tenant. code destination_unavailable: the destination IS an active agent of this tenant, but their seat is not :available right now (ringing, on a call, in wrap-up, paused, or not checked in). This phase does not move the seat, so transferring onto a busy agent would double-book them; this answer only ever names an agent of the caller's OWN roster, so it is not a cross-tenant oracle either. codecaller_id_unavailable: an EXTERNAL transfer on a call that has no tenant DID to present — refused rather than dialled with the carrier gateway's default ANI, which the tenant may not own. code did_unverified (core#2493): the DID the external leg would present (on an inbound call, the number the consumer dialled) is a customer DID whose ownership was never verified, and the tenant key did_ownership_proof is enforce. With observe the leg goes out and dialer_dids_unverified_use_total{surface="voice_transfer"} counts it; with off nothing changes. Applies to an external blind transfer and to an external conference add.

In every case the call_control_actions row settles failed with the requested destination recorded; ZERO ESL commands ran and the call is untouched.

Or the consultation was never taken (attended transfer only) — code consultation_unanswered: the destination is not registered, is busy, or did not answer the consultation leg. It is a 422 and not a 502 because the switch worked and a person did not pick up. THE HOLD HAS ALREADY BEEN RELEASED: the consumer is back with their agent before this response is written, and no consultation is left open.

Or the conference leg was never taken (conference add only) — codeconference_unanswered: the colleague is not registered, is busy, or did not answer. A 422 and not a 502 for the same reason; the call continues exactly as it was and no conference is left open.

destination_not_found

json
{
  "error": {
    "code": "destination_not_found",
    "message": "no active agent with that destination in this tenant"
  }
}

destination_not_supported

json
{
  "error": {
    "code": "destination_not_supported",
    "message": "an external transfer destination must be E.164 (+<country><number>)"
  }
}

destination_unavailable

json
{
  "error": {
    "code": "destination_unavailable",
    "message": "that agent is not available to take a transfer right now"
  }
}

caller_id_unavailable

json
{
  "error": {
    "code": "caller_id_unavailable",
    "message": "this call has no tenant caller-ID to present on a transferred leg"
  }
}

did_unverified

json
{
  "error": {
    "code": "did_unverified",
    "message": "the caller-ID this transferred leg would present is not a verified number of the tenant"
  }
}

compliance_blocked_mid_call

json
{
  "error": {
    "code": "compliance_blocked",
    "message": "the mid-call compliance re-check refused this blind_transfer (consent_revoked)",
    "gate": "mid_call",
    "reason_code": "consent_revoked",
    "rule_id": "regf.revocation.v1"
  }
}

compliance_blocked_pre_dial

json
{
  "error": {
    "code": "compliance_blocked",
    "message": "the origination gate refused the leg this blind_transfer would create (dnc_listed)",
    "gate": "pre_dial",
    "reason_code": "dnc_listed",
    "rule_id": "dnc.lists.v1",
    "decision_id": "018f3f2a-0000-7000-8000-000000000000"
  }
}

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. Every braked verb — the four toggles and the blind transfer — shares 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. Retry-After is the time left in the fixed window.

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

Complete the attended transfer — hand the call to the destination ​

http
POST /v1/calls/{call_id}/transfer/attended/complete

Hangs the consultation leg up and moves the CONSUMER leg to the destination, the same uuid_transfer the blind transfer runs. A control_handoff is stamped naming the DESTINATION agent: from that row on the ownership gate authorizes them and refuses the origin agent, without touching the call's agent_id.

NO BODY. The destination comes from the OPEN consultation, never from the request, so a caller cannot consult with one agent and complete to another. It IS re-resolved: a destination deactivated since start is refused 422 rather than handed a call.

Gated like the blind transfer (the consumer lands on live audio with a new party), so the GATE_MID_CALL trio runs BEFORE the switch is touched and refuses 422 compliance_blocked with the consultation left open. .../cancel runs the SAME gate, so it is refused too while the block lasts: the consumer stays on hold with the C9 auto-hangup armed, exactly as a blocked unhold leaves them.

IF THE TRANSFER ITSELF FAILS the hold is released and the consumer goes back to the agent who was already attending them — never to nobody — and no handoff is stamped for a transfer that did not happen.

200 MEANS THE SWITCH TOOK THE TRANSFER, NOT THAT THE DESTINATION ANSWERED. The origin agent's leg is gone by then, so the response carries call.bridge_confirm_by: the instant by which the consumer is bridged to the destination, or has hung up, or is hung up by core with cause NORMAL_UNSPECIFIED (in the CDR and the call.ended webhook, where the call still closes as answered) and an audit row hangup / attended_transfer_unbridged. It is never left on the line alone. null means core could not arm that check; the audit row of this complete then says bridge_net_unarmed.

409 no_consultation when there is no open consultation on this call.

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 ​

Transfer issued (handoff stamped, consultation closed, bridge check armed).

FieldTypeRequiredDescription
callobject (CallControlResult)yes
call.verbhold · unhold · mute · unmute · hangup · blind_transfer · attended_transfer_start · attended_transfer_complete · attended_transfer_cancelno
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.
call.bridge_confirm_bystring,null (date-time)noattended_transfer_complete only. When core checks that the consumer is bridged to the destination; a consumer still unbridged then is hung up with cause NORMAL_UNSPECIFIED. null when that check could not be armed.

401 — Unauthorized ​

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.

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

403 — Forbidden ​

The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.

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 — TransferRefused ​

The blind transfer never reached the switch. Three families of refusal share this status, and gate (when present) says which checkpoint refused.

The GATE_MID_CALL re-check refused it — code compliance_blocked, gate mid_call, with the same envelope unhold returns (reason_code, rule_id, auto_hangup_at): consent revoked, a written cease-and-desist on file, or the frozen consumer timezone(s) outside the quiet-hours window. A transfer puts the consumer back on live audio just like an unhold, so it is gated like one; otherwise a blocked unhold could be walked around by transferring the same call. The C9 auto-hangup is armed.

The GATE_PRE_DIAL chain refused the NEW LEG — codecompliance_blocked, gate pre_dial, with the engine decision_id and rule_id. Only an EXTERNAL destination can earn this: the leg it would create is an origination and passes the same chain a dial does, evaluated against the DESTINATION number (its DNC status, its NPA's quiet-hours window). NOTHING was dialled, NOTHING is armed and the consumer's own call is untouched — they are still talking to their agent. reason_code engine_unavailable is the fail-closed answer when the gate could not be evaluated at all: a gate that cannot decide blocks. reason_code destination_unregistered (rule_idtransfer.destination_unregistered.v1) is the refusal of an E.164 the tenant never registered in /v1/transfer-destinations, and destination_registry_unavailable the fail-closed answer when that registry could not be read; both come before the chain, with no decision_id. reason_code destination_not_allowed (rule_idegress.destination_not_allowed.v1) is the refusal of an E.164 outside the destination allowlist of a BYOC trunk of the sede (core#1281), and destination_allowlist_unavailable (egress.destination_allowlist_unavailable.v1) the fail-closed answer when that allowlist could not be read; both come before the registry, with decision_id when their gate_blocks row was written.

Or the destination itself was refused — envelope ErrorEnvelope. code destination_not_supported: the destination is number-shaped but not strict E.164 (bare national digits, a malformed + string). External destinations are dialled from E.164 only, because jurisdiction is resolved from that number. code destination_not_found: anything else that does not resolve to an active roster agent of THIS tenant. The split is decided by the SHAPE of what was sent, never by a second lookup, so neither answer says whether the destination exists in another tenant. code destination_unavailable: the destination IS an active agent of this tenant, but their seat is not :available right now (ringing, on a call, in wrap-up, paused, or not checked in). This phase does not move the seat, so transferring onto a busy agent would double-book them; this answer only ever names an agent of the caller's OWN roster, so it is not a cross-tenant oracle either. codecaller_id_unavailable: an EXTERNAL transfer on a call that has no tenant DID to present — refused rather than dialled with the carrier gateway's default ANI, which the tenant may not own. code did_unverified (core#2493): the DID the external leg would present (on an inbound call, the number the consumer dialled) is a customer DID whose ownership was never verified, and the tenant key did_ownership_proof is enforce. With observe the leg goes out and dialer_dids_unverified_use_total{surface="voice_transfer"} counts it; with off nothing changes. Applies to an external blind transfer and to an external conference add.

In every case the call_control_actions row settles failed with the requested destination recorded; ZERO ESL commands ran and the call is untouched.

Or the consultation was never taken (attended transfer only) — code consultation_unanswered: the destination is not registered, is busy, or did not answer the consultation leg. It is a 422 and not a 502 because the switch worked and a person did not pick up. THE HOLD HAS ALREADY BEEN RELEASED: the consumer is back with their agent before this response is written, and no consultation is left open.

Or the conference leg was never taken (conference add only) — codeconference_unanswered: the colleague is not registered, is busy, or did not answer. A 422 and not a 502 for the same reason; the call continues exactly as it was and no conference is left open.

destination_not_found

json
{
  "error": {
    "code": "destination_not_found",
    "message": "no active agent with that destination in this tenant"
  }
}

destination_not_supported

json
{
  "error": {
    "code": "destination_not_supported",
    "message": "an external transfer destination must be E.164 (+<country><number>)"
  }
}

destination_unavailable

json
{
  "error": {
    "code": "destination_unavailable",
    "message": "that agent is not available to take a transfer right now"
  }
}

caller_id_unavailable

json
{
  "error": {
    "code": "caller_id_unavailable",
    "message": "this call has no tenant caller-ID to present on a transferred leg"
  }
}

did_unverified

json
{
  "error": {
    "code": "did_unverified",
    "message": "the caller-ID this transferred leg would present is not a verified number of the tenant"
  }
}

compliance_blocked_mid_call

json
{
  "error": {
    "code": "compliance_blocked",
    "message": "the mid-call compliance re-check refused this blind_transfer (consent_revoked)",
    "gate": "mid_call",
    "reason_code": "consent_revoked",
    "rule_id": "regf.revocation.v1"
  }
}

compliance_blocked_pre_dial

json
{
  "error": {
    "code": "compliance_blocked",
    "message": "the origination gate refused the leg this blind_transfer would create (dnc_listed)",
    "gate": "pre_dial",
    "reason_code": "dnc_listed",
    "rule_id": "dnc.lists.v1",
    "decision_id": "018f3f2a-0000-7000-8000-000000000000"
  }
}

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. Every braked verb — the four toggles and the blind transfer — shares 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. Retry-After is the time left in the fixed window.

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

Cancel the consultation — the call stays with its agent ​

http
POST /v1/calls/{call_id}/transfer/attended/cancel

Hangs the consultation leg up and takes the consumer off hold. THE CALL IS LEFT EXACTLY AS IT WAS: the consumer never left the bridge, so this is a restoration by construction, not a rebuild — the music stops and they are back with the agent who was already attending them.

NO HANDOFF IS STAMPED, and that is the point rather than an omission: control never MOVED during a consultation (.../start deliberately does not stamp), so the origin agent kept the call throughout and there is nothing to give back. The attended_transfer_cancel audit row is the durable evidence that the consultation ended.

NO BODY. Gated like unhold (the consumer returns to live audio), so a 422 compliance_blocked leaves them on hold with the C9 auto-hangup armed, exactly as a blocked unhold does.

A consultation leg that is already gone (the destination hung up) does NOT fail this verb: tearing it down is best-effort, because the verb's real work is giving the consumer their agent back. If the ORIGIN AGENT hung up during the consultation there is no leg to take off hold and no agent to return to, so this answers 409 not_bridged — completing the transfer is then the safe way out.

409 no_consultation when there is no open consultation on this call.

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 ​

Consultation cancelled; the call continues with its agent.

FieldTypeRequiredDescription
callobject (CallControlResult)yes
call.verbhold · unhold · mute · unmute · hangup · blind_transfer · attended_transfer_start · attended_transfer_complete · attended_transfer_cancelno
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.
call.bridge_confirm_bystring,null (date-time)noattended_transfer_complete only. When core checks that the consumer is bridged to the destination; a consumer still unbridged then is hung up with cause NORMAL_UNSPECIFIED. null when that check could not be armed.

401 — Unauthorized ​

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.

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

403 — Forbidden ​

The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.

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 — TransferRefused ​

The blind transfer never reached the switch. Three families of refusal share this status, and gate (when present) says which checkpoint refused.

The GATE_MID_CALL re-check refused it — code compliance_blocked, gate mid_call, with the same envelope unhold returns (reason_code, rule_id, auto_hangup_at): consent revoked, a written cease-and-desist on file, or the frozen consumer timezone(s) outside the quiet-hours window. A transfer puts the consumer back on live audio just like an unhold, so it is gated like one; otherwise a blocked unhold could be walked around by transferring the same call. The C9 auto-hangup is armed.

The GATE_PRE_DIAL chain refused the NEW LEG — codecompliance_blocked, gate pre_dial, with the engine decision_id and rule_id. Only an EXTERNAL destination can earn this: the leg it would create is an origination and passes the same chain a dial does, evaluated against the DESTINATION number (its DNC status, its NPA's quiet-hours window). NOTHING was dialled, NOTHING is armed and the consumer's own call is untouched — they are still talking to their agent. reason_code engine_unavailable is the fail-closed answer when the gate could not be evaluated at all: a gate that cannot decide blocks. reason_code destination_unregistered (rule_idtransfer.destination_unregistered.v1) is the refusal of an E.164 the tenant never registered in /v1/transfer-destinations, and destination_registry_unavailable the fail-closed answer when that registry could not be read; both come before the chain, with no decision_id. reason_code destination_not_allowed (rule_idegress.destination_not_allowed.v1) is the refusal of an E.164 outside the destination allowlist of a BYOC trunk of the sede (core#1281), and destination_allowlist_unavailable (egress.destination_allowlist_unavailable.v1) the fail-closed answer when that allowlist could not be read; both come before the registry, with decision_id when their gate_blocks row was written.

Or the destination itself was refused — envelope ErrorEnvelope. code destination_not_supported: the destination is number-shaped but not strict E.164 (bare national digits, a malformed + string). External destinations are dialled from E.164 only, because jurisdiction is resolved from that number. code destination_not_found: anything else that does not resolve to an active roster agent of THIS tenant. The split is decided by the SHAPE of what was sent, never by a second lookup, so neither answer says whether the destination exists in another tenant. code destination_unavailable: the destination IS an active agent of this tenant, but their seat is not :available right now (ringing, on a call, in wrap-up, paused, or not checked in). This phase does not move the seat, so transferring onto a busy agent would double-book them; this answer only ever names an agent of the caller's OWN roster, so it is not a cross-tenant oracle either. codecaller_id_unavailable: an EXTERNAL transfer on a call that has no tenant DID to present — refused rather than dialled with the carrier gateway's default ANI, which the tenant may not own. code did_unverified (core#2493): the DID the external leg would present (on an inbound call, the number the consumer dialled) is a customer DID whose ownership was never verified, and the tenant key did_ownership_proof is enforce. With observe the leg goes out and dialer_dids_unverified_use_total{surface="voice_transfer"} counts it; with off nothing changes. Applies to an external blind transfer and to an external conference add.

In every case the call_control_actions row settles failed with the requested destination recorded; ZERO ESL commands ran and the call is untouched.

Or the consultation was never taken (attended transfer only) — code consultation_unanswered: the destination is not registered, is busy, or did not answer the consultation leg. It is a 422 and not a 502 because the switch worked and a person did not pick up. THE HOLD HAS ALREADY BEEN RELEASED: the consumer is back with their agent before this response is written, and no consultation is left open.

Or the conference leg was never taken (conference add only) — codeconference_unanswered: the colleague is not registered, is busy, or did not answer. A 422 and not a 502 for the same reason; the call continues exactly as it was and no conference is left open.

destination_not_found

json
{
  "error": {
    "code": "destination_not_found",
    "message": "no active agent with that destination in this tenant"
  }
}

destination_not_supported

json
{
  "error": {
    "code": "destination_not_supported",
    "message": "an external transfer destination must be E.164 (+<country><number>)"
  }
}

destination_unavailable

json
{
  "error": {
    "code": "destination_unavailable",
    "message": "that agent is not available to take a transfer right now"
  }
}

caller_id_unavailable

json
{
  "error": {
    "code": "caller_id_unavailable",
    "message": "this call has no tenant caller-ID to present on a transferred leg"
  }
}

did_unverified

json
{
  "error": {
    "code": "did_unverified",
    "message": "the caller-ID this transferred leg would present is not a verified number of the tenant"
  }
}

compliance_blocked_mid_call

json
{
  "error": {
    "code": "compliance_blocked",
    "message": "the mid-call compliance re-check refused this blind_transfer (consent_revoked)",
    "gate": "mid_call",
    "reason_code": "consent_revoked",
    "rule_id": "regf.revocation.v1"
  }
}

compliance_blocked_pre_dial

json
{
  "error": {
    "code": "compliance_blocked",
    "message": "the origination gate refused the leg this blind_transfer would create (dnc_listed)",
    "gate": "pre_dial",
    "reason_code": "dnc_listed",
    "rule_id": "dnc.lists.v1",
    "decision_id": "018f3f2a-0000-7000-8000-000000000000"
  }
}

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. Every braked verb — the four toggles and the blind transfer — shares 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. Retry-After is the time left in the fixed window.

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

Bring a colleague, or a registered external number, into the call (three-way conference) ​

http
POST /v1/calls/{call_id}/conference/add

Rings a roster colleague of THIS tenant and puts them on the call's audio, so the agent, the consumer and the colleague talk together. An external/PSTN destination is 422 destination_not_supported unless the tenant has the conference_external_enabled flag ON (default OFF, and it needs conference_enabled).

EXTERNAL THIRD PARTY (core#1700, ADR #223 D6/D8). With that flag ON an E.164 goes through the SAME chain an external blind transfer does — plan gate, egress allowlist, the tenant's caller-ID, the registry of /v1/transfer-destinations (reason_code destination_unregistered, no decision_id) and the GATE_PRE_DIAL chain over the DESTINATION (DNC, quiet hours; 422 compliance_blocked with a queryable decision_id) — plus the consumer's attestation: the body must carry consumer_agreed: true, or the answer is 422consumer_agreement_required BEFORE anything is evaluated or dialled. The attestation is recorded in the call-control audit row and on the third party's own attempt. An external number is refused with zero commands to the switch whenever any of those refuses.

THE EXTERNAL LEG is the carrier leg an external transfer has: it leaves with the tenant's DID by the carrier the LCR chose for a house tenant (or the tenant's own trunk for BYOC), metered against that carrier's contracted call rate (429 carrier_cps, 503 no_route, 503 capacity_unavailable, as for a transfer). Unlike a transfer it replaces nobody, so it is ONE MORE CHANNEL of the tenant: a tenant at its channel ceiling or call-rate window is refused 429tenant_channels / tenant_cps with nothing written or dialled. It is billed from ITS OWN answer, never from the command's acknowledgement.

BEHIND THE conference_enabled FLAG, per tenant. With the flag off this path answers 404 not_found — the same answer as a path that does not exist — before the body is read, the rate budget is spent or the call is looked up, and nothing else on the call surface changes.

WHAT EACH PARTY HEARS, stated rather than implied. The colleague hears the CONSUMER's channel and joins it as a participant, so the consumer and the agent can hear them. WHAT IS RECORDED: the colleague's voice is mixed into the consumer's recording (the same channel), it is not a separate track and cannot be separated afterwards. Whether the consumer must be told, and what consent applies to a recording that now contains a third voice, is an open compliance question tracked in core#1648; this phase adds no disclosure.

ONE CONFERENCE PER CALL, and never on top of a consultation: a second add is 409 conference_in_progress, and so is add while an attended-transfer consultation is open (409 consultation_in_progress). A blind transfer or an attended-transfer start asked while a conference is open is 409 conference_in_progress as well.

GATED like a transfer: the GATE_MID_CALL trio (quiet hours, consent revocation, cease and desist) is re-checked, and a tenant whose plan cannot be read is refused (402 plan_inactive, fail-closed). The third party's leg is its own call_attempts row (leg = conference, conference_kind internal or external), which belongs to no debtor and no agent. An internal colleague's leg has no carrier and is not billed; an external leg carries its carrier and is billed from its own answer.

NOT REACHABLE WITH A WIDGET (ddw_) TOKEN in this phase: the widget allowlist is default-deny and does not list these paths, so that token is refused 403. Opening them to the widget is a separate, explicit decision.

THE AGENT KEEPS THE CALL. Ownership is the same ADR #66 gate as every other verb: an agent who no longer controls the call is refused 403.

Parameters

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

Request body (JSON, required)

FieldTypeRequiredDescription
destinationstringyesInternal sip_extension / agent id, or (external conference enabled) an E.164 number registered in /v1/transfer-destinations. Trimmed and bounded like BlindTransferRequest.destination.
consumer_agreedbooleannoThe consumer agreed to the external third party. Without a literal true an external destination is 422 consumer_agreement_required.

Responses

200 ​

Conference open (audit row durable, the colleague's leg up).

FieldTypeRequiredDescription
callobject (CallControlResult)yes
call.verbhold · unhold · mute · unmute · hangup · blind_transfer · attended_transfer_start · attended_transfer_complete · attended_transfer_cancelno
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.
call.bridge_confirm_bystring,null (date-time)noattended_transfer_complete only. When core checks that the consumer is bridged to the destination; a consumer still unbridged then is hung up with cause NORMAL_UNSPECIFIED. null when that check could not be armed.

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). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.

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

403 — Forbidden ​

The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.

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 — TransferRefused ​

The blind transfer never reached the switch. Three families of refusal share this status, and gate (when present) says which checkpoint refused.

The GATE_MID_CALL re-check refused it — code compliance_blocked, gate mid_call, with the same envelope unhold returns (reason_code, rule_id, auto_hangup_at): consent revoked, a written cease-and-desist on file, or the frozen consumer timezone(s) outside the quiet-hours window. A transfer puts the consumer back on live audio just like an unhold, so it is gated like one; otherwise a blocked unhold could be walked around by transferring the same call. The C9 auto-hangup is armed.

The GATE_PRE_DIAL chain refused the NEW LEG — codecompliance_blocked, gate pre_dial, with the engine decision_id and rule_id. Only an EXTERNAL destination can earn this: the leg it would create is an origination and passes the same chain a dial does, evaluated against the DESTINATION number (its DNC status, its NPA's quiet-hours window). NOTHING was dialled, NOTHING is armed and the consumer's own call is untouched — they are still talking to their agent. reason_code engine_unavailable is the fail-closed answer when the gate could not be evaluated at all: a gate that cannot decide blocks. reason_code destination_unregistered (rule_idtransfer.destination_unregistered.v1) is the refusal of an E.164 the tenant never registered in /v1/transfer-destinations, and destination_registry_unavailable the fail-closed answer when that registry could not be read; both come before the chain, with no decision_id. reason_code destination_not_allowed (rule_idegress.destination_not_allowed.v1) is the refusal of an E.164 outside the destination allowlist of a BYOC trunk of the sede (core#1281), and destination_allowlist_unavailable (egress.destination_allowlist_unavailable.v1) the fail-closed answer when that allowlist could not be read; both come before the registry, with decision_id when their gate_blocks row was written.

Or the destination itself was refused — envelope ErrorEnvelope. code destination_not_supported: the destination is number-shaped but not strict E.164 (bare national digits, a malformed + string). External destinations are dialled from E.164 only, because jurisdiction is resolved from that number. code destination_not_found: anything else that does not resolve to an active roster agent of THIS tenant. The split is decided by the SHAPE of what was sent, never by a second lookup, so neither answer says whether the destination exists in another tenant. code destination_unavailable: the destination IS an active agent of this tenant, but their seat is not :available right now (ringing, on a call, in wrap-up, paused, or not checked in). This phase does not move the seat, so transferring onto a busy agent would double-book them; this answer only ever names an agent of the caller's OWN roster, so it is not a cross-tenant oracle either. codecaller_id_unavailable: an EXTERNAL transfer on a call that has no tenant DID to present — refused rather than dialled with the carrier gateway's default ANI, which the tenant may not own. code did_unverified (core#2493): the DID the external leg would present (on an inbound call, the number the consumer dialled) is a customer DID whose ownership was never verified, and the tenant key did_ownership_proof is enforce. With observe the leg goes out and dialer_dids_unverified_use_total{surface="voice_transfer"} counts it; with off nothing changes. Applies to an external blind transfer and to an external conference add.

In every case the call_control_actions row settles failed with the requested destination recorded; ZERO ESL commands ran and the call is untouched.

Or the consultation was never taken (attended transfer only) — code consultation_unanswered: the destination is not registered, is busy, or did not answer the consultation leg. It is a 422 and not a 502 because the switch worked and a person did not pick up. THE HOLD HAS ALREADY BEEN RELEASED: the consumer is back with their agent before this response is written, and no consultation is left open.

Or the conference leg was never taken (conference add only) — codeconference_unanswered: the colleague is not registered, is busy, or did not answer. A 422 and not a 502 for the same reason; the call continues exactly as it was and no conference is left open.

destination_not_found

json
{
  "error": {
    "code": "destination_not_found",
    "message": "no active agent with that destination in this tenant"
  }
}

destination_not_supported

json
{
  "error": {
    "code": "destination_not_supported",
    "message": "an external transfer destination must be E.164 (+<country><number>)"
  }
}

destination_unavailable

json
{
  "error": {
    "code": "destination_unavailable",
    "message": "that agent is not available to take a transfer right now"
  }
}

caller_id_unavailable

json
{
  "error": {
    "code": "caller_id_unavailable",
    "message": "this call has no tenant caller-ID to present on a transferred leg"
  }
}

did_unverified

json
{
  "error": {
    "code": "did_unverified",
    "message": "the caller-ID this transferred leg would present is not a verified number of the tenant"
  }
}

compliance_blocked_mid_call

json
{
  "error": {
    "code": "compliance_blocked",
    "message": "the mid-call compliance re-check refused this blind_transfer (consent_revoked)",
    "gate": "mid_call",
    "reason_code": "consent_revoked",
    "rule_id": "regf.revocation.v1"
  }
}

compliance_blocked_pre_dial

json
{
  "error": {
    "code": "compliance_blocked",
    "message": "the origination gate refused the leg this blind_transfer would create (dnc_listed)",
    "gate": "pre_dial",
    "reason_code": "dnc_listed",
    "rule_id": "dnc.lists.v1",
    "decision_id": "018f3f2a-0000-7000-8000-000000000000"
  }
}

429 — TransferThrottled ​

code rate_limited: the NR-18 request-rate brake, exactly as CallControlThrottled describes it (refused before the call is looked up). code carrier_cps: an EXTERNAL transfer whose leg would leave by a carrier that is already at its contracted calls-per-second this second. Nothing was written or dialled and the consumer is still with their agent. Retryable within a second. rate_limited carries Retry-After (time left in the fixed window); carrier_cps does not. code tenant_channels / tenant_cps (conference add to an EXTERNAL number only): the third party is one more channel of the tenant, which is at its channel ceiling or call-rate window; nothing was dialled and the call goes on with the people already on it.

rate_limited

json
{
  "error": {
    "code": "rate_limited",
    "message": "call control rate limit exceeded; retry later"
  }
}

carrier_cps

json
{
  "error": {
    "code": "carrier_cps",
    "message": "the carrier for the transferred leg is at its contracted call rate; retry"
  }
}

tenant_channels

json
{
  "error": {
    "code": "tenant_channels",
    "message": "the tenant is at its channel or call-rate ceiling; the third party was not dialled"
  }
}

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 — TransferUnavailable ​

The four 503 codes of a blind transfer. audit_unavailable and esl_unavailable are the ones CallControlUnavailable describes. Two more apply to an EXTERNAL destination only. no_route: the house leg has no directed carrier route (routing switched off, no candidate carrier), so it is not placed, because house traffic is always directed. capacity_unavailable: the capacity gate could not admit the leg's carrier, and it fails closed. In both cases nothing was written or dialled.

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

no_route

json
{
  "error": {
    "code": "no_route",
    "message": "no directed carrier route for the transferred leg; nothing was dialled"
  }
}

capacity_unavailable

json
{
  "error": {
    "code": "capacity_unavailable",
    "message": "the capacity gate could not admit the transferred leg, retry"
  }
}

Take the colleague out of the call ​

http
POST /v1/calls/{call_id}/conference/remove

Hangs up the colleague's own leg. The consumer and the agent are left exactly as they were: this verb only ever takes a voice AWAY, so it is never gated by compliance or by the plan.

NO BODY. The leg to hang up comes from the OPEN conference, never from the request. A leg the switch no longer knows (the colleague already hung up) is the state this verb was asked to reach, so it still answers 200. 409 no_conference when this call has no conference open.

BEHIND THE conference_enabled FLAG: with it off this path answers 404not_found like a path that does not exist.

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 ​

Conference closed; the call continues with its agent.

FieldTypeRequiredDescription
callobject (CallControlResult)yes
call.verbhold · unhold · mute · unmute · hangup · blind_transfer · attended_transfer_start · attended_transfer_complete · attended_transfer_cancelno
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.
call.bridge_confirm_bystring,null (date-time)noattended_transfer_complete only. When core checks that the consumer is bridged to the destination; a consumer still unbridged then is hung up with cause NORMAL_UNSPECIFIED. null when that check could not be armed.

401 — Unauthorized ​

Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.

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

403 — Forbidden ​

The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.

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

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. Every braked verb — the four toggles and the blind transfer — shares 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. Retry-After is the time left in the fixed window.

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

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