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
- 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 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
2xxas soon as you've stored the event; do slow work afterwards.
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.