DOCS

Search the developer hub

Jump to a page or an endpoint.

Webhooks

Get order and job 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 → Webhooks and chooses which events it receives, or all of them.

Events

type Sent when data is
order.created An order is placed, from the office, the booking page or the API An order
order.cancelled An order is cancelled An order
job.started A driver starts a job and heads to the site A job
job.completed A driver completes a job A job
job.failed A driver records a failed attempt A job

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

What your endpoint receives

A POST with a JSON body:

{
  "id": "5f0c1b9e-8d7a-4e6f-9a2b-3c4d5e6f7a8b",
  "type": "job.completed",
  "createdAt": "2026-09-24T21:31:11.000Z",
  "organisationId": "org_a1b2c3d4e5f6a7b8",
  "data": {
    "object": "job",
    "id": "2c9d7e1f-4a3b-4c5d-9e8f-7a6b5c4d3e2f",
    "reference": "J-20931",
    "status": "done"
  }
}

data is the same object the API returns for that order or job (shortened above), as it stood when the event happened. Fetch it again from the API if you need its current state.

Each request carries these headers:

Header Value
OPS25-Event-Id The event's id. The same on every retry of one event.
OPS25-Event-Type The event's type.
OPS25-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 OPS25-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. OPS25 retries it after 1, 5, 30, 120 and 360 minutes, then gives up. Because an event can arrive more than once, use OPS25-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. OPS25 refuses URLs that point at private or local network addresses.