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.

  1. Open the agent and expand Webhook Events.
  2. Set the Webhook URL to an endpoint on your server that accepts POST with a JSON body.
  3. Optionally turn on Enable Authentication and supply a header name and value. This header is attached to every delivery, verbatim.
  4. 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.test rather than call_ended, so write your handler to accept event values other than call_ended instead of failing on them.
  5. Set the Webhook Timeout, from one to thirty seconds.
  6. 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.

Heads upRespond fast with a 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_id is the call session id. It matches the session_uuid returned by the trigger API, which is how you tie a result back to the request that started it.
  • call holds the telephony facts: direction, the two numbers, start and end times, duration in seconds, why it ended, and a link to the recording.
  • conversation holds the turn-by-turn transcript, the summary, the sentiment (positive, neutral, negative, or null when none was determined), and the detected language.
  • extracted_variables holds whatever you declared in the agent's Variable Extraction accordion. The keys are yours, so this is the block your business logic usually reads.
NoteTimestamps are in the agent's configured timezone, not UTC. Convert on your side rather than assuming, especially if agents in one workspace serve different regions.

End reasons

end_reasonWhat happened
customer_hangupThe person on the other end ended the call.
agent_endedThe agent closed the call itself, normally because its end call instructions were satisfied.
transferred_to_humanThe call was handed off to a person.
max_duration_reachedThe call hit the configured maximum call duration and was ended.
no_connectionThe call never connected.
carrier_errorThe telephony provider reported an error.
network_errorThe call dropped for network reasons.
no_answerNobody picked up.
voicemailThe call reached an answering machine.
errorThe call ended because of an error on the platform side.
unknownThe reason could not be determined.
Heads upTreat this list as open. New values may be added, so handle an unrecognised 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.