Webhooks
Receive album ingest completion and failure in real time, without polling the ingest status lookup.
The moment an ingest status changes to completed or failed, KiTbetter sends a POST request to your registered URL.
Registration
Partner self-registration is not available yet. Send us your receiving URL through the contact channel and we will register it for you.
- Only HTTPS URLs are allowed.
- Use the same channel to change or remove a URL.
- The ingest status lookup remains available after you register a webhook — we recommend keeping a backup polling loop in case a delivery is missed.
Events
There is a single eventType, ingest; the outcome is distinguished by data.status.
data.status |
When | What to check |
|---|---|---|
completed |
Everything, including resource file loading, has finished | — |
failed |
Resource loading failed | The reason code in data.error |
New event types may be added, so implement your handler to ignore unknown eventType values.
Payload
data is wrapped in a common envelope (eventId, eventType, occurredAt), and data has the same shape as the
ingest status lookup (GET /ddex/ingests/{ingestId}) response.
{
"eventId": "evt_01HZX0A2B3C4D5E6F7G8H9J0K1",
"eventType": "ingest",
"occurredAt": "2026-08-06T10:42:00+09:00",
"data": {
"ingestId": "1042",
"releaseId": "DIST-2026-00123",
"status": "completed",
"receivedAt": "2026-08-06T10:38:00+09:00",
"completedAt": "2026-08-06T10:42:00+09:00",
"resources": { "total": 14, "completed": 14, "failed": 0 }
}
}
Failure events carry an ingest failure reason code in data.error.
{
"eventId": "evt_01HZX9Z8Y7X6W5V4U3T2S1R0Q9",
"eventType": "ingest",
"occurredAt": "2026-08-06T10:44:00+09:00",
"data": {
"ingestId": "1042",
"releaseId": "DIST-2026-00123",
"status": "failed",
"receivedAt": "2026-08-06T10:38:00+09:00",
"completedAt": "2026-08-06T10:44:00+09:00",
"resources": { "total": 14, "completed": 12, "failed": 2 },
"error": { "code": "RESOURCE_FETCH_FAILED", "message": "The download URLs for resources A3 and A7 have expired." }
}
}
Verifying requests (recommended)
Verification is recommended, not required — webhooks work without it. But if your receiving URL is exposed, anyone can send forged requests, so we recommend one of the two approaches below.
Option 1 — Signature verification
Webhook requests include an X-KiT-Signature header.
X-KiT-Signature: t=1754886000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
t— send time (Unix time, in seconds)v1— the string{t}.{raw body}signed with HMAC-SHA256 using your API key as the secret
This is the same scheme as GitHub (X-Hub-Signature-256) and Stripe (Stripe-Signature), so you can implement it with
standard libraries only, and you verify with the API key you already hold instead of a separate webhook secret.
import hashlib, hmac, time
def verify_webhook(headers: dict, raw_body: bytes, api_key: str) -> bool:
try:
parts = dict(p.split("=", 1) for p in headers.get("X-KiT-Signature", "").split(","))
t, v1 = parts["t"], parts["v1"]
except (ValueError, KeyError):
return False
if abs(time.time() - int(t)) > 300: # older than 5 minutes — prevents replay of intercepted requests
return False
expected = hmac.new(api_key.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1) # constant-time comparison
Do not trust the body of a request that fails verification; respond with 401.
Option 2 — Treat it as a signal only
If you would rather not implement signature verification, do not trust the webhook body — use it only as a signal that it is time to check. When a webhook arrives, confirm the state yourself with the ingest status lookup; that lookup is authenticated with your API key, so it cannot be forged.
Warning
If you use neither approach and trust the webhook body as-is, a forged request could trick you into recording the wrong state.
Response rules and retries
- Respond with 2xx within 10 seconds of receiving a webhook. Without a response, or with a non-2xx response, KiTbetter treats the delivery as failed and retries — so a slow response means you receive the same event again even though it arrived fine. The response body is ignored.
- We recommend queuing heavy processing and responding immediately.
- On a non-2xx response or a timeout, KiTbetter retries up to 5 times with increasing intervals (the count and intervals may change).
- Retries mean the same event can arrive more than once — process events idempotently, keyed on
eventId. - Event order is not guaranteed. Base your decisions on
occurredAtanddata.statusin the payload. - If every retry fails, the event is lost — use backup polling to recover the state.