Skip to content

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.

View .md

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.

terminal
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.

CallWhat it does
GET /v2/webhookThe webhook (null when none) and the v1 sender webhooks still on.
PUT /v2/webhookCreate or replace it. Replacing keeps the secret.
POST /v2/webhook/secretRotate the secret.
DELETE /v2/webhookRemove 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?" } }
FieldMeaning
eventThe event name, also sent in the X-Zavu-Event header.
fromOutbound message events: your address. Inbound events and conversation.new: the contact. Calls: the caller.
toThe other end.
typeThe transport, as in GET /v2/channels.
dataThe 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:

Code
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.

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:

terminal
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.