Skip to content

Live call control

Requires webhook version 2026-06-01

Live control lets you mutate a Voice App call while it’s in progress (hang it up, mute a leg, send DTMF, or play audio into it) via authenticated REST. Every operation is asynchronous and idempotent; the outcome is reflected in the call/leg state you can read back from the inspection API.

All four operations follow the same shape:

  • URL pattern: POST /api/v1/voice/calls/{call_id}/{action} where call_id is cs_<ULID> and action is one of hangup, mute, dtmf, play.
  • Response: 202 Accepted with { "request_id": "req_..." }. The work is queued and applied to the live call.
  • Idempotency-Key header: required on every request. See Idempotency below.
  • Live-call requirement: the call must be in initiated, ringing, or answered state. Otherwise: 409 call_not_live.

Hang up a specific leg or the whole call.

POST /api/v1/voice/calls/{call_id}/hangup
Content-Type: application/json
Idempotency-Key: <unique-per-attempt>
{
"leg_id": "cl_...", // optional; omit to hang up the whole call
"reason": "agent_done" // optional; free-form label surfaced on the call
}

Mute or unmute a specific leg. Muting stops audio from the leg but keeps the call connected.

POST /api/v1/voice/calls/{call_id}/mute
Content-Type: application/json
Idempotency-Key: <unique-per-attempt>
{
"leg_id": "cl_...", // required
"on": true // true = mute; false = unmute
}

Send DTMF digits into the call as if the platform pressed them (useful for punching through IVRs).

POST /api/v1/voice/calls/{call_id}/dtmf
Content-Type: application/json
Idempotency-Key: <unique-per-attempt>
{
"leg_id": "cl_...", // optional; omit to send on primary leg
"digits": "1234#", // required; matches ^[0-9*#A-D]+$
"duration_ms": 100 // optional; 40..1000, default 100
}

Play an audio file into the call. Returns immediately with a playback_id; playback ends naturally or when interrupted by DTMF.

POST /api/v1/voice/calls/{call_id}/play
Content-Type: application/json
Idempotency-Key: <unique-per-attempt>
{
"leg_id": "cl_...", // optional; omit to play on primary leg
"media_url": "https://.../audio.mp3", // required; HTTPS MP3 or WAV
"loop": 1, // optional; 1..10, default 1
"on_dtmf": "stop" // optional; "stop" | "ignore", default "stop"
}

Response:

{
"request_id": "req_...",
"playback_id": "pb_..."
}

Every control request must include an Idempotency-Key header (up to 255 chars). This makes the API safe to retry.

Semantics:

  • Same key, same body → server returns the cached response from the first attempt. No new work is queued.
  • Same key, different body → 422 idempotency_key_collision.
  • Same key, still in flight → 409 idempotency_replay_in_progress. Wait and retry.
  • Keys are scoped per account. Cached responses live for 24 hours. In-flight locks expire after 5 minutes.

Recommended: use a UUID per logical intent (not per HTTP retry). If your code decides “hang up this call because the agent finished,” mint one key at that decision point and reuse it on every network retry.

See Retries & idempotency for the general contract.

{ "request_id": "req_..." }

play additionally returns playback_id.

{
"type": "invalid_state",
"code": "call_not_live",
"message": "Call is in state 'completed'; live control requires initiated/ringing/answered."
}
HTTPcodeWhen
400idempotency_key_requiredThe header wasn’t sent.
400idempotency_key_too_longHeader value exceeds 255 chars.
404noneCall ID doesn’t exist or isn’t owned by your account.
409call_not_controllableCall has no Voice App (SIP Trunk calls don’t support runtime mutations).
409unsupported_api_versionOwning Voice App is pinned to 2025-08-01. Bump the pin to 2026-06-01.
409call_not_liveCall state is not one of initiated / ringing / answered.
409leg_not_foundThe leg_id you passed isn’t a leg of this call.
409idempotency_replay_in_progressSame key is currently being processed.
410call_lost_ownerCall ended or was migrated to another platform instance.
422idempotency_key_collisionSame key, different request body.
503call_platform_unavailablePlatform is temporarily unreachable. Retry with backoff.
503idempotency_unavailableIdempotency store is temporarily unavailable. Retry with backoff.
503wrong_instanceThe call is served by another platform instance. Retry with backoff.

Two control requests against the same call are processed in the order they arrive at the platform. If you send mute then hangup back-to-back, mute will complete first (even if just barely). If you need to serialize on your side, confirm the effect via the inspection API before firing the next mutation.

Requests against different calls have no ordering guarantee.