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.
terminalcurl -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{ "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.
Signature
Every delivery carries X-Zavu-Signature, signed with the webhook's secret:
CodeX-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.
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:
terminalcurl -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.