View as Markdown

Webhooks

When something happens in a shop — an order is placed, paid, dispatched, delivered — Kibanda sends a POST to your endpoint with the event. Create endpoints in the dashboard (Developers → Webhooks) or with the API.

{
  "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, customer or cart) as it was when the event was first sent.

Delivery

Verifying signatures

Deliveries follow the Standard Webhooks 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):

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):

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.