Skip to main content
When a call hands off to a voice agent, Yotel publishes a stream of webhook events that mirror the AI’s lifecycle: started, ended, transferred, escalated, and conference changes. The four pre-existing call.* events (started, answered, ended, recording_ready) still fire — call.recording_ready gains a stereo flag when the call was AI-driven. This page is the reference for AI-call subscribers. For the global event catalogue and delivery mechanics see Webhooks overview and Event catalog.
AI events ride the same delivery infrastructure as every other event — same envelope, same signature scheme, same retry schedule. Adding ai_session.* is a pure additive change; existing handlers keep working.

Common envelope

{
  "id": "evt_01HW8...",
  "type": "ai_session.transferred",
  "created_at": "2026-05-01T14:37:08.102Z",
  "tenant_id": "<uuid>",
  "data": { /* event-specific payload */ }
}
Headers:
X-Yotel-Signature: t=<unix_ts>,v0=<hex_hmac_sha256>
X-Yotel-Event-Id: evt_01HW8...
X-Yotel-Event-Type: ai_session.transferred
X-Yotel-Signature follows the Stripe shape: HMAC_SHA256(secret, f"{t}.{raw_body}"). A 5-min freshness window prevents replay. See Signature verification for verifier code.

Subscribing

Webhook subscriptions are managed from the dashboard, not the public API. Pick AI-session events with the wildcard ai_session.* to opt into the full stream:
curl -X POST https://api.yotel.in/api/v1/webhooks \
  -H "Authorization: Bearer $YOTEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-host.example.com/webhooks/yotel",
    "event_types": ["ai_session.*", "call.recording_ready"]
  }'
Save the signing_secret from the response — it’s shown once.

Per-event reference

call.started

Pre-answer originate event. Fires for every call, AI or human. For AI campaigns, voice_agent_id is the resolved UUID.
{
  "call_id": "<uuid>",
  "direction": "outbound",
  "from": "+91...",
  "to": "+91...",
  "campaign_id": "<uuid|null>",
  "voice_agent_id": "<uuid|null>",
  "started_at": "2026-05-01T14:37:02.419Z"
}

call.answered

Fires on CHANNEL_ANSWER. answered_by_amd is Yotel’s AMD classification — "machine" calls still get connected to the AI; your AI service can decide whether to leave a message or hang up.
{
  "call_id": "<uuid>",
  "answer_time": "2026-05-01T14:37:08.102Z",
  "answered_by_amd": "human"
}

call.ended

Fires on CHANNEL_HANGUP. disposition is null if the AI never called set_disposition. For full AI lifecycle context see ai_session.ended (fires alongside this one for AI calls).
{
  "call_id": "<uuid>",
  "end_time": "2026-05-01T14:39:02.102Z",
  "hangup_cause": "NORMAL_CLEARING",
  "total_duration_s": 120,
  "talk_duration_s": 114,
  "disposition": "interested"
}

call.recording_ready

Fires when the WAV upload to GCS completes (typically +2-5s after hangup). stereo: true for AI calls (left=caller, right=AI); false for human-only calls.
{
  "call_id": "<uuid>",
  "recording_url": "https://api.yotel.in/api/v1/calls/<id>/recording",
  "signed_url": "https://storage.googleapis.com/.../<id>.wav?...",
  "duration_s": 47,
  "stereo": true,
  "format": "audio/wav"
}
The signed_url expires ~15 minutes after issue. For long-lived access, hit recording_url (the proxy endpoint) on demand — or pull the bytes promptly into your own storage.

ai_session.started

Fires when Yotel opens the audio fork to your voice agent. ws_url is the resolved URL (after override hierarchy).
{
  "call_id": "<uuid>",
  "voice_agent_id": "<uuid>",
  "external_agent_id": "tier1_v3",
  "ws_url": "wss://ai.example.com/yotel/audio-fork",
  "started_at": "2026-05-01T14:37:08.500Z"
}

ai_session.ended

Fires when the AI session terminates (transfer, hangup, error, or 30-min timeout). metadata carries the full audit log — every log invocation plus the final transfer/hangup metadata.
{
  "call_id": "<uuid>",
  "voice_agent_id": "<uuid>",
  "ended_at": "2026-05-01T14:39:01.999Z",
  "outcome": "transferred_agent",
  "metadata": {
    "logs": [ {"step": "intent_classified", "intent": "renewal"} ],
    "final": {"destination_type": "agent_queue", "destination": "tier1"}
  }
}
outcome is one of: transferred_agent, transferred_e164, transferred_sip_uri, hangup, error, timeout.

ai_session.transferred

Fires on a successful transfer verb. The AI’s leg drops; the caller is bridged to destination. ai_session.ended fires immediately after.
{
  "call_id": "<uuid>",
  "voice_agent_id": "<uuid>",
  "destination_type": "agent_queue",
  "destination": "tier1",
  "transferred_at": "2026-05-01T14:39:01.999Z",
  "metadata": { "intent": "billing", "summary": "..." }
}

ai_session.escalated

Fires on request_supervisor invocation and again on supervisor claim. Subscribers should dedupe on event.id if they only want the terminal claim.
{
  "call_id": "<uuid>",
  "voice_agent_id": "<uuid>",
  "reason": "Caller is angry",
  "urgency": "high",
  "supervisor_id": null,
  "escalated_at": "2026-05-01T14:38:30.000Z"
}
supervisor_idPhase
nullInvocation — escalation enqueued, no human yet
<uuid>Claim — supervisor accepted from the dashboard

ai_session.conference_changed

Fires on every conference state transition (conference_start, add, remove, leave) and the supervisor verbs (whisper, barge, monitor_start, monitor_stop).
{
  "call_id": "<uuid>",
  "voice_agent_id": "<uuid>",
  "conference_id": "<uuid>",
  "change": "added",
  "actor": "ai",
  "participants": [
    {"member_id": "mem-1", "role": "caller", "destination": "+9198..."},
    {"member_id": "mem-2", "role": "ai", "destination": "<voice-agent-id>"},
    {"member_id": "mem-3", "role": "agent", "destination": "ag-7"}
  ],
  "changed_at": "2026-05-01T14:38:15.420Z"
}
change is one of: started, added, removed, left, monitor_started, monitor_stopped, whisper, barge. actor is "ai", "supervisor", or "tenant_api".

Stereo recording flag

For AI-driven calls, RECORD_STEREO=true is set on the FreeSWITCH dialplan. The resulting WAV has two channels:
  • Left — the caller’s audio (PSTN side).
  • Right — the AI’s audio (synthesised TTS or pre-recorded URL payback returned over the WS).
Mono recordings are returned for human-only calls (no connect_voice_agent step). The stereo flag on call.recording_ready lets transcript/QA pipelines pick the right diarisation strategy without probing the file.

SDK signature verification

Both SDKs already ship a webhook helper — no new function for ai_session.*. The WebhookEventType union grew by 5 (and 1 field on RecordingReady); upgrade the SDK to get the new types.
Python
import yotel

event = yotel.webhook.verify_and_parse(
    raw_body,
    headers=request.headers,
    signing_secret=os.environ["YOTEL_WEBHOOK_SECRET"],
)
if event.type == "ai_session.transferred":
    ...
TypeScript
import { verifyWebhook } from "@yotel/client";

const event = verifyWebhook({
  rawBody: req.body,
  headers: req.headers,
  signingSecret: process.env.YOTEL_WEBHOOK_SECRET!,
});
if (event.type === "ai_session.transferred") {
  // event.data is fully typed
}

Retry & dead-letter

Inherited from the global delivery layer:
  • 3 retries with backoff 200ms → 2s → 20s.
  • After 3 failures the delivery is marked dead_lettered.
  • Replay manually via POST /api/v1/webhooks/deliveries/{id}/replay.
  • 2xx = success; 4xx (except 408/429) = no retry; 5xx + 408/429 retry.
See Retries for the full schedule.

See also