Webhooks
Receive signed events in your own systems and verify they came from Kabaido.
Webhooks push events to a URL you control. Add a webhook in Settings, then Integrations, choose the events you want and set a signing secret. Kabaido posts a JSON body and signs it so you can verify the source.
Events
The current event vocabulary is locked. Renaming an event would break subscribers, so the names below are stable.
- quote.created
- quote.sent
- quote.accepted
- order.created
- order.cancelled
- service_item.updated
Payload
Every delivery is a POST with a JSON body carrying the event name, your organisation id and the event data.
{
"event": "quote.accepted",
"org_id": "00000000-0000-0000-0000-000000000000",
"data": {
"id": "...",
"number": "Q-1024",
"status": "accepted"
}
}Headers and signature
Two headers accompany each delivery. X-Kabaido-Event carries the event name. X-Kabaido-Signature carries a timestamp and an HMAC SHA-256 signature of the string formed by the timestamp, a full stop and the exact raw request body, keyed by your signing secret.
X-Kabaido-Event: quote.accepted
X-Kabaido-Signature: t=1718000000,v1=<hex hmac sha256 of "t.body">Verify in TypeScript
import { createHmac, timingSafeEqual } from "node:crypto";
// Verify an incoming Kabaido webhook. Returns true when the signature is valid.
export function verify(secret: string, header: string, rawBody: string): boolean {
// Header looks like: t=1718000000,v1=<hex>
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=") as [string, string]),
);
const timestamp = parts.t;
const signature = parts.v1;
if (!timestamp || !signature) return false;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const a = Buffer.from(signature, "hex");
const b = Buffer.from(expected, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}Verify in Python
import hashlib
import hmac
# Verify an incoming Kabaido webhook. Returns True when the signature is valid.
def verify(secret: str, header: str, raw_body: str) -> bool:
# Header looks like: t=1718000000,v1=<hex>
parts = dict(kv.split("=", 1) for kv in header.split(","))
timestamp = parts.get("t")
signature = parts.get("v1")
if not timestamp or not signature:
return False
expected = hmac.new(
secret.encode(),
f"{timestamp}.{raw_body}".encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(signature, expected)Compute the signature over the raw request body bytes, before any JSON parsing. Reserialising the body can change it and break the check.
Retries
A delivery is successful on any 2xx response. Failures retry up to five times, at 1, 5, 30, 120 and 360 minutes after the first attempt, after which the integration records the last error. Recent deliveries with their status and a manual retry button are visible on the endpoint card in Settings. Respond quickly and do your own work after acknowledging.
Body formats
An endpoint can deliver in one of three body formats, chosen when you create or edit it. The signed JSON envelope above is the default. The Slack format posts each event as a readable message for a Slack incoming webhook URL. The Teams format posts an Adaptive Card for a Microsoft Teams Workflows request URL. Slack and Teams ignore the signature header; the webhook URL itself is the credential, so treat it like a secret.