
# Webhooks

When something happens in a shop — an order is placed, paid, dispatched, delivered — Kibanda sends a `POST` to your endpoint with the [event](https://kibanda.africa/docs/events). Create endpoints in the dashboard (**Developers → Webhooks**) or with the [API](https://kibanda.africa/docs/webhook-endpoints).

```json
{
  "id": "evt_8f3a…",
  "object": "event",
  "type": "order.paid",
  "created": 1792716000,
  "data": { "object": { "id": "ord_…", "object": "order", "status": "confirmed", "total": 31000, … } }
}
```

`data.object` is the full object (an [order](https://kibanda.africa/docs/orders), [customer](https://kibanda.africa/docs/customers) or cart) as it was when the event was first sent.

## Delivery

- **Never lost.** An event is written in the same database transaction as the change it describes, then delivered. If we crash in between, it's sent when we're back.
- **Retries.** Anything but a 2xx within 10 seconds is retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about two days), then marked failed. You can [retry by hand](https://kibanda.africa/docs/webhook-endpoints#retry-a-delivery) any time.
- **At least once.** You may get the same event twice. Use `webhook-id` (the event id) to ignore duplicates.
- **Order.** Events are usually in order but aren't guaranteed to be. Compare `created`, or fetch the object (`GET /v1/orders/{id}`) when order matters.
- **Respond fast.** Return `2xx` as soon as you've stored the event; do slow work afterwards.

## Verifying signatures

Deliveries follow the [Standard Webhooks](https://www.standardwebhooks.com) spec, so any of its libraries verifies them. Three headers:

| Header | Value |
|---|---|
| `webhook-id` | The event id. Same on every retry. |
| `webhook-timestamp` | Unix seconds when this attempt was sent. |
| `webhook-signature` | `v1,<base64 HMAC-SHA256>` |

The signed content is `{webhook-id}.{webhook-timestamp}.{raw body}`, keyed with your endpoint secret **base64-decoded after removing the `whsec_` prefix**. Reject timestamps more than 5 minutes old.

**Node.js** (with the `standardwebhooks` package):

```js
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.KIBANDA_WEBHOOK_SECRET); // "whsec_…"

app.post("/kibanda", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = wh.verify(req.body, req.headers); // throws if invalid
  } catch {
    return res.status(400).end();
  }
  if (event.type === "order.paid") fulfil(event.data.object);
  res.status(204).end();
});
```

**Python** (no library):

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

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

Always verify against the **raw** request body, before parsing JSON.

## Testing

`POST /v1/webhook_endpoints/{id}/test` sends a signed `ping` event and returns what your server answered. Each endpoint's recent deliveries, with response codes and errors, are at `GET /v1/webhook_endpoints/{id}/deliveries` and in the dashboard.

## Rotating the secret

`POST /v1/webhook_endpoints/{id}/rotate_secret` returns a new secret, effective immediately. Deploy it and rotate in quick succession.
