API reference
Webhooks
Register an HTTPS endpoint and Wakili will POST to it when a call ends, so you don't have to poll GET /calls/{id} waiting for status: "ended". Every delivery is signed so you can verify it came from Wakili.
Register a webhook
POST /webhooks
{
"url": "https://your-app.com/hooks/wakili",
"events": ["call.ended"],
"secret": "optional, at least 16 chars"
}
url must be http(s) (422 otherwise). If you omit secret, one is generated (whsec_...). Response 201:
{
"id": "a1b2...",
"url": "https://your-app.com/hooks/wakili",
"events": ["call.ended"],
"active": true,
"secret": "whsec_...",
"created_at": "2026-08-27T12:00:00Z"
}
The secret is returned here — store it; you need it to verify signatures.
List / delete
GET /webhooks # your webhooks, newest first
DELETE /webhooks/{id} # 204
The call.ended event
Fired once per call, after billing and the recording upload have finished. Body:
{
"event": "call.ended",
"call_id": "5b1a...",
"conversation_id": "9e3c...",
"agent_id": "...",
"status": "ended",
"ended_at": "2026-08-27T12:03:11",
"transcript_url": "https://api.wakili.dev/calls/5b1a...",
"recording_available": true
}
Fetch transcript_url (with your API key) for the full transcript, metrics, and cost — see Calls API.
Headers
| header | value |
|---|---|
X-Wakili-Event | call.ended |
X-Wakili-Signature | sha256=<hex> |
<hex> is HMAC-SHA256(key = webhook secret, message = raw request body bytes).
Verifying
Recompute the HMAC over the exact bytes you received and compare in constant time:
import hashlib, hmac
def valid(secret: str, body: bytes, header: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header or "")
import { createHmac, timingSafeEqual } from "node:crypto";
function valid(secret: string, body: Buffer, header: string): boolean {
const expected = "sha256=" + createHmac("sha256", secret).update(body).digest("hex");
return header.length === expected.length &&
timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}
Delivery semantics
- One attempt per webhook, with a short timeout. There is no automatic retry.
- Every attempt (success or failure) is logged server-side.
- If your endpoint is down when a call ends, recover by fetching the call over the REST API — no event is queued for redelivery.
- Return any
2xxquickly; do slow work asynchronously.