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

# Webhook

> One webhook per project in API v2: configure it, read its payload, verify its signature, and switch off the v1 sender webhooks when you are ready.

## One webhook for the project

v2 has one webhook per project. It receives every event it is subscribed to,
from every address: messages, conversations, calls, templates, campaigns,
domains and invitations.

```bash theme={null}
curl -X PUT https://api.zavu.dev/v2/webhook \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.example.com/webhooks/zavu",
    "events": ["message.inbound", "message.delivered", "message.failed", "conversation.new"]
  }'
```

The `201` that creates it returns `webhook.secret` once. Keep it: later reads
do not return it. `POST /v2/webhook/secret` rotates it, and the old one stops
working at once.

| Call | What it does |
| - | - |
| `GET /v2/webhook` | The webhook (`null` when none) and the v1 sender webhooks still on. |
| `PUT /v2/webhook` | Create or replace it. Replacing keeps the secret. |
| `POST /v2/webhook/secret` | Rotate the secret. |
| `DELETE /v2/webhook` | Remove it and its secret. |

`url` must be HTTPS. An event name that does not exist is refused with `400`,
with the valid names in `details.allowed`.

## Payload

```json theme={null}
{
  "id": "evt_1736850000000_abc123",
  "event": "message.inbound",
  "timestamp": 1736850000000,
  "projectId": "jx7xyz789ghi012",
  "from": "+56912345678",
  "to": "+13125551212",
  "type": "whatsapp",
  "data": {
    "messageId": "jx7744x3wckc9cwy0j9c8g6v0h7z7shs",
    "from": "+56912345678",
    "to": "+13125551212",
    "channel": "whatsapp",
    "messageType": "text",
    "text": "Hi, is my order on its way?"
  }
}
```

| Field | Meaning |
| - | - |
| `event` | The event name, also sent in the `X-Zavu-Event` header. |
| `from` | Outbound message events: your address. Inbound events and `conversation.new`: the contact. Calls: the caller. |
| `to` | The other end. |
| `type` | The transport, as in `GET /v2/channels`. |
| `data` | The same event data v1 sends, without `senderId`. |

No sender id appears anywhere in the payload. Project-level events
(`invitation.*`, `domain.*`, `broadcast.*`) are not about one address: read
`data` for them.

The event names and their `data` are listed under `WebhookEvent` in the
reference, and explained in [Webhook events](/guides/receiving-messages/events).

## Signature

Every delivery carries `X-Zavu-Signature`, signed with the webhook's secret:

```
X-Zavu-Signature: t=1786113454,v2=b4b2b61cdcdb241e4270298a08c3c13c964642df...
```

`v2` is `HMAC_SHA256(secret, "{t}.{body}")` over the raw body, and it is the
default for a new v2 webhook. `signatureVersion` can also be `v1` or `v1+v2`.
Moving straight between `v1` and `v2` is refused with `400`: go through
`v1+v2`. Verification code is in [Security](/guides/receiving-messages/security).

## Retries

A delivery that does not get a `2xx` is retried, up to 5 attempts in total:
after 1 minute, 5 minutes, 15 minutes and 1 hour. Each event has its own `id`;
use it to skip a delivery you already processed.

## Next to v1 webhooks

Setting up the v2 webhook is the opt-in. From then on each event goes to it **in
addition to** any v1 sender webhook or v1 project webhook subscribed to the same
event. They are separate deliveries with different event ids, and nothing is
deduplicated.

`senderWebhooks` on `GET /v2/webhook` lists the v1 sender webhooks still on.
Once your v2 receiver works, switch them off in the same call:

```bash theme={null}
curl -X PUT https://api.zavu.dev/v2/webhook \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.example.com/webhooks/zavu",
    "events": ["message.inbound", "message.delivered", "message.failed", "conversation.new"],
    "disableSenderWebhooks": true
  }'
```

They are only switched off. URL, events and secret stay, so v1 can turn one back
on. `DELETE /v2/webhook` does not turn them back on.

The v2 webhook is stored apart from the v1 project webhook
(`/v1/invitations/webhook`). Neither API reads or writes the other's.


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