call.completed
Fires when a call ends normally with a final duration and billing record.
Status: This event will start emitting once gateway-go upstream signals land. The contract documented here is stable.
call.completed fires when a call ends with a successful hangup. It is the primary signal for billing, CDR writes, and the call_stats_daily rollup. It is routed per-number via voice_callback_url / events_url and is not subscribable as a workspace webhook.
Note: there is a small dispatch-loop lag (up to ~5 seconds in steady state) between this event being written and the stats rollup reflecting it.
{
"kind": "call.completed",
"event_id": "01900000-0000-7000-8000-000000000001",
"workspace_id": "01900000-0000-7000-8000-000000000002",
"occurred_at": "2026-06-27T10:05:00.000Z",
"data": {
"call_id": "01900000-0000-7000-8000-000000000003",
"direction": "inbound",
"from": "+254700000001",
"to": "+254700000002",
"number_id": "01900000-0000-7000-8000-000000000004",
"status": "completed",
"duration_seconds": 295,
"answered_at": "2026-06-27T10:00:05.000Z",
"ended_at": "2026-06-27T10:05:00.000Z",
"hangup_cause": "normal_clearing"
}
}Every webhook delivery includes the following request headers:
| Header | Description |
|---|---|
X-Sautikit-Signature | HMAC-SHA256 of the raw body, hex-encoded. Verify with your subscription secret. |
X-Sautikit-Idempotency-Key | Unique delivery ID for deduplication. |
X-Sautikit-Event | Literal event kind: call.completed. |
event_id or the X-Sautikit-Idempotency-Key header.dead_letter.import { createHmac } from "node:crypto";
export async function POST(req) {
const sig = req.headers["x-sautikit-signature"];
const body = await req.text();
const expected = createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(body)
.digest("hex");
if (sig !== expected) return new Response("Forbidden", { status: 403 });
const event = JSON.parse(body);
if (event.kind === "call.completed") {
const { call_id, duration_seconds, hangup_cause } = event.data;
console.log(`Call ${call_id} completed: ${duration_seconds}s, cause: ${hangup_cause}`);
// write CDR, update dashboard, trigger post-call flow...
}
return new Response("OK", { status: 200 });
}voice_callback_url / events_url on the Numbers Routing tab.