Skip to main content

Webhooks

A webhook delivers a signed JSON POST to the address you registered when an event happens. Register it through the console (Settings → Webhooks), through the API (POST /v1/webhooks) or through kadu.yaml.

Body​

{
"event": "execution.completed",
"event_id": "5b0e…",
"delivered_at": "2026-10-31T12:04:51.210Z",
"organization_id": "1d7a…",
"workspace_id": "9c3f…",
"data": { "…": "the event's resource, in snake_case" }
}
FieldWhat it is
eventthe event name (list in Webhooks)
event_idevent id: the same on all delivery attempts of it
delivered_atwhen this delivery went out
organization_idthe organization
workspace_idthe event's workspace, or null on an organization event
datathe event's resource. custom_metadata and metadata go as they were stored.

A webhook with a workspace receives only that workspace's events. A webhook without a workspace receives all of them.

Headers​

HeaderContent
X-Kobana-Appkadu
X-Kobana-Environmentproduction or staging. Check before processing: a staging delivery must never touch production data.
X-Kobana-Webhook-Idthe webhook that generated the delivery
X-Kobana-Eventthe event name
X-Kobana-Event-Idthe event id
X-Kobana-Delivery-Idthis delivery's id
X-Kobana-TimestampUnix seconds at which the delivery was signed
X-Kobana-Signaturethe signature
webhook-id, webhook-timestamp, webhook-signaturethe same three values, in the Standard Webhooks layout
Authorization or the configured headerthe credential you configured on the webhook (Bearer, Basic or header)

Verify the signature​

The signature follows Standard Webhooks: any compatible verification library works.

  • Signing secret: whsec_ followed by 32 bytes in base64. The HMAC key is the decoded bytes after the prefix.
  • Signed content: <delivery_id>.<timestamp>.<raw body>, with X-Kobana-Delivery-Id (= webhook-id) and X-Kobana-Timestamp.
  • Header: v1,<base64(HMAC-SHA256)>. During a rotation, the header carries one signature per still-valid secret, separated by spaces. Accept the delivery if any one matches.
  • Window: reject a delivery whose timestamp is more than 5 minutes from your clock, to limit replay.
import base64, hashlib, hmac, time

def verify(secret: str, delivery_id: str, timestamp: str, body: bytes, signature: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
content = f"{delivery_id}.{timestamp}.".encode() + body
expected = base64.b64encode(hmac.new(key, content, hashlib.sha256).digest()).decode()
return any(
hmac.compare_digest(token.split(",", 1)[1], expected)
for token in signature.split()
if token.startswith("v1,")
)

Use the raw body, before any JSON parsing: reserializing changes the bytes and breaks the signature.

:::note Legacy header X-Kobana-Signature-Sha256 is Kadu's old scheme (sha256=<hex> of timestamp.body) and is deprecated. Use X-Kobana-Signature. :::

Signing secret and rotation​

The signing secret appears once, when the webhook is created. To change it, use POST /v1/webhooks/{id}/rotate-secret: the response carries the new secret, and the previous one keeps signing for 24 hours (previous_signing_secret_expires_at), so you can switch the key in the receiver without losing deliveries.

Deliveries and failures​

  • Respond 2xx within 10 seconds and process later. Any other response, or the timeout, counts as a failure.
  • The same event can arrive more than once: use event_id to discard duplicates.
  • After 5 consecutive failures, the webhook is deactivated and the emails registered on it are notified.
  • GET /v1/webhook_deliveries lists the 50 most recent deliveries, with status and response, and POST /v1/webhook_deliveries/{id}/replay resends a delivery.
  • Deliveries are kept for 90 days.

The destination URL must be public and https. Private, loopback or http addresses are rejected.