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.completeddoes not mean the call connected. A busy line and a wrong number fire it too. Read the session status forstateandfailureReasonbefore 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.completedlands first. occurredAtis when the analysis ran, not when the call ended. For call timings, read the session’s timestamps.
What arrives
eventIdis your idempotency key. It iscallId#eventType, and a retry can deliver the same event twice. Process once pereventId.campaignIdappears only on campaign calls. On every other call the key is absent, not null.callIdandsessionIdare 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.
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.
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:
{"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 any5xxare retried at 30 seconds, 5 minutes, and 30 minutes. Four attempts total. - Any other 4xx does not retry. A
401or404is 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:
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.
