Guide

Webhooks

Let us call you when an async job finishes, instead of polling for it.

POST /v1/webhook

Register an endpoint

curl -sS -X POST "{{BASE}}/v1/webhook" \
  -H "Authorization: Bearer pk_0123456789abcdef0123456789abcdef" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://shop.example.com/hooks/ka-bot"}'
{
  "ok": true,
  "url": "https://shop.example.com/hooks/ka-bot",
  "secret": "whsec_…",
  "note": "Shown once. Verify KA-Signature as HMAC-SHA256(secret, f'{t}.{raw_body}')."
}
The secret is shown once. Store it before you close the response — registering again issues a new one. The URL must be https://, otherwise 400 https_required.

Events

EventFires whenPayload data
redeem.completed A redeem finished — including one where every code failed The full redeem body (same shape as a sync response)
redeem.failed An async job crashed on our side; the requests were refunded An error body plus job_id
{
  "event": "redeem.completed",
  "created_at": 1789000042,
  "data": { "ok": true, "job_id": "job_46c8…", "codes": [ … ] }
}

Delivery

Verify every delivery

Each request carries:

KA-Event: redeem.completed
KA-Signature: t=1786341684,v1=9f2c4e…
Anyone can POST to your URL. Verify the signature before you trust the body — and reject timestamps older than about 5 minutes so a captured delivery cannot be replayed at you later.

v1 is HMAC-SHA256(secret, "{t}." + raw_body). Use the raw request bytes — not a re-serialised object, whose key order or spacing will differ and break the check.

from ka_bot import KaBot

KaBot.verify_webhook(secret, raw_body, request.headers["KA-Signature"])
await KaBot.verifyWebhook(secret, rawBody, req.headers["ka-signature"]);

By hand, if you are not using an SDK:

import hashlib, hmac, time

def verify(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    ts, sig = parts.get("t", ""), parts.get("v1", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > tolerance:
        return False                      # too old / clock skew — reject
    expected = hmac.new(secret.encode(),
                        f"{ts}.".encode() + raw_body,
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)   # constant-time