> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vocily.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Events

> The 7 call.* webhook events and their payloads.

All events share the [envelope](/developers/webhooks/overview#the-envelope); `data.object` is always
`"call"` and `data.call` is the **call object** below — the same shape as
[`GET /v1/calls/{id}`](/developers/calls/get). `analysis` and `custom_analysis` are always fields on
the call (`null` until they're ready); the transfer event additionally attaches a `transfer` sub-object.

**Current API version:** `2026-08-01`

| Event                                                               | Fires when                                                         |
| ------------------------------------------------------------------- | ------------------------------------------------------------------ |
| [`call.started`](#call-started)                                     | The call is placed and dialing/ringing.                            |
| [`call.transfer.updated`](#call-transfer-updated)                   | A human-transfer state changes.                                    |
| [`call.ended`](#call-ended)                                         | The call reaches any terminal status.                              |
| [`call.recording.ready`](#call-recording-ready)                     | The recording is available.                                        |
| [`call.analysis.completed`](#call-analysis-completed)               | Platform AI analysis finished.                                     |
| [`call.custom_analysis.completed`](#call-custom-analysis-completed) | Your custom-analysis items finished.                               |
| [`call.processing.completed`](#call-processing-completed)           | Everything is done — the complete record. **Recommended default.** |

## The call object

Present as `data.call` on every event. It's the **same object** as [`GET /v1/calls/{id}`](/developers/calls/get) —
one shape to learn across REST and webhooks. `cost`, `analysis`, and `custom_analysis` are `null` until
they're finalized post-call (so `call.started` / `call.ended` carry `null` for them; they're populated
by the later analysis/processing events).

<ResponseField name="id" type="string">The call id (matches `execution_id` from `POST /v1/calls`).</ResponseField>
<ResponseField name="agent_id" type="string | null">The agent that ran the call.</ResponseField>
<ResponseField name="status" type="string">Lifecycle status.</ResponseField>
<ResponseField name="direction" type="string">`inbound` or `outbound`.</ResponseField>
<ResponseField name="from_number" type="string | null">Caller number.</ResponseField>
<ResponseField name="to_number" type="string | null">Destination number.</ResponseField>
<ResponseField name="duration_seconds" type="integer | null">Connected duration.</ResponseField>
<ResponseField name="created_at" type="string">When the call was created (ISO 8601, UTC).</ResponseField>
<ResponseField name="connected_at" type="string | null">When it connected.</ResponseField>
<ResponseField name="ended_at" type="string | null">When it ended.</ResponseField>
<ResponseField name="metadata" type="object | null">Your passthrough JSON from `POST /v1/calls`.</ResponseField>
<ResponseField name="variables" type="object | null">The call's dynamic/custom variables — from `POST /v1/calls` `variables`, or resolved for inbound (e.g. `caller_no`). Only your custom variables, not internal runtime state.</ResponseField>
<ResponseField name="cost" type="object | null">`{ currency: "USD", total, rate_per_min }` — `null` until billed.</ResponseField>
<ResponseField name="analysis" type="object | null">`{ sentiment, outcome, language, summary }` — `null` until analyzed.</ResponseField>
<ResponseField name="custom_analysis" type="object | null">Your configured custom-analysis data.</ResponseField>
<ResponseField name="transcript" type="object">The conversation.</ResponseField>
<ResponseField name="recording_url" type="string | null">Stable recording URL; `null` until ready.</ResponseField>

## Call started

`call.started` — the call was placed and is dialing/ringing. Cost, transcript, and recording are not
yet available.

## Call transfer updated

`call.transfer.updated` — a human-transfer state changed. Adds a `transfer` sub-object:

```json theme={"dark"}
"transfer": {
  "state": "answered",          // transferring | answered | failed
  "target": "+911140001111",    // the human agent's number
  "reason": null                 // set when state = failed
}
```

## Call ended

`call.ended` — the call reached a terminal status. The core outcome event, with transcript, timings,
and cost.

```json theme={"dark"}
{
  "event_id": "evt_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
  "event_type": "call.ended",
  "api_version": "2026-08-01",
  "occurred_at": "2026-08-04T10:00:47.123Z",
  "workspace_id": "0f32de8b-9243-4a8d-9203-63caffdae3b8",
  "org_id": "7c1e9a02-3b44-4d51-8f2a-1a2b3c4d5e6f",
  "sequence": 42,
  "data": {
    "object": "call",
    "call": {
      "id": "c7a1e2b3-4d5f-6789-a0b1-c2d3e4f5a6b7",
      "agent_id": "9b1c3d5e-7a90-4c21-8b6d-2e4f6a8c0d11",
      "status": "completed",
      "direction": "outbound",
      "from_number": "+911171366867",
      "to_number": "+919876543210",
      "duration_seconds": 42,
      "created_at": "2026-08-04T10:00:00.000Z",
      "connected_at": "2026-08-04T10:00:05.000Z",
      "ended_at": "2026-08-04T10:00:47.000Z",
      "metadata": { "order_id": "88431" },
      "variables": { "customer_name": "Rahul" },
      "cost": { "currency": "USD", "total": 0.0554, "rate_per_min": 0.0792 },
      "analysis": null,
      "custom_analysis": null,
      "transcript": {},
      "recording_url": null
    }
  }
}
```

## Call recording ready

`call.recording.ready` — the recording is available. In addition to the stable `recording_url`, each
delivery carries a **ready-to-use, expiring** download link, minted fresh for each delivery:

```json theme={"dark"}
"call": {
  "...": "…",
  "recording_url": "https://api.vocily.com/v1/calls/c7a1e2b3-.../recording",
  "recording_presigned_url": "https://storage.vocily.ai/rec/…?X-Amz-Expires=86400&…",
  "recording_url_expires_at": "2026-08-05T10:00:47.000Z"
}
```

## Call analysis completed

`call.analysis.completed` — platform AI analysis finished. The call's `analysis` field is now populated
(same shape as the REST [`analysis` field](/developers/calls/get)):

```json theme={"dark"}
"analysis": {
  "sentiment": "positive",   // positive | neutral | negative
  "outcome": "booked",
  "language": "en",
  "summary": "Customer confirmed the appointment for Friday at 3pm."
}
```

## Call custom analysis completed

`call.custom_analysis.completed` — your configured custom-analysis items finished. The call's
`custom_analysis` field is now populated (the extracted business data):

```json theme={"dark"}
"custom_analysis": { "lead_score": 9, "wants_callback": true }
```

## Call processing completed

`call.processing.completed` — **the recommended default subscription.** A fan-in event that fires once analysis, custom analysis,
and the recording are all terminal — the complete record in a single payload (call object +
`analysis` + `custom_analysis`). Subscribe to this if you want one clean event per finished call
instead of stitching the individual ones together.

```json theme={"dark"}
{
  "event_type": "call.processing.completed",
  "api_version": "2026-08-01",
  "sequence": 45,
  "data": {
    "object": "call",
    "call": {
      "id": "c7a1e2b3-4d5f-6789-a0b1-c2d3e4f5a6b7",
      "status": "completed",
      "duration_seconds": 42,
      "cost": { "currency": "USD", "total": 0.0554, "rate_per_min": 0.0792 },
      "recording_url": "https://api.vocily.com/v1/calls/c7a1e2b3-.../recording",
      "analysis": { "sentiment": "positive", "outcome": "booked", "language": "en", "summary": "…" },
      "custom_analysis": { "lead_score": 9 },
      "metadata": { "order_id": "88431" }
    }
  }
}
```

<Tip>
  Subscribe to **`call.processing.completed`** for the full record in one event, or to
  **`call.ended`** if you need the outcome the instant the call hangs up (before analysis/recording are
  ready).
</Tip>
