Create or replace the project webhook
Create or replace the webhook. url must be HTTPS. The signature scheme defaults to v2. A webhook cannot move straight from v1 to v2, or from v2 to v1 (400): a receiver verifying the current scheme would break on the next delivery, so both directions go through v1+v2. Replacing keeps the secret.
Pass disableSenderWebhooks: true to switch off the /v1 sender webhooks of this project in the same call, so each event arrives once.
curl --request PUT \
--url https://api.zavu.dev/v2/webhook \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"url": "<string>",
"events": [],
"active": true,
"signatureVersion": "v2",
"disableSenderWebhooks": false
}
'import requests
url = "https://api.zavu.dev/v2/webhook"
payload = {
"url": "<string>",
"events": [],
"active": True,
"signatureVersion": "v2",
"disableSenderWebhooks": False
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
url: '<string>',
events: [],
active: true,
signatureVersion: 'v2',
disableSenderWebhooks: false
})
};
fetch('https://api.zavu.dev/v2/webhook', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.zavu.dev/v2/webhook",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'url' => '<string>',
'events' => [
],
'active' => true,
'signatureVersion' => 'v2',
'disableSenderWebhooks' => false
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.zavu.dev/v2/webhook"
payload := strings.NewReader("{\n \"url\": \"<string>\",\n \"events\": [],\n \"active\": true,\n \"signatureVersion\": \"v2\",\n \"disableSenderWebhooks\": false\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.put("https://api.zavu.dev/v2/webhook")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"<string>\",\n \"events\": [],\n \"active\": true,\n \"signatureVersion\": \"v2\",\n \"disableSenderWebhooks\": false\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.zavu.dev/v2/webhook")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"<string>\",\n \"events\": [],\n \"active\": true,\n \"signatureVersion\": \"v2\",\n \"disableSenderWebhooks\": false\n}"
response = http.request(request)
puts response.read_body{
"webhook": {
"url": "<string>",
"events": [
"message.queued"
],
"active": true,
"signatureVersion": "v2",
"secret": "<string>"
},
"senderWebhooks": [
{
"address": "<string>",
"url": "<string>",
"v1SenderId": "<string>"
}
],
"disabledSenderWebhooks": 123
}{
"webhook": {
"url": "<string>",
"events": [
"message.queued"
],
"active": true,
"signatureVersion": "v2",
"secret": "<string>"
},
"senderWebhooks": [
{
"address": "<string>",
"url": "<string>",
"v1SenderId": "<string>"
}
],
"disabledSenderWebhooks": 123
}{
"code": "invalid_request",
"message": "<string>",
"details": {
"errors": [
{
"path": [
"<string>"
],
"message": "<string>",
"code": "<string>"
}
]
}
}{
"code": "invalid_request",
"message": "<string>",
"details": {
"errors": [
{
"path": [
"<string>"
],
"message": "<string>",
"code": "<string>"
}
]
}
}Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
HTTPS only.
Any event in WebhookEvent. An unknown value is refused with 400 and details.allowed.
1Type of event that triggers the webhook.
Message lifecycle events:
message.queued: Message created and queued for sending.data.status=queuedmessage.sent: Message accepted by the provider.data.status=sentmessage.delivered: Message delivered to recipient.data.status=deliveredmessage.read: Message was read by the recipient (WhatsApp only).data.status=readmessage.failed: Message failed to send.data.status=failed
Inbound events:
message.inbound: New message received from a contact.data.conversationId, the inbox thread id (deep-link withhttps://dashboard.zavu.dev/{locale}/inbox?conv={conversationId}), is sent on email messages only, and isnullthere on the first message of a brand-new thread. On every other channel it is absent: read it fromGET /v2/messages/{messageId}, which always has it, or fromconversation.newfor a new contact. Reactions are delivered asmessage.inboundwithmessageType='reaction'. When the contact replied to (quoted) an earlier message,data.contentcarries the reply context:replyToMessageId,replyToProviderMessageId,replyToFrom,replyToText, andreplyToMessageType.data.providerTimestampis the provider's original receive time in Unix milliseconds (the moment the channel received the message from the contact — WhatsApp, Telegram, Instagram, Messenger;nullfor SMS and email). Compare it against the top-leveltimestamp(when Zavu dispatched the webhook) to detect and ignore delayed deliveries. When the conversation was opened from a Click-to-WhatsApp ad or post,data.referralcarries the ad attribution — includingctwaClid, the identifier Meta's Conversions API needs to credit a conversion back to that ad. WhatsApp only, and only on the first message of the thread: it is absent from every later message, so persist it when it arrives.message.unsupported: Received a message type that is not supported
Broadcast events:
broadcast.status_changed: Broadcast status changed (pending_review, approved, rejected, sending, completed, cancelled)
Other events:
conversation.new: New conversation started with a contact.datacarriesconversationId(the inbox thread id — deep-link withhttps://dashboard.zavu.dev/{locale}/inbox?conv={conversationId}), thephoneNumberoremailkey,channel,firstMessageId,firstMessageText, andprofileName.template.status_changed: WhatsApp template approval status changed.datacarriestemplateId,name,previousStatus,currentStatus,rejectionReason, andcategory— the category Meta currently bills the template under. Meta can recategorize a template (typicallyUTILITYtoMARKETING) at approval or long afterwards, which changes what each message costs;categoryis how that reaches you. A recategorization with no status change is delivered as this same event, so comparecategoryagainst what you hold rather than only reacting tocurrentStatus.
Partner events:
invitation.status_changed: A partner invitation's stored status changed: toin_progress,completed,failed,cancelled, or back topendingwhen it is resent from the dashboard. A change to the same status sends nothing, and expiry is not a stored change, so no event is sent when an invitation expires. Delivered to the project that created the invitation; a parent project does not receive its sub-accounts' events.datacarriesinvitationId,clientName,clientEmail,connectionType(whatsapp_wabaormessenger),previousStatus, andcurrentStatus. Oncompletedit also carriesconnectedAccount(channel,id,name), the WhatsApp number or Facebook Page that was linked, and, for WhatsApp,wabaAccountId. Onfailedit carriesfailureReason; the invitation link stays usable, so a client can retry it.
Voice Agent events:
For every voice event, data carries callId, direction, from, to, status, durationSeconds, endReason, and transcriptAvailable. The terminal events (call.completed, call.failed) additionally carry cost — what the call was billed, in USD, combining telephony and the managed voice pipeline — and currency. They are dispatched after the call is charged, so cost is populated rather than zero; telephony can still be settling on an outbound call, in which case GET /v1/calls/{callId} holds the reconciled figure.
call.initiated: An outbound call was created and is dialing, or an inbound call was received.data.status=ringingcall.answered: The call was answered and the voice agent is connected.data.status=in_progresscall.completed: The call ended after a conversation.data.status=completed;durationSecondsandendReasondescribe how it ended, andtranscriptAvailableindicates whether a transcript can be fetched.call.failed: The call could not be completed (busy, no answer, canceled, or an error).data.statusis the terminal status andendReasonexplains the cause.
Custom domain events:
domain.verified: A custom email domain passed verification (DKIM, and SPF/DMARC/MAIL FROM if enhanced records are enabled)domain.failed: A custom email domain failed verification or is partially verified
message.queued, message.sent, message.delivered, message.read, message.failed, message.inbound, message.unsupported, broadcast.status_changed, conversation.new, template.status_changed, invitation.status_changed, call.initiated, call.answered, call.completed, call.failed, domain.verified, domain.failed Which X-Zavu-Signature scheme this receiver is sent. The header is t=<unix seconds> followed by the signatures.
v1:v1=HMAC_SHA256(secret, body).v2:v2=HMAC_SHA256(secret, "{t}.{body}"). The current scheme, and the default for a new /v2 webhook. It signs the timestamp together with the body.v1+v2: both signatures, sharing onet. The migration setting: a receiver reading either one works.
Moving straight between v1 and v2, in either direction, returns 400. Set v1+v2 first. See https://docs.zavu.dev/guides/receiving-messages/signature-migration
v1, v1+v2, v2 "v2"
Switch off the /v1 sender webhooks of this project in the same call, so each event arrives once. They are only switched off (webhookActive: false on /v1); URL, events and secret stay, so /v1 can turn one back on. Off by default: the /v2 webhook is added next to them and both receive every event until you decide. senderWebhooks on the response lists what is still on.
Response
Replaced. secret is not returned.
The one webhook for everything in the project. Stored apart from the /v1 project webhook (/v1/invitations/webhook); neither API reads or writes the other's.
Show child attributes
Show child attributes
Show child attributes
Show child attributes
How many /v1 sender webhooks this call switched off.
curl --request PUT \
--url https://api.zavu.dev/v2/webhook \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"url": "<string>",
"events": [],
"active": true,
"signatureVersion": "v2",
"disableSenderWebhooks": false
}
'import requests
url = "https://api.zavu.dev/v2/webhook"
payload = {
"url": "<string>",
"events": [],
"active": True,
"signatureVersion": "v2",
"disableSenderWebhooks": False
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
url: '<string>',
events: [],
active: true,
signatureVersion: 'v2',
disableSenderWebhooks: false
})
};
fetch('https://api.zavu.dev/v2/webhook', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.zavu.dev/v2/webhook",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'url' => '<string>',
'events' => [
],
'active' => true,
'signatureVersion' => 'v2',
'disableSenderWebhooks' => false
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.zavu.dev/v2/webhook"
payload := strings.NewReader("{\n \"url\": \"<string>\",\n \"events\": [],\n \"active\": true,\n \"signatureVersion\": \"v2\",\n \"disableSenderWebhooks\": false\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.put("https://api.zavu.dev/v2/webhook")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"<string>\",\n \"events\": [],\n \"active\": true,\n \"signatureVersion\": \"v2\",\n \"disableSenderWebhooks\": false\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.zavu.dev/v2/webhook")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"<string>\",\n \"events\": [],\n \"active\": true,\n \"signatureVersion\": \"v2\",\n \"disableSenderWebhooks\": false\n}"
response = http.request(request)
puts response.read_body{
"webhook": {
"url": "<string>",
"events": [
"message.queued"
],
"active": true,
"signatureVersion": "v2",
"secret": "<string>"
},
"senderWebhooks": [
{
"address": "<string>",
"url": "<string>",
"v1SenderId": "<string>"
}
],
"disabledSenderWebhooks": 123
}{
"webhook": {
"url": "<string>",
"events": [
"message.queued"
],
"active": true,
"signatureVersion": "v2",
"secret": "<string>"
},
"senderWebhooks": [
{
"address": "<string>",
"url": "<string>",
"v1SenderId": "<string>"
}
],
"disabledSenderWebhooks": 123
}{
"code": "invalid_request",
"message": "<string>",
"details": {
"errors": [
{
"path": [
"<string>"
],
"message": "<string>",
"code": "<string>"
}
]
}
}{
"code": "invalid_request",
"message": "<string>",
"details": {
"errors": [
{
"path": [
"<string>"
],
"message": "<string>",
"code": "<string>"
}
]
}
}