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
| Event | Fires when | Payload 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
- Retried up to 3 times with a growing pause, then dropped.
- Answer
2xxquickly — do the slow work after you reply. - Delivery is best-effort. Keep
GET /v1/jobs/{id}as your fallback: a webhook you never received must not leave an order stuck.
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