Post-call webhooks: events, triggers, payload, signatures and retries

Receive every call's recording, transcript and outcome in your own system: the call.completed and call.classified events, the trigger filter, every payload field including hangupReason, how to verify X-ElevateBox-Signature, and the retry schedule.

Updated 5 October 2026

On the caller's Outcomes tab, Post-call webhooks send each call's recording, transcript and outcome to your system as soon as the call ends. Up to five endpoints per caller. Webhooks are not versioned: a saved change applies to the next call.

Add an endpoint

  1. Click Add webhook and enter a public https:// URL (port 443, no credentials, no IP addresses or local hosts).
  2. Choose When this webhook fires (see triggers below) and Save webhook.
  3. Copy the signing secret (whsec_…) when it is shown. It is shown once.
  4. Use the test button: we send a signed sample call.completed with "test": true and report “Test delivery sent and accepted by your server.”, or the reason it failed (unreachable, no answer within 10 seconds, or the HTTP status).

Events

  • call.completed: sent when the call ends. The transcript and timings are there; outcome, summary, intent, sentiment and capturedValues are still null and classification.status is pending.
  • call.classified: sent once the outcome, summary and captured values are known, usually within a minute or two of the call ending. Treat this as the one to act on.

Triggers

  • Every call (default): fires for every attempt, answered or not.
  • Answered calls only: only calls someone actually answered. Skips voicemail, busy, no answer and failures.
  • Voicemail only: only calls that reached voicemail, so you can follow up another way. Since the outcome is unknown at call.completed, such an endpoint receives only call.classified.

Payload

call.classified
{
  "event": "call.classified",
  "deliveryId": "dlv_…",
  "sentAt": "2026-10-05T09:31:12.000Z",
  "call": {
    "id": "…", "agentId": "…", "direction": "outbound", "status": "completed",
    "startedAt": "…", "answeredAt": "…", "endedAt": "…", "durationSeconds": 84,
    "contact": { "name": "Asha Rao", "phone": "+919876543210" },
    "hangupReason": "completed",
    "outcome": "qualified", "summary": "Asked for a site visit on Saturday…",
    "intent": "booking", "intentConfidence": 0.9, "intentReason": "…",
    "sentiment": "positive", "sentimentConfidence": 0.8, "sentimentReason": "…",
    "capturedValues": { "budget": "50L", "visit_day": "Saturday" },
    "transcript": [ { "speaker": "agent", "text": "…" }, { "speaker": "user", "text": "…" } ],
    "recordingUrl": "https://…",
    "recording": { "status": "ready", "url": "https://…", "expiresAt": "…", "mimeType": "audio/mpeg", "readyAt": "…", "deletedAt": null },
    "classification": { "at": "…", "status": "done" }
  }
}
  • hangupReason is completed, failed or cancelled for a connected call, and voicemail, no_answer, busy, call_failed or no_response for one that never connected.
  • outcome is one of qualified, interested, callback, not_interested, no_response, no_answer, voicemail, busy, call_failed. sentiment is positive, neutral or negative.
  • transcript holds up to 400 turns; speaker is agent or user. recording.url is a signed link and is null until the recording is ready.
  • capturedValues is keyed by your field keys from the Outcomes tab. There is no cost field; credits are in the Billing statement.

Verify the signature

Every delivery carries X-ElevateBox-Delivery, X-ElevateBox-Event, X-ElevateBox-Timestamp (Unix seconds) and X-ElevateBox-Signature (v1=<hex>). The signature is HMAC-SHA256 of "<timestamp>.<raw body>" with your whsec_ secret. The same four headers are also sent as X-Vikku-* for receivers built on the old name; those stop on 4 January 2027, so switch to the X-ElevateBox-* names.

Node
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, headers, rawBody) {
  const expected = createHmac("sha256", secret)
    .update(`${headers["x-elevatebox-timestamp"]}.${rawBody}`)
    .digest("hex");
  const given = String(headers["x-elevatebox-signature"]).replace(/^v1=/, "");
  return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}

Retries

Any 2xx answer counts as delivered; answer quickly and do the work afterwards, because the request times out after 10 seconds and redirects are not followed. A failure is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 8 hours, six attempts in all, then marked dead. Each endpoint shows “Active/Paused · N delivered · N pending · N failed”; Pause stops deliveries without deleting the endpoint. Use deliveryId to ignore a repeat.

More in Calls and results

Ready to put it to work?

Create an AI caller, try it in your browser, and let it ring your first lead today.

50 free creditsNo card neededLive in minutes