# Verify events

A verification produces events for its **session** and each **delivery attempt**. The session starts when Bird creates the verification and converts when the recipient enters the correct code. Each passcode send creates an attempt on one channel, which can be delivered or undelivered. Resends and channel failover add attempts to the same session.

| Event                          | Axis     | Fires when                                                                 |
| ------------------------------ | -------- | -------------------------------------------------------------------------- |
| `verify.verification.created`  | Session  | A verification is created and the first passcode is queued for send        |
| `verify.attempt.sent`          | Delivery | A passcode has been handed to a channel for delivery                       |
| `verify.attempt.delivered`     | Delivery | The channel confirmed the passcode reached the recipient                   |
| `verify.attempt.undelivered`   | Delivery | The channel could not get the passcode to the recipient                    |
| `verify.verification.verified` | Session  | The recipient submitted the correct code before the verification expired   |
| `verify.verification.failed`   | Session  | The delivery plan ended with failures indicating that no passcode was sent |

A verification that does not convert never emits `verify.verification.verified`, and its [status](/docs/guides/verify/sending-verifications#statuses) does not tell you why on its own. `failed` is shared: a verification lands there both when too many incorrect passcodes were submitted, with `reason` `attempts_exhausted`, and when the delivery plan ends with failures indicating that no passcode was sent, with `reason` `undeliverable`. Only the second emits `verify.verification.failed`, and that event always carries `reason` `undeliverable`, so the event is what separates the two where the status cannot. A validity window that elapses resolves to `expired`. Neither `expired` nor an attempts-exhausted `failed` emits an event of its own. A fallback channel creates its own `verify.attempt.sent`, so one verification can have multiple attempt sequences.

The event type list is open: new types may be added over time, so treat an unrecognized value as a future event rather than an error.

## The event envelope

Events arrive at your webhook endpoint in the [Standard Webhooks](https://www.standardwebhooks.com) nested envelope described in the [Webhooks guide](/docs/guides/webhooks): a `type`, a `timestamp`, and a type-specific `data` object. The event's identity is not in the body: it rides in the `webhook-id` HTTP header, which is stable across retries of the same delivery and is your deduplication key.

Every event's `data` carries this identity base:

- `verification_id`: the verification this event belongs to, matching the `id` from [`POST /v1/verify/verifications`](/docs/api/reference/create-verification)
- `workspace_id`: the workspace that created the verification
- `to`: the recipient identity of the verification, an object with `email` and/or `phone_number` matching what the create request supplied. A single passcode attempt reports the one address it went to in its own `address` field
- `metadata`: the free-form object from the create request, echoed unchanged, or `null` when the request carried none

## Session events

### `verify.verification.created`

Fires as soon as a verification is created and its first passcode is queued. Adds `channel` (the channel the first attempt is going out on), `status: "pending"`, and `created_at`.

```json
{
  "type": "verify.verification.created",
  "timestamp": "2026-07-23T14:45:58Z",
  "data": {
    "verification_id": "vrf_01ky7q1fdze3695yvyz7z9nm3a",
    "workspace_id": "ws_01ky7m235keycbnwyajabe1a6b",
    "channel": "sms",
    "to": { "phone_number": "+14155550100" },
    "status": "pending",
    "created_at": "2026-07-23T14:45:58Z",
    "metadata": { "user_id": "usr_4821" }
  }
}
```

### `verify.verification.verified`

Fires when [`POST /v1/verify/verifications/check`](/docs/api/reference/create-verification-check) confirms the correct code. Adds `status: "verified"`, `channel` (whichever channel delivered the submitted code, or `null` when the verification resolved without attributing a channel), and `verified_at`.

```json
{
  "type": "verify.verification.verified",
  "timestamp": "2026-07-23T14:46:38Z",
  "data": {
    "verification_id": "vrf_01ky7q1fdze3695yvyz7z9nm3a",
    "workspace_id": "ws_01ky7m235keycbnwyajabe1a6b",
    "to": { "phone_number": "+14155550100" },
    "status": "verified",
    "channel": "sms",
    "verified_at": "2026-07-23T14:46:38Z",
    "metadata": { "user_id": "usr_4821" }
  }
}
```

### `verify.verification.failed`

Fires when the delivery plan is exhausted and the recorded failures indicate that no passcode was sent. The payload adds `status: "failed"`, `reason: "undeliverable"`, `channel` (the last channel tried, or `null` when none was attributed), `last_attempt_reason`, and `failed_at`.

`channel_unavailable`, `channel_disabled`, `channel_restricted`, and `not_billable` indicate that an attempt did not send a passcode. If an attempt might have sent one, a later bounce, carrier rejection, or delivery timeout leaves the session pending and emits no `verify.verification.failed`. An earlier code can still verify before it expires.

`last_attempt_reason` uses the same failure reasons as `verify.attempt.undelivered`. A `not_billable` failure means the send could not be charged; check the workspace balance and whether pricing is available for the destination.

## Delivery events

Every passcode Bird sends is one **attempt**. A resend or channel failover creates another attempt against the same `verification_id`, with its own delivery sequence. No event carries an attempt identifier, and `webhook-id` will not group them: it identifies one delivery of one event, so the `sent` and the `delivered` for a single attempt carry different values. Pair them on `verification_id`, `channel` and `address` in timestamp order. A resend on the same channel is the case that defeats this, since its events differ only by timestamp.

### `verify.attempt.sent`

Fires once Bird has handed the passcode to the channel. Adds `channel`, `address` (the single address this attempt was sent to, an E.164 phone number or an email address), `from` (the sending address or number, `null` when the channel exposes no sender), and `sent_at`.

```json
{
  "type": "verify.attempt.sent",
  "timestamp": "2026-07-23T14:45:59Z",
  "data": {
    "verification_id": "vrf_01ky7q1fdze3695yvyz7z9nm3a",
    "workspace_id": "ws_01ky7m235keycbnwyajabe1a6b",
    "to": { "phone_number": "+14155550100" },
    "channel": "sms",
    "address": "+14155550100",
    "from": "29999",
    "sent_at": "2026-07-23T14:45:59Z",
    "metadata": { "user_id": "usr_4821" }
  }
}
```

### `verify.attempt.delivered`

Fires when the channel confirms that the passcode reached the recipient. Adds `channel`, `address`, `carrier`, `mcc_mnc` (the handling network and its mobile country/network code), and `delivered_at`. The `carrier` and `mcc_mnc` fields are always `null` for email, WhatsApp, and Telegram. This event omits `from`; read it from `verify.attempt.sent` for the same attempt.

```json
{
  "type": "verify.attempt.delivered",
  "timestamp": "2026-07-23T14:46:03Z",
  "data": {
    "verification_id": "vrf_01ky7q1fdze3695yvyz7z9nm3a",
    "workspace_id": "ws_01ky7m235keycbnwyajabe1a6b",
    "to": { "phone_number": "+14155550100" },
    "channel": "sms",
    "address": "+14155550100",
    "carrier": "Example Wireless",
    "mcc_mnc": "310260",
    "delivered_at": "2026-07-23T14:46:03Z",
    "metadata": { "user_id": "usr_4821" }
  }
}
```

### `verify.attempt.undelivered`

Fires when the channel could not deliver the passcode. Adds `channel`, `address`, `reason` (an open enum including `carrier_rejected`, `hard_bounce`, `soft_bounce`, `undelivered`, `channel_unavailable`, `channel_restricted`, `channel_disabled`, `delivery_timeout`, and `not_billable`), `error` (display-only detail, or `null`), and `failed_at`. Like `verify.attempt.delivered`, this event omits `from`.

```json
{
  "type": "verify.attempt.undelivered",
  "timestamp": "2026-07-23T14:46:04Z",
  "data": {
    "verification_id": "vrf_01ky7q1fdze3695yvyz7z9nm3a",
    "workspace_id": "ws_01ky7m235keycbnwyajabe1a6b",
    "to": { "phone_number": "+14155550100" },
    "channel": "sms",
    "address": "+14155550100",
    "reason": "carrier_rejected",
    "error": "Carrier rejected the message before delivery",
    "failed_at": "2026-07-23T14:46:04Z",
    "metadata": { "user_id": "usr_4821" }
  }
}
```

An undelivered attempt on a recipient with more than one channel available does not end the verification. Bird advances to the next channel in the [delivery plan](/docs/guides/verify/sending-verifications#recipient), which gets its own `verify.attempt.sent`. A channel that fails before sending emits `verify.attempt.undelivered` with `reason: "channel_unavailable"` and advances in the same way, as does one that does not carry passcodes to the recipient's country, with `reason: "channel_restricted"` (see [Country configuration](/docs/guides/verify/countries#where-whatsapp-cannot-carry-a-passcode)). That attempt has no `verify.attempt.sent` or later delivery report. Bird emits `verify.attempt.undelivered` for each failed attempt. If the plan is exhausted and the recorded failures indicate that no passcode was sent, it also emits `verify.verification.failed` for the session.

Delivery reports are indicative rather than guaranteed. Carriers and mailbox providers vary in what they confirm and how quickly. In some markets, attempt events arrive minutes later or do not distinguish delivery from acceptance. Treat `verify.verification.verified` as the definitive signal that a recipient received and used their code.

## Webhooks

Subscribe an endpoint to any `verify.*` type from the [**Webhooks**](https://bird.com/dashboard/w/webhooks) page in the dashboard or through the webhooks API. The [Webhooks guide](/docs/guides/webhooks) covers creating endpoints, verifying the Standard Webhooks signature, retries, and replaying failed deliveries.

## Next steps

| Page                                                                            | What it covers                                              |
| ------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| [Sending verifications](/docs/guides/verify/sending-verifications)              | The send and check calls, statuses, settings, and limits    |
| [Webhooks & events](/docs/guides/webhooks)                                      | Endpoint setup, signature verification, retries, and replay |
| [API reference: create a verification](/docs/api/reference/create-verification) | Send-endpoint schema and error details                      |

## Related resources

- [Verify phone numbers at signup](/learn/series/verify-phone-numbers-at-signup) (video)
- [What does OTP mean? One-time passwords explained](/explained/verify/what-does-otp-mean) (answer)
- [Customer verification](/verify-api) (product)
- [Build your first integration](/learn/paths/integration) (course)

[Get an implementation brief](/learn/workspace?topic=verify)
