# Webhooks

> Get Kabu's events at your own HTTPS endpoint, signed per Standard Webhooks. Register an endpoint, verify the signature, retries and replay.

A webhook endpoint is a URL of yours that Kabu calls with every [event](/docs/concepts/events) you subscribe to: a
message answered, an ask expired, a session ended. The events are the same objects the event stream delivers, signed
per [Standard Webhooks](https://www.standardwebhooks.com), so any of its libraries can verify them.

## Register an endpoint

Endpoints are managed with a key holding `webhooks:write` (a service or admin key), or in the console:

```sh
curl https://usekabu.com/api/v2/webhook_endpoints \
  -H "Authorization: Bearer $KABU_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"url": "https://example.com/kabu/webhook", "events": ["message.resolved", "message.expired"]}'
```

`events` lists the types to send, or `["*"]` for all. The response includes the endpoint's signing `secret`
(`whsec_…`) this once: store it. The URL must be public HTTPS; its address is checked again on every delivery, and
redirects are never followed. `POST /v2/webhook_endpoints/{endpoint_id}/test` sends a `ping` event to try it.

## What arrives

```http
POST /kabu/webhook HTTP/1.1
Content-Type: application/json
webhook-id: evt_01J9Z…
webhook-timestamp: 1793353260
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
Kabu-Version: 2026-10-17

{"object": "event", "id": "evt_01J9Z…", "type": "message.resolved", "resource": "msg_…", "resource_version": 3, "data": {"object": {…}}}
```

The body is an [Event](/docs/api/events#schema-Event): its `type`, the `resource` it is about and that resource's
`resource_version`, and `data.object`, the resource as it is now. Event types:

| Type | When |
|---|---|
| `message.created` | A message was sent |
| `message.delivered` | It reached a device |
| `message.resolved` | An ask was answered |
| `message.expired` | An ask expired unanswered |
| `message.cancelled` | An ask was cancelled |
| `session.created`, `session.updated`, `session.ended` | A session started, changed or ended |
| `agent.paused` | An agent sent too many near-identical messages and was paused |
| `ping` | A test, from `…/test` |

New types may appear; ignore the ones you do not know.

## Verify the signature

The signature is HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{body}`, keyed with your secret's bytes (the
base64 after `whsec_`), sent base64-encoded after `v1,`. While a rotated secret is still valid, the header carries two
signatures separated by a space; accept the delivery if either matches. Check the timestamp is recent too, so an old
delivery cannot be replayed at you:

```python
import base64, hashlib, hmac, time

def verify(secret: str, headers: dict, body: bytes) -> bool:
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{headers['webhook-id']}.{headers['webhook-timestamp']}.".encode() + body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    fresh = abs(time.time() - int(headers["webhook-timestamp"])) < 300
    return fresh and any(
        hmac.compare_digest(expected, sig.removeprefix("v1,")) for sig in headers["webhook-signature"].split()
    )
```

Verify the raw body, before parsing it: re-encoded JSON does not match.

## Answer fast, then work

Answer any `2xx` within 10 seconds and do the work afterwards. Anything else is retried: first after 5 seconds, then
twice as long each time, up to an hour apart, for 3 days. Delivery is at least once and may repeat an event, so apply an
event only when its `resource_version` is newer than the one you hold.

## Rotate, inspect, replay

- `POST /v2/webhook_endpoints/{endpoint_id}/rotate_secret` issues a new secret; the old one keeps signing alongside it
  for a while, so you can switch without missing a delivery.
- `GET /v2/webhook_endpoints/{endpoint_id}/attempts` lists each delivery attempt and its outcome.
- `POST /v2/webhook_endpoints/{endpoint_id}/replay` sends the events after a checkpoint again, up to 7 days and
  100,000 events back.

The [Webhooks reference](/docs/api/webhooks) has every operation, the endpoint's fields and the delivery itself.
