Webhooks
Receive signed events at your own URL and prove each one came from Kabaido.
Updated 23 September 2026
On this page
- Why
- Your own system should hear about a quote or an order the moment it happens, not when somebody polls.
- What
- Kabaido posts a signed JSON event to each endpoint that subscribes to it.
- How
- Add an endpoint on Custom outbound webhook, choose its events and verify the signature on your side.
Add an endpoint
- Step 1.
In
/app/settings/integrations, open Custom outbound webhook (or Slack, Microsoft Teams, Zapier, Make or n8n) and choose Add endpoint. It needs the admin role. - Step 2.
Enter the Endpoint URL, pick the Events (none picked means all events) and the Body format.
- Step 3.
Copy the signing secret. It is shown once.
Events
Event names are never renamed; new ones are only added.
| Event | Fires when |
|---|---|
quote.created | A quote is created |
quote.sent | A quote is sent to the customer |
quote.accepted | A quote is accepted |
quote.declined | A quote is declined |
order.created | An order is created |
order.updated | An order changes status (other than to cancelled), is edited or is emailed |
order.cancelled | An order is cancelled |
service_item.updated | A service item changes |
design.saved | A design is saved |
design.exported | A design is exported |
quote.superseded | A quote is replaced by a new version |
quote.expired | A quote passes its valid until date |
quote.viewed | The customer opens the quote |
order.invoiceable | An order is fulfilled, so an invoice can be raised |
purchase_order.sent | A purchase order is sent to the supplier |
purchase_order.received | Goods are received against a purchase order |
order.dispatched | An order's goods leave |
order.delivered | An order is delivered |
delivery.dispatched | A consignment is dispatched |
delivery.completed | A consignment is delivered |
delivery.failed | A delivery attempt fails |
monitor.fired | A monitor's condition is met |
Payload and headers
{
"event": "quote.accepted",
"data": {
"quote_id": "7f0c0000-0000-0000-0000-000000000000",
"number": "Q-1042",
"customer_id": "3a1e0000-0000-0000-0000-000000000000",
"total_minor": 58200,
"currency": "GBP"
},
"org_id": "00000000-0000-0000-0000-000000000000"
}What data holds depends on the event and on where it happened: read the fields you need and ignore the rest.
X-Kabaido-Event carries the event name. X-Kabaido-Signature is t=<unix seconds>,v1=<hex>, where the hex is an HMAC SHA-256 of the timestamp, a full stop and the raw body, keyed by the signing secret.
Verify the signature
import { createHmac, timingSafeEqual } from "node:crypto";
// header: the X-Kabaido-Signature value, "t=<unix seconds>,v1=<hex>"
// rawBody: the request body exactly as received, before any JSON parsing
export function verify(secret: string, header: string, rawBody: string): boolean {
const match = /^t=(\d+),v1=([0-9a-f]+)$/.exec(header.trim());
if (!match) return false;
const [, t, v1] = match;
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // five minutes
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const received = Buffer.from(v1, "hex");
return received.length === expected.length && timingSafeEqual(received, expected);
}import hashlib, hmac, re, time
# header: the X-Kabaido-Signature value; raw_body: the body bytes as received
def verify(secret: str, header: str, raw_body: bytes) -> bool:
match = re.fullmatch(r"t=(\d+),v1=([0-9a-f]+)", header.strip())
if not match:
return False
t, v1 = match.groups()
if abs(time.time() - int(t)) > 300: # five minutes
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(v1, expected)Sign over the raw body bytes, before any JSON parsing: a reserialised body will not match.
Delivery and retries
A delivery succeeds on any 2xx answer within 10 seconds. A failed one is tried again by the platform's nightly run, up to five attempts in all. Recent deliveries on the endpoint shows each status, with Retry to send one now. Answer fast and do the work after.
Body formats
The signed JSON envelope is the default. Slack message (incoming webhook) and Microsoft Teams card (Workflows) post readable messages instead; those services ignore the signature, so the URL itself is the secret.
Related
- Inbound endpointsGive any system a URL it can post JSON to, and choose what each post does.
- The REST APICall every v1 resource with the right scope, page through lists and handle every error it returns.
- SlackThe channel your team already watches sees a quote sent, an order raised or a service due as it happens.
- Connecting other systemsRead any integration's label correctly and pick the right path for a system that has no named connection.