Skip to content

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." }
  }
}

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 occurredAt and data.status in the payload.
  • If every retry fails, the event is lost — use backup polling to recover the state.