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

EventWhen it fires
call.startedOutbound call has been queued at the carrier; inbound call has connected.
call.completedCall ended normally. Payload includes duration + disposition.
call.failedCall failed (carrier reject, no-answer ringout, etc.).
campaign.lead.updatedA lead's status changed (PENDING → IN_CALL → REACHED / NO_ANSWER / etc.).
sms.receivedAn inbound SMS arrived on one of your numbers.
appointment.createdThe AI booked an appointment via book_appointment during a call.

Request format

Every event is delivered as an HTTPS POST with:

  • Content-Type: application/json
  • X-ZazaVoice-Event: <event-type> — e.g. call.completed
  • X-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:

  1. Verify it's enabled and subscribed to the right event type.
  2. Verify HTTPS — HTTP URLs are rejected at endpoint creation.
  3. Check your firewall / WAF — some block raw POST from unknown origins. We post from Cloud Run egress IPs (rotating); whitelist broadly or by hostname if your security setup requires it.
  4. Email compliance@zazavoice.com with your endpoint ID and roughly when you expected events. We can replay from delivery logs.