# 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
```

```json
{
  "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`:

```json
{
  "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:

```json
{
  "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](/docs/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:

```python
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 "")
```

```ts
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.
