Skip to content

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

  1. 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.

  2. Step 2.

    Enter the Endpoint URL, pick the Events (none picked means all events) and the Body format.

  3. Step 3.

    Copy the signing secret. It is shown once.

Events

Event names are never renamed; new ones are only added.

EventFires when
quote.createdA quote is created
quote.sentA quote is sent to the customer
quote.acceptedA quote is accepted
quote.declinedA quote is declined
order.createdAn order is created
order.updatedAn order changes status (other than to cancelled), is edited or is emailed
order.cancelledAn order is cancelled
service_item.updatedA service item changes
design.savedA design is saved
design.exportedA design is exported
quote.supersededA quote is replaced by a new version
quote.expiredA quote passes its valid until date
quote.viewedThe customer opens the quote
order.invoiceableAn order is fulfilled, so an invoice can be raised
purchase_order.sentA purchase order is sent to the supplier
purchase_order.receivedGoods are received against a purchase order
order.dispatchedAn order's goods leave
order.deliveredAn order is delivered
delivery.dispatchedA consignment is dispatched
delivery.completedA consignment is delivered
delivery.failedA delivery attempt fails
monitor.firedA monitor's condition is met

Payload and headers

json
{
  "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

typescript
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);
}
python
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.