Webhooks
Webhooks are how the platform calls you. When a call completes, a lead status changes, an inbound SMS arrives, or an appointment is booked, ZazaVoice POSTs a signed JSON event to your URL — so your product reacts in real time without polling.
If you're building a customer-facing product on top of ZazaVoice, this is where the loop closes: you dispatched a call via the API, the AI ran the conversation, the result lands back at your server, and you write it into your own DB and decide what happens next.
Setting up a webhook endpoint
Dashboard → Settings → Webhooks → New endpoint. Provide:
- URL — must be HTTPS, publicly reachable.
- Event types — one or more of the supported events below.
On save, we generate a signing secret (whsec_…). It's shown once;
store it server-side. Every event posted to your endpoint will be signed
with this secret so you can verify authenticity.
Event types
| Event | When it fires |
|---|---|
call.started | Outbound call has been queued at the carrier; inbound call has connected. |
call.completed | Call ended normally. Payload includes duration + disposition. |
call.failed | Call failed (carrier reject, no-answer ringout, etc.). |
campaign.lead.updated | A lead's status changed (PENDING → IN_CALL → REACHED / NO_ANSWER / etc.). |
sms.received | An inbound SMS arrived on one of your numbers. |
appointment.created | The AI booked an appointment via book_appointment during a call. |
Request format
Every event is delivered as an HTTPS POST with:
Content-Type: application/jsonX-ZazaVoice-Event: <event-type>— e.g.call.completedX-ZazaVoice-Signature: <hex hmac>— HMAC-SHA256 of the request body with your endpoint's signing secret as the key.
Body shape:
{
"event": "campaign.lead.updated",
"data": {
"leadId": 4421,
"campaignId": 12,
"targetName": "Alex Chen",
"email": "alex@acme.io",
"variables": {
"AMOUNT": "5000",
"CASE_NUMBER": "CASE-7741"
},
"status": "REACHED",
"lastDisposition": "INTERESTED"
},
"timestamp": "2026-05-21T15:30:00Z"
}
The data object's shape depends on the event type.
Verifying signatures
The X-ZazaVoice-Signature header is a hex-encoded HMAC-SHA256 of the
raw request body using your endpoint's signing secret as the key.
Node.js example
import crypto from 'node:crypto';
function verifySignature(rawBody, headerSig, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(headerSig, 'hex'),
);
}
app.post('/webhooks/zazavoice', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.header('X-ZazaVoice-Signature');
if (!verifySignature(req.body, sig, process.env.ZAZAVOICE_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const payload = JSON.parse(req.body.toString());
// …handle the event
res.sendStatus(200);
});
Python example
import hmac, hashlib
def verify_signature(raw_body: bytes, header_sig: str, secret: str) -> bool:
expected = hmac.new(
secret.encode("utf-8"),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, header_sig)
Always verify the signature before trusting the payload. Without it, anyone who guesses your URL can post fake events.
Retry and delivery semantics
We treat any HTTP 2xx response as a successful delivery.
On any non-2xx response (or a connection failure / timeout), we retry:
- After 30 seconds.
- After 5 minutes.
- After 30 minutes.
If all three retries fail, the event is marked failed and we give up. We don't queue indefinitely.
To recover from outages, design your handler to be idempotent — the same event ID can arrive more than once if you 5xx'd the first delivery but our retry succeeded after you came back.
We don't guarantee ordering. If you receive call.completed before
call.started for the same call, that's allowed; reconcile by call id.
Disabling / deleting an endpoint
Dashboard → Settings → Webhooks → endpoint → Disable / Delete. Disable
stops delivery without removing the configuration so you can re-enable
later. Delete removes it permanently and invalidates the signing secret.
Testing your endpoint
There's no built-in "send test event" button yet (planned). For now, test by triggering the underlying action: dispatch a test call, send an SMS to yourself, run a tiny campaign with one lead. We'll add a test-fire button in the dashboard soon.
If your endpoint isn't receiving events you expected:
- Verify it's enabled and subscribed to the right event type.
- Verify HTTPS — HTTP URLs are rejected at endpoint creation.
- Check your firewall / WAF — some block raw
POSTfrom unknown origins. We post from Cloud Run egress IPs (rotating); whitelist broadly or by hostname if your security setup requires it. - Email compliance@zazavoice.com with your endpoint ID and roughly when you expected events. We can replay from delivery logs.