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

# WhatsApp webhook events

> Subscribe to signed message and number events

Create a WhatsApp webhook endpoint with a 60db API key scoped for WhatsApp. Owner, admin and developer roles may manage subscriptions on an active paid plan.

## Manage subscriptions

| Method | Path | Purpose |
| - | - | - |
| GET | `/whatsapp/webhooks` | List workspace endpoints with pagination |
| POST | `/whatsapp/webhooks` | Create an endpoint |
| DELETE | `/whatsapp/webhooks/{id}` | Disable an endpoint and stop further delivery |

```json theme={null}
{
  "url": "https://your-app.example/webhooks/whatsapp",
  "events": ["message.received", "message.status", "number.connected"]
}
```

The URL must use public HTTPS on port 443. Credentials, fragments and destinations resolving to private or internal addresses are rejected. Redirects are not followed.

The creation response includes `data.signing_secret` **once**. Save it on your receiving server. List responses never include this secret.

The dashboard's **WhatsApp → Webhooks** form offers message, status, connection and assistant-failure selection. Select at least one event. Connection alerts and assistant failures are optional; they are not added to the default message/status/connected selection. Save or acknowledge the one-time secret before creating another endpoint. The additional `number.updated`, `message.mirrored` and `message.updated` events are currently selectable through the API.

## Event envelope

```json theme={null}
{
  "id": "33333333-3333-4333-8333-333333333333",
  "type": "message.received",
  "created_at": "2026-10-07T05:00:00.000Z",
  "data": {
    "number_id": "11111111-1111-4111-8111-111111111111",
    "conversation_id": "44444444-4444-4444-8444-444444444444",
    "message": {"id": "55555555-5555-4555-8555-555555555555", "type": "text", "direction": "inbound"}
  }
}
```

This is an illustrative envelope; the message object also carries its content, status and timestamps.

| Event | Data |
| - | - |
| `message.received` | Number UUID, conversation UUID and inbound message |
| `message.mirrored` | Number UUID, conversation UUID and outbound Business app message; no duplicate provider send |
| `message.updated` | Number UUID, conversation UUID and message after deferred history media, app edit or revocation |
| `message.status` | Number UUID and updated outbound `message`, including its status |
| `number.connected` | Public number record |
| `number.disconnected` | Public number record after local disconnect; tokens are omitted |
| `number.action_required` | Public number record after confirmed invalid/missing permission/selected-asset access or a supported terminal account event, including `connection_error` |
| `number.updated` | `{field, number}`: supported provider management field, `history` or `sync`, and current public number record, including `provider_state` and `coexistence_state` |
| `assistant.failed` | `number_id`, `conversation_id`, `run_id` and failure `code`; fetch the run for saved stages and credits used |

`message.status` also reports local dispatch failures, cancellation and unconfirmed sends, including broadcast messages. These terminal dispatch state changes and their events are persisted together. Internal request hashes are omitted from the message payload. A cancellation may leave an interrupted send unconfirmed; no automatic resend follows.

Subscribe to the number lifecycle events explicitly through the API or dashboard. Stopping a number and recording its normalized event occur in the same transaction. An old token failure does not produce a revocation event for a successfully replaced token.

Subscribe to `number.updated` through the API for supported account review/disable/deletion/offboarding, verified partner app/portfolio changes, policy violation/restriction, verified-name and quality changes. Its `field` identifies the native provider signal. Ordinarily `number.provider_state[field].at` records its epoch-second timestamp; `ACCOUNT_VIOLATION` and `ACCOUNT_RESTRICTION` both use `field: "account_update"` and store independent state in `account_violation` and `account_restriction`. Restriction entries retain their own `at`, expiry and optional remediation, so a later unrelated account notice cannot erase them. Multiple restriction changes in one receipt can each produce a distinct updated snapshot.

These metadata changes and events commit with the provider receipt. A terminal event may produce both `number.action_required` and `number.updated`; warning/temporary restriction records do not clear credentials or emit `number.action_required`. Account reinstatement/reconnection and app/partner-added notices never emit `number.connected` or restart paused work. Later supported notices in the same receipt persist after its stop without admitting new inbound messages. See [number resources](https://docs.60db.ai/api-reference/whatsapp/resources.md) for supported action guards, errors and required partner binding. Unknown variants are ignored; these events do not import coexistence history or certify arbitrary provider asset/app-review changes.

Separate verified Business app callbacks now mirror supported history, contacts and app messages. `number.updated` with `field: "history"` carries current import progress or declined sharing in `number.coexistence_state.history`. Historical originals do not emit `message.received` or start AI. `message.mirrored` represents a new app-originated outbound message and a human handoff; `message.updated` replaces the displayed record after media hydration, edit or irreversible revocation. Its message has `app_updated_at`/`app_revoked` when applicable. Internal request hashes remain omitted. Receipt, message/state and normalized event persistence are atomic; deduplicate event IDs and refetch records when callbacks arrive out of order. [Callback scope and unfinished onboarding](https://docs.60db.ai/api-reference/whatsapp/resources.md#business-app-callback-support)

`number.updated` with `field: "sync"` carries durable contact/history initiation transitions in `number.coexistence_state.sync`. `accepted` retains the support request ID but does not certify sharing/import completion; `unconfirmed` is never automatically dispatched again. A caller should refetch the number's current state rather than assume delivery order. Flow-specific Business app offboarding can also record `coexistence_state.offboarded_at` with an account update; it cannot stop an ordinary or differently bound number. [Initiation states and recovery](https://docs.60db.ai/api-reference/whatsapp/resources.md#one-time-business-app-synchronization)

## Verify the signature

Headers include `X-60db-Event-ID`, `X-60db-Timestamp`, `X-60db-Signature` and `X-60db-Delivery-Attempt`.

The signature is `v1=` followed by the hexadecimal HMAC-SHA256 of **timestamp + "." + raw request body**, using the signing secret's UTF-8 bytes. Do not decode the hexadecimal-looking secret into binary and do not parse/re-serialize the body before verification.

```js theme={null}
import crypto from "node:crypto";

function verifyWhatsAppEvent(rawBody, timestamp, signature, secret) {
  if (!/^\d+$/.test(timestamp || "") || !/^v1=[a-f0-9]{64}$/.test(signature || "")) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = "v1=" + crypto.createHmac("sha256", secret)
    .update(timestamp).update(".").update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
```

Use a five-minute timestamp tolerance and deduplicate by event `id`. Persist the verified event before acknowledging it with a `2xx` response. Process your application's work asynchronously after acknowledgement.

## Retries

Timeouts, network errors, HTTP `408`, `425`, `429` and server errors retry with exponential delay, for up to eight attempts. Other `4xx` responses are terminal. Delivery can be repeated after a worker restart, so processing must be idempotent. Ordering is not guaranteed.

This signing secret is for **60db → your application** deliveries. Meta → 60db receipt verification uses the platform's Meta app secret and is handled by 60db.


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