Webhooks
Un webhook entrega un POST JSON, firmado, en la dirección que registraste,
cuando ocurre un evento. Regístralo desde la consola
(Configuración → Webhooks), por la API
(POST /v1/webhooks) o mediante el kadu.yaml.
Cuerpo
{
"event": "execution.completed",
"event_id": "5b0e…",
"delivered_at": "2026-10-31T12:04:51.210Z",
"organization_id": "1d7a…",
"workspace_id": "9c3f…",
"data": { "…": "el recurso del evento, en snake_case" }
}
| Campo | Qué es |
|---|---|
event | el nombre del evento (lista en Webhooks) |
event_id | id del evento: el mismo en todos los intentos de entrega de él |
delivered_at | cuándo salió esta entrega |
organization_id | la organización |
workspace_id | el workspace del evento, o null en un evento de la organización |
data | el recurso del evento. custom_metadata y metadata van como se guardaron. |
Un webhook con workspace recibe solo los eventos de ese workspace. Un webhook sin workspace recibe todos.
Headers
| Header | Contenido |
|---|---|
X-Kobana-App | kadu |
X-Kobana-Environment | production o staging. Verifícalo antes de procesar: una entrega de staging nunca debe tocar datos de producción. |
X-Kobana-Webhook-Id | el webhook que generó la entrega |
X-Kobana-Event | el nombre del evento |
X-Kobana-Event-Id | el id del evento |
X-Kobana-Delivery-Id | el id de esta entrega |
X-Kobana-Timestamp | segundos Unix en que se firmó la entrega |
X-Kobana-Signature | la firma |
webhook-id, webhook-timestamp, webhook-signature | los mismos tres valores, en el formato de Standard Webhooks |
Authorization o el header configurado | la credencial que configuraste en el webhook (Bearer, Basic o header) |
Verificar la firma
La firma sigue Standard Webhooks: sirve cualquier biblioteca de verificación compatible.
- Signing secret:
whsec_seguido de 32 bytes en base64. La clave del HMAC son los bytes decodificados después del prefijo. - Contenido firmado:
<delivery_id>.<timestamp>.<cuerpo crudo>, con elX-Kobana-Delivery-Id(=webhook-id) y elX-Kobana-Timestamp. - Header:
v1,<base64(HMAC-SHA256)>. Durante una rotación, el header trae una firma por cada secret aún válido, separadas por espacio. Acepta la entrega si cualquiera coincide. - Ventana: rechaza una entrega con un timestamp a más de 5 minutos de tu reloj, para limitar el replay.
import base64, hashlib, hmac, time
def verificar(secret: str, delivery_id: str, timestamp: str, corpo: bytes, assinatura: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
chave = base64.b64decode(secret.removeprefix("whsec_"))
conteudo = f"{delivery_id}.{timestamp}.".encode() + corpo
esperado = base64.b64encode(hmac.new(chave, conteudo, hashlib.sha256).digest()).decode()
return any(
hmac.compare_digest(token.split(",", 1)[1], esperado)
for token in assinatura.split()
if token.startswith("v1,")
)
Usa el cuerpo crudo, antes de cualquier parse de JSON: reserializar cambia los bytes y rompe la firma.
:::note Header heredado
X-Kobana-Signature-Sha256 es el esquema antiguo de Kadu (sha256=<hex> de
timestamp.cuerpo) y está obsoleto. Usa X-Kobana-Signature.
:::
Signing secret y rotación
El signing secret aparece una vez, en la creación del webhook. Para
cambiarlo, usa POST /v1/webhooks/{id}/rotate-secret: la respuesta trae el
nuevo secret, y el anterior sigue firmando por 24 horas
(previous_signing_secret_expires_at), para que cambies la clave en el
receptor sin perder entregas.
Entregas y fallas
- Responde
2xxen hasta 10 segundos y procesa después. Cualquier otra respuesta, o el timeout, cuenta como falla. - El mismo evento puede llegar más de una vez: usa el
event_idpara descartar duplicados. - Después de 5 fallas seguidas, el webhook se desactiva y se avisa a los correos registrados en él.
GET /v1/webhook_deliverieslista las 50 entregas más recientes, con estado y respuesta, yPOST /v1/webhook_deliveries/{id}/replayreenvía una entrega.- Las entregas se guardan por 90 días.
La URL de destino debe ser pública y https. Las direcciones privadas, de
loopback o en http se rechazan.