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" }
}
| Field | What it is |
|---|---|
event | the event name (list in Webhooks) |
event_id | event id: the same on all delivery attempts of it |
delivered_at | when this delivery went out |
organization_id | the organization |
workspace_id | the event's workspace, or null on an organization event |
data | the 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​
| Header | Content |
|---|---|
X-Kobana-App | kadu |
X-Kobana-Environment | production or staging. Check before processing: a staging delivery must never touch production data. |
X-Kobana-Webhook-Id | the webhook that generated the delivery |
X-Kobana-Event | the event name |
X-Kobana-Event-Id | the event id |
X-Kobana-Delivery-Id | this delivery's id |
X-Kobana-Timestamp | Unix seconds at which the delivery was signed |
X-Kobana-Signature | the signature |
webhook-id, webhook-timestamp, webhook-signature | the same three values, in the Standard Webhooks layout |
Authorization or the configured header | the 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>, withX-Kobana-Delivery-Id(=webhook-id) andX-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
2xxwithin 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_idto discard duplicates. - After 5 consecutive failures, the webhook is deactivated and the emails registered on it are notified.
GET /v1/webhook_deliverieslists the 50 most recent deliveries, with status and response, andPOST /v1/webhook_deliveries/{id}/replayresends a delivery.- Deliveries are kept for 90 days.
The destination URL must be public and https. Private, loopback or http
addresses are rejected.