Developers
API: post-call webhooks
Receive call results in your backend.
What the post-call webhook is
When a call ends, VoiceSquad sends one HTTP POST to a URL you own, carrying the whole result: how the call went, the full transcript, the summary, the sentiment, and any variables the agent extracted from the conversation.
This is how call results reach your systems. There is no endpoint to poll a call's status or pull a transcript with API credentials, so if a CRM record needs updating or a follow-up needs queueing, the webhook is what drives it. The console shows everything else.
When it fires
Once per call, after the post-call analysis has finished, which is a few seconds after hangup rather than the instant the line drops. The delay is deliberate: it means the payload already contains the summary and the evaluated result instead of a bare transcript you would have to interpret yourself.
It fires for live phone calls, web calls, test chats and simulation runs, so treat the payload as potentially describing a test rather than production traffic. If an agent has no webhook URL configured, nothing is sent, and the skip is recorded on the call's Events tab in the console.
Configuring it
The webhook is configured per agent, in the agent editor, under the Webhook Events accordion.
- Open the agent and expand Webhook Events.
- Set the Webhook URL to an endpoint on your server that accepts POST with a JSON body.
- Optionally turn on Enable Authentication and supply a header name and value. This header is attached to every delivery, verbatim.
- Click Test URL. This fires a test event at your endpoint so you can confirm it is reachable before a real call depends on it. The test carries the event value
webhook.testrather thancall_ended, so write your handler to accept event values other thancall_endedinstead of failing on them. - Set the Webhook Timeout, from one to thirty seconds.
- Publish the agent. The webhook is part of the agent config, so a change only takes effect for calls handled by a published version that carries it.
A view event schema modal in the same accordion shows the exact payload shape, which is the fastest way to check a field name while you are writing the handler.
Securing your endpoint
Your webhook URL is a public endpoint, so it needs to prove that a request really came from VoiceSquad. The mechanism is the custom header: add a secret header under Enable Authentication, and verify it on your server before you act on the body.
# What you configure in the console
Header name: Authorization
Header value: Bearer YOUR_SHARED_SECRET
# What your handler checks, before parsing anything
if request.headers.get("Authorization") != f"Bearer {YOUR_SHARED_SECRET}":
return 401- Serve the endpoint over HTTPS. The header travels with the request, so the transport has to protect it.
- Compare the value with a constant-time comparison, not a plain string equality check.
- Keep the secret in your server-side configuration, and rotate it by updating the agent config and your server together.
- Do not trust the body without checking the header first. Anyone who learns the URL can post to it.
Delivery and retries
- Up to three attempts per call.
- A short backoff between attempts: roughly two seconds, then four.
- A ten second timeout per attempt.
- After three failures the delivery is marked failed. There is no replay queue, so a call whose deliveries all fail will not be retried later.
Every attempt is visible in the console on the call itself, on the Call information tab of the call detail slider in Conversations, along with what was sent and how each attempt went. That is the first place to look when your system is missing a call it should have received.
2xx and do your real work asynchronously. Acknowledge the request, put the payload on a queue, and process it after. A handler that writes to a CRM inline will eventually exceed the ten second timeout and burn a retry, and after three of those the result is gone.Make your handler idempotent as well. Key on call_id, so a retry that lands after your first successful write does not create a duplicate record.
The payload
{
"event": "call_ended",
"call_id": "9c15b8de-0000-0000-0000-000000000000",
"agent_id": "efa64971-0000-0000-0000-000000000000",
"timestamp": "2026-08-14T15:42:11+05:30",
"call": {
"direction": "outbound",
"from": "+14155551234",
"to": "+14155559876",
"started_at": "2026-08-14T15:39:58+05:30",
"ended_at": "2026-08-14T15:42:08+05:30",
"duration_seconds": 130,
"end_reason": "customer_hangup",
"recording_url": "https://recordings.example.com/9c15b8de.mp3"
},
"conversation": {
"transcript": [
{
"role": "agent",
"content": "Hi Alex, this is Nova calling about invoice INV-20481.",
"timestamp": "2026-08-14T15:40:01+05:30"
},
{
"role": "user",
"content": "Yes, I meant to pay that this morning.",
"timestamp": "2026-08-14T15:40:06+05:30"
}
],
"summary": "Customer confirmed the invoice and agreed to pay by Friday.",
"sentiment": "positive",
"language": "en-US"
},
"extracted_variables": {
"payment_promised": true,
"promised_date": "2026-08-16"
}
}call_idis the call session id. It matches thesession_uuidreturned by the trigger API, which is how you tie a result back to the request that started it.callholds the telephony facts: direction, the two numbers, start and end times, duration in seconds, why it ended, and a link to the recording.conversationholds the turn-by-turntranscript, thesummary, thesentiment(positive, neutral, negative, or null when none was determined), and the detectedlanguage.extracted_variablesholds whatever you declared in the agent's Variable Extraction accordion. The keys are yours, so this is the block your business logic usually reads.
End reasons
| end_reason | What happened |
|---|---|
customer_hangup | The person on the other end ended the call. |
agent_ended | The agent closed the call itself, normally because its end call instructions were satisfied. |
transferred_to_human | The call was handed off to a person. |
max_duration_reached | The call hit the configured maximum call duration and was ended. |
no_connection | The call never connected. |
carrier_error | The telephony provider reported an error. |
network_error | The call dropped for network reasons. |
no_answer | Nobody picked up. |
voicemail | The call reached an answering machine. |
error | The call ended because of an error on the platform side. |
unknown | The reason could not be determined. |
end_reason gracefully rather than throwing. A default branch that logs the value and keeps the rest of the payload is enough.Putting it together
A working integration is usually three small pieces: place the call with the trigger API and store the returned session_uuid against your own record, verify and queue the webhook when it arrives, then match on call_id and write the outcome using end_reason and extracted_variables. Anything you need beyond that, including the recording, the evaluation and the goal verdict, is on the call in the console.