# Voice events

Bird emits a webhook event at three points in a call, so your systems learn what happened without polling the dashboard. This page is the event vocabulary. How events reach your endpoint (subscriptions, signatures, retries, replay) is covered in the [Webhooks guide](/docs/guides/webhooks).

A call's path through the event types:

1. `voice_call.initiated`: Bird accepted the call setup request (a SIP `INVITE`) and started routing the call
2. `voice_call.answered`: the number you called picked up. Only answered calls get this event
3. `voice_call.ended`: the call is over, and the event carries the outcome

Read `initiated` as "a call exists" and `ended` as the verdict. A call Bird [refuses](/docs/guides/voice/call-log#rejected-calls) after accepting the `INVITE` still ends with `voice_call.ended`, carrying `status: "failed"` and `sip_response_code: 503`, so every call you hear about opening also closes.

The event `type` is an **open enum**: Bird may add types over time, so match the ones you handle and ignore the rest rather than treating an unfamiliar type as an error.

## The event envelope

Voice events arrive in the same nested envelope as every other Bird event, described in the [Webhooks guide](/docs/guides/webhooks): `type`, `timestamp`, and a type-specific `data` object. The event's identity is in the `webhook-id` HTTP header rather than the body.

| Field       | Description                                                              |
| ----------- | ------------------------------------------------------------------------ |
| `type`      | One of the three types on this page, for example `voice_call.ended`      |
| `timestamp` | When the event occurred (RFC 3339). Sort by this, never by arrival order |
| `data`      | Event-specific payload, always carrying the same call identity fields    |

Every voice event's `data` carries the same identity fields, so you can correlate against your own records without a second lookup. Both numbers are in E.164 format, a leading `+` followed by the country code and national number.

| Field          | Description                                                                                                  |
| -------------- | ------------------------------------------------------------------------------------------------------------ |
| `call_id`      | The call record's id (`vcl_…`), the same one shown in the [Call log](/docs/guides/voice/call-log)            |
| `session_id`   | Shared by every leg of a transferred or multi-party call (`vcs_…`). Null when no session correlation applies |
| `workspace_id` | The workspace the call belongs to                                                                            |
| `direction`    | `outbound` for calls your equipment placed                                                                   |
| `from`         | The calling number                                                                                           |
| `to`           | The number that was called                                                                                   |

## voice_call.initiated

Bird received the `INVITE` and began routing.

```json
{
  "type": "voice_call.initiated",
  "timestamp": "2026-06-10T14:30:00Z",
  "data": {
    "call_id": "vcl_01krdgeqcxet5s7t44vh8rt9mg",
    "session_id": "vcs_01krdgeqcxet5s7t44vh8rt9mh",
    "workspace_id": "wsp_01krdgeqcxet5s7t44vh8rt9mj",
    "direction": "outbound",
    "from": "+14155551234",
    "to": "+16505559876"
  }
}
```

## voice_call.answered

The number you called picked up, and billable time starts here. An unanswered call never emits this event, so its absence between `initiated` and `ended` is how you know nobody answered.

The payload is the [call identity fields](#the-event-envelope) every voice event carries, with `timestamp` set to the moment of answer.

## voice_call.ended

The call is over. This event adds the outcome:

| Field               | Description                                                                                                                      |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `status`            | How it ended: `answered`, `no_answer`, `failed`, `rejected`, or `unknown` (see [Statuses](/docs/guides/voice/call-log#statuses)) |
| `sip_response_code` | The call's final SIP code, for example `200` or `486`. A call Bird refused carries `503`; null when no final code was recorded   |
| `duration_ms`       | Total call length in milliseconds, from the moment Bird received the call to hangup                                              |
| `billable_ms`       | Answered time in milliseconds, so a call nobody answered reports zero                                                            |

```json
{
  "type": "voice_call.ended",
  "timestamp": "2026-06-10T14:31:05Z",
  "data": {
    "call_id": "vcl_01krdgeqcxet5s7t44vh8rt9mg",
    "session_id": "vcs_01krdgeqcxet5s7t44vh8rt9mh",
    "workspace_id": "wsp_01krdgeqcxet5s7t44vh8rt9mj",
    "direction": "outbound",
    "from": "+14155551234",
    "to": "+16505559876",
    "status": "answered",
    "sip_response_code": 200,
    "duration_ms": 65000,
    "billable_ms": 60000
  }
}
```

Two details live on the call record rather than in this event. The rejection reason is one, so a `failed` status that Bird caused rather than a carrier is only distinguishable there: open the call in the [Call log](/docs/guides/voice/call-log) to read which check refused it. Cost is the other, since rating happens once the call has ended.

## Consuming them safely

- **Deduplicate on `webhook-id`.** Bird delivers at least once, and a call's `initiated` event can be published more than once when a signaling retry replays. Same call, same stage, same `webhook-id`, so keying on it collapses the duplicate.
- **Do not rely on order.** Deliveries are not ordered, so `answered` can reach you after `ended`. Sort by `timestamp`, and let a later-arriving event with an earlier timestamp lose.
- **Treat `ended` as the only reliable outcome.** It is the event that carries status and durations, and it is the one to key your own records off.
- **Events are best-effort alongside the record.** The [Call log](/docs/guides/voice/call-log) is the record of what happened; the events are how you find out promptly. For reconciliation, export the calls as CSV rather than replaying events.

## Next steps

| Page                                       | What it covers                                              |
| ------------------------------------------ | ----------------------------------------------------------- |
| [Webhooks & events](/docs/guides/webhooks) | Endpoint setup, signature verification, retries, and replay |
| [Call log](/docs/guides/voice/call-log)    | Every field on a call record, and CSV export                |