DOCS

Search the developer hub

Jump to a page or an endpoint.

Webhooks

Get events pushed to your server as they happen, signed so you can trust them.

Webhooks tell your systems when something happens in an organisation, so you do not have to poll the API. An owner or admin adds an endpoint in the office under Settings → Developer → Webhooks and chooses which events it receives, or all of them.

Events

type Sent when
person.added Someone is added to the team
person.archived Someone is marked as having left the team
job.created A job is created, in the office or from an online booking
job.status_changed A job moves to another status, including when it is closed out, cancelled or booked
quote.sent A quote is sent to the customer
quote.accepted A quote is accepted, by the customer online or marked by the office
quote.declined A quote is declined, by the customer online or marked by the office
invoice.issued An invoice is issued
invoice.paid An invoice is paid: by card online, marked paid, or paid in Xero

An endpoint on All events also receives event types added later. Handle types you do not recognise by ignoring them and answering 200.

The data of each event

data is a short summary of the record as it stood when the event happened, not the whole record. id is CREW25's id for it. number is the number people see without the organisation's prefix or padding, so job JB-00042 is 42. Amounts are whole cents.

Object Events Fields
person person.added, person.archived id, name; personType (admin, office, worker or subcontractor) on person.added only
job job.created id, number, title, status (enquiry or scheduled)
job job.status_changed id, number, from and status, each enquiry, scheduled, in_progress, on_hold, completed or cancelled
quote quote.* id, number, totalCents: the quote's total before GST
invoice invoice.* id, number, totalCents: the invoice's total including GST

What your endpoint receives

A POST with a JSON body:

{
  "id": "5f0c1b9e-8d7a-4e6f-9a2b-3c4d5e6f7a8b",
  "type": "person.added",
  "createdAt": "2026-09-24T21:31:11.000Z",
  "organisationId": "org_a1b2c3d4e5f6a7b8",
  "data": {
    "object": "person",
    "id": "2c9d7e1f-4a3b-4c5d-9e8f-7a6b5c4d3e2f",
    "name": "Sam Carpenter",
    "personType": "worker"
  }
}

Each request carries these headers:

Header Value
CREW25-Event-Id The event's id. The same on every retry of one event.
CREW25-Event-Type The event's type.
CREW25-Signature t=<unix seconds>,v1=<signature>. See below.

Verifying the signature

Every endpoint has a signing secret, starting whsec_, shown in its settings. Check every request before trusting it:

  1. Split CREW25-Signature on , into t and v1.
  2. Compute an HMAC-SHA256 of <t>.<raw request body>, keyed with the signing secret, as hex.
  3. Compare it with v1 in constant time, and reject the request if they differ.
  4. Reject the request if t is more than five minutes from your clock.

Use the raw body exactly as it arrived. Parsing and re-serialising the JSON changes the bytes and the signature will not match.

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(header, rawBody, secret) {
  const parts = Object.fromEntries(header.split(',').map((part) => part.split('=')));
  const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;

  return fresh && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ''));
}

Rotating the secret in settings replaces it immediately. Update your receiver straight away.

Responding and retries

Answer with any 2xx status within 10 seconds. Do slow work after you respond, not before.

Anything else, including a redirect or a timeout, is a failed delivery. CREW25 retries it after 1, 5, 30, 120 and 360 minutes, then gives up. Because an event can arrive more than once, use CREW25-Event-Id to ignore ones you have already handled. Events are not guaranteed to arrive in order; compare createdAt if order matters.

Settings shows the latest deliveries with their response codes, and Send test event posts a webhook.test event to check an endpoint straight away.

Endpoint requirements

Endpoints must use https:// and be reachable on the public internet. CREW25 refuses URLs that point at private or local network addresses.