Skip to main content
A call ends, we analyze it, and we POST what happened to your endpoint. That is the whole model: configure a webhook connection once per project, and every call reports in — inbound, web, API dials, and campaigns alike.
This page is about event webhooks: us telling your system a call finished. Webhook tools are the other direction — the assistant calling your API mid-conversation and speaking the answer. Both arrive signed the same way, but you set them up separately. A webhook gets every call event and never a tool call. A tool endpoint gets every tool call and never an event.

The four events

Four things to know before you build on these:
  • call.completed does not mean the call connected. A busy line and a wrong number fire it too. Read the session status for state and failureReason before treating it as a conversation.
  • No scorecard means no call.graded, ever. Grading rides the scorecard: assistant-level for direct calls, campaign-level for campaign calls.
  • Emit order is fixed, arrival order is not. Events go out as completed, graded, conversion, callback, but each one retries independently. Don’t assume call.completed lands first.
  • occurredAt is when the analysis ran, not when the call ended. For call timings, read the session’s timestamps.

What arrives

  • eventId is your idempotency key. It is callId#eventType, and a retry can deliver the same event twice. Process once per eventId.
  • campaignId appears only on campaign calls. On every other call the key is absent, not null.
  • callId and sessionId are the same string today. Either is the id every session endpoint takes.
  • The payload is thin on purpose. No transcript, no recording, no per-rule breakdown, no phone number. The event tells you when to look, and the session detail is where you look.
Full payloads for all four events are on the event reference.

Subscribing

Add a webhook on the portal’s Webhooks page: a name, the HTTPS URL, and a signing secret. Generate makes a strong secret for you. Store it on your server. Every delivery is signed with it.
  • Adding the webhook is the opt-in. Every webhook in the project receives every event for every call. There is no per-event filter.
  • The URL must be HTTPS and publicly routable. Private, loopback, and link-local addresses are rejected at create time.
  • To rotate the secret, add a webhook with the new one, then delete the old one.
Set the signing secret when you add the webhook. Without one, deliveries still go out, signed with an empty key, which is no signature at all.

Verifying the signature

Every delivery carries one header:
v1 is HMAC-SHA256 over `${t}.${body}` with your signingSecret, hex-encoded. t is unix seconds and sits inside the signed material, which is what makes replay detection work. Reject anything older than five minutes and compare in constant time:
Sign over the raw request body, before any JSON parsing touches it. One more POST to expect: the Test button on the Webhooks page sends {"type": "ping", "connectionId": "…"}, signed the same way but with no envelope. Handle it before you switch on eventType, and answer 2xx.

Delivery and retries

  • Answer fast with any 2xx. The response body is ignored. The deadline is 10 seconds, so queue the work rather than doing it inline.
  • Transient failures retry. Timeouts, network errors, 408, 429, and any 5xx are retried at 30 seconds, 5 minutes, and 30 minutes. Four attempts total.
  • Any other 4xx does not retry. A 401 or 404 is a permanent failure and the delivery dies on the first attempt.
  • Dead deliveries land in Delivery failures on the Webhooks page, with the status code, the reason, and the attempt count, kept for 90 days. There is no event for a failed delivery.

Testing from localhost

We only call public HTTPS URLs. localhost and private addresses are rejected when you save the connection, so put a tunnel in front of your dev server:
The tunnel prints a public address like https://a1b2c3d4.ngrok-free.app. The url is that address plus your route. Add a webhook with it exactly as in Subscribing, with the secret your local server checks. Nothing to wire after that. Every webhook connection receives every call event. Make a call, and call.completed lands a few seconds after hangup. For a faster first check, Test sends a signed ping with no call at all. Every request shows up in ngrok’s inspector at http://127.0.0.1:4040, headers and body included. Replay one from there while you fix the handler.
  • Free ngrok URLs change every restart. Claim ngrok’s free static domain, so the URL you saved keeps working.
  • The tunnel is public. Keep the signature check on, even on your laptop.