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

# Event catalog

> Every webhook event Tawked sends: when it fires, who receives it, its payload and how to handle it.

Every event travels in the same envelope, `{ "event", "data", "sent_at" }`, signed the same way. [Webhooks](/webhooks) covers the setup, the signature, the timeout and the retries; this page covers what each event means.

## Index

| Event | Fires when | Reaches |
| - | - | - |
| [`verification.verified`](#when-a-code-is-verified) | A `check` matched the code. | The application's webhook; the partner webhook when subscribed |
| [`verification.failed`](#when-a-verification-fails) | The attempts ran out without a matching code. | The application's webhook; the partner webhook when subscribed |
| [`verification.expired`](#when-a-code-expires) | The code expired unused. | The application's webhook; the partner webhook when subscribed |
| [`message.sent`](#message-events) | Meta accepted a WhatsApp message. | The application's webhook |
| [`message.delivered`](#message-events) | The message reached the phone. | The application's webhook |
| [`message.read`](#message-events) | The message was read. | The application's webhook |
| [`message.failed`](#message-events) | Meta reported the message as failed. | The application's webhook |
| [`service.approved`](#partner-events) | A client application was approved. | The partner webhook, by default |
| [`service.rejected`](#partner-events) | A client application was rejected. | The partner webhook, by default |
| [`service.suspended`](#partner-events) | A client application was suspended. | The partner webhook, by default |
| [`service.resumed`](#partner-events) | A client application was switched back on. | The partner webhook, by default |
| [`service.paused`](#partner-events) | The protection guard paused a client application. | The partner webhook, by default |
| [`service.unpaused`](#partner-events) | The pause was lifted. | The partner webhook, by default |
| [`service.review_requested`](#partner-events) | A brand change sent a client application back to review. | The partner webhook, by default |
| [`service.archived`](#partner-events) | A client application was archived. | The partner webhook, by default |
| [`service.restored`](#partner-events) | A client application came back from the archive. | The partner webhook, by default |
| [`balance.low`](#partner-events) | A charge took the partner's balance below its threshold. | The partner webhook, by default |

## Handling any event

* **Verify the signature first**, over the raw body. See [Verifying the signature](/webhooks#verifying-the-signature).
* **Answer `2xx` within 5 seconds**, then do the work from a queue.
* **Deduplicate** on `data.id` together with `event`. The same event can arrive twice.
* **Do not rely on order.** Compare the status in `data` with what you stored, and never move a record backwards.
* **Skip test events.** An envelope with `"test": true` is a rehearsal from the console's **Send a test event**.
* **Ignore what you do not know.** New events and new `data` fields can be added; an unknown event deserves a `2xx` and nothing else.

## Verification events

The data is the verification, in the same shape for the three events. A partner's copy adds `external_id`.

| Field | Meaning |
| - | - |
| `id` | The verification's id, as `start` answered it. |
| `status` | `verified`, `failed` or `expired`. |
| `application_id` | The application the code was sent for. |
| `reference` | Your `reference` from `start`, or `null`. |
| `channel` | `sms`. |
| `to` | The destination in E.164, `+9665...`. |
| `mode` | The key it was sent with: `live`, `test`, `partner` or `partner_test`. |
| `attempts` | Codes checked so far, right or wrong. |
| `expires_at`, `verified_at`, `created_at` | ISO 8601 in UTC, with milliseconds. `verified_at` is `null` unless verified. |

Codes sent with a test key fire these events too, with `mode: "test"`. Canceling a verification fires no event.

### When a code is verified

`verification.verified` fires on the `check` that matched the code, once per verification. A later `check` of the same id answers `verified` again but sends no second event.

```json theme={"dark"}
{
  "event": "verification.verified",
  "data": {
    "id": "b7e5c2b0-9c1a-4e2f-8f2a-3a6b0e9d1c44",
    "status": "verified",
    "application_id": "c2a10f3e-8b7a-4d2e-9c1a-1a2b3c4d5e6f",
    "reference": "order-42",
    "channel": "sms",
    "to": "+966551234567",
    "mode": "live",
    "attempts": 1,
    "expires_at": "2026-09-01T12:34:56.789Z",
    "verified_at": "2026-09-01T12:31:02.145Z",
    "created_at": "2026-09-01T12:29:56.789Z"
  },
  "sent_at": "1788265862201"
}
```

**Handle it** by marking the action behind `reference` as confirmed, if your own `check` call has not done so already. Your server already knows the outcome from the `check` answer; the event matters when another service makes the check, or as a record.

### When a verification fails

`verification.failed` fires when a wrong code uses the last attempt (`max_attempts`, 3 by default), or when a `check` finds the attempts already used. The code can no longer be verified.

```json theme={"dark"}
{
  "event": "verification.failed",
  "data": {
    "id": "b7e5c2b0-9c1a-4e2f-8f2a-3a6b0e9d1c44",
    "status": "failed",
    "application_id": "c2a10f3e-8b7a-4d2e-9c1a-1a2b3c4d5e6f",
    "reference": "order-42",
    "channel": "sms",
    "to": "+966551234567",
    "mode": "live",
    "attempts": 3,
    "expires_at": "2026-09-01T12:34:56.789Z",
    "verified_at": null,
    "created_at": "2026-09-01T12:29:56.789Z"
  },
  "sent_at": "1788265862201"
}
```

A send that fails at `start` (`502 send_failed`) fires no event: the API answer is the news.

**Handle it** by refusing the action behind `reference`. Repeated failures from one number or one address are a fraud signal worth logging.

### When a code expires

`verification.expired` fires when a code reaches `expires_at` unused. A `check`, a `GET /v1/verify/{id}` or a `cancel` that finds the expiry fires it at once; otherwise a sweep finds it, every ten minutes, at least five minutes after the expiry. Expect it up to about a quarter of an hour late.

```json theme={"dark"}
{
  "event": "verification.expired",
  "data": {
    "id": "b7e5c2b0-9c1a-4e2f-8f2a-3a6b0e9d1c44",
    "status": "expired",
    "application_id": "c2a10f3e-8b7a-4d2e-9c1a-1a2b3c4d5e6f",
    "reference": "order-42",
    "channel": "sms",
    "to": "+966551234567",
    "mode": "live",
    "attempts": 0,
    "expires_at": "2026-09-01T12:34:56.789Z",
    "verified_at": null,
    "created_at": "2026-09-01T12:29:56.789Z"
  },
  "sent_at": "1788265862201"
}
```

**Handle it** as a cleanup signal, never as the only timeout: your sign-in screen should count down from `expires_at` itself.

## Message events

`message.sent`, `message.delivered`, `message.read` and `message.failed` follow a WhatsApp template message sent through Tawked Notifications, from the API or from the console. They reach the application's own webhook only; a partner's client applications send no WhatsApp.

```json theme={"dark"}
{
  "event": "message.delivered",
  "data": {
    "id": "0f4c9c0e-6d2b-4b8a-9c3e-7a1d2e3f4a5b",
    "status": "delivered",
    "application_id": "c2a10f3e-8b7a-4d2e-9c1a-1a2b3c4d5e6f",
    "to": "+9665•• ••• 000",
    "template": "order_received_v1",
    "lang": "ar",
    "reference": "order-1042",
    "price_halalas": 0,
    "error": null,
    "occurred_at": "2026-09-06T09:00:04.512Z",
    "created_at": "2026-09-06T09:00:01.087Z"
  },
  "sent_at": "1788685205201"
}
```

| Field | Meaning |
| - | - |
| `id` | The message's id, as `POST /v1/whatsapp/messages` answered it. |
| `status` | The status this event announces. |
| `to` | The destination, masked. Match on `id` or `reference`, not on the number. |
| `template`, `lang` | The template's name and language. |
| `reference` | Your `reference` from the send, or `null`. |
| `price_halalas` | The fee charged for the message. |
| `error` | On `message.failed`, `{ "code", "message" }` as Meta reported them; `null` otherwise. |
| `occurred_at` | When the message reached this status. |
| `created_at` | When the message was sent. |

How the events behave:

* **They arrive within about a minute** of Meta reporting the status, and a message is followed for 48 hours after its send.
* **A status only moves forward**: `sent`, then `delivered`, then `read`. A step can be skipped: a message read before its delivery was reported fires `message.read` alone.
* **`read` and `failed` are final.** Nothing follows them.
* **A send refused at once** answers `502 send_failed` to the API call and fires no `message.failed`.

**Handle them** by storing the latest status per `id` and ignoring an event whose status is behind the stored one. [Sending messages](/whatsapp/messages#statuses-and-webhooks) has the status lifecycle.

## Partner events

The `service.*` events and `balance.low` reach a partner's webhook only, for the client applications it provisions and for its balance. Each `service.*` event carries `service_id`, `application_id` and the partner's `external_id`.

```json theme={"dark"}
{
  "event": "service.approved",
  "data": {
    "service_id": "c2a10f3e-8b7a-4d2e-9c1a-1a2b3c4d5e6f",
    "application_id": "c2a10f3e-8b7a-4d2e-9c1a-1a2b3c4d5e6f",
    "external_id": "store_88",
    "status": "active"
  },
  "sent_at": "1788265862201"
}
```

[Partner events](/partners/events) has each one with its extra fields, the `reason_code` of a rejection, the subscriptions and the feed to poll instead.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.