API reference

View raw .md

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

headervalue
X-Wakili-Eventcall.ended
X-Wakili-Signaturesha256=<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 2xx quickly; do slow work asynchronously.