OrdstageDocs

Concepts

Webhooks

Events Ordstage sends to your endpoint, their payloads, retries, and verifying signatures in Node, Python and PHP.

Webhooks tell your system when something changes in Ordstage, for example a booking made by the assistant, a conversation handed to the team, a new contact or a lead's status, so you don't have to poll. An area's events are sent while the business's API access includes it (its …:read scope) and its module or feature is on.

Setting up an endpoint

  1. In Settings → Developers → Webhooks, add an endpoint: a public https:// address on your server.
  2. Choose the events to receive (none chosen: every event, including ones added later), and optionally limit booking events to some calendars.
  3. Copy the signing secret (it starts with whsec_). It is shown once; keep it on your server like a password.
  4. Press Send test. Ordstage sends a ping event so you can check your endpoint and its signature check.

Events

Event Sent when
booking.created Sent when a booking is made from any source (the assistant, the team, another system).
booking.confirmed Sent when the team approves a booking that waited for approval.
booking.updated Sent when a booking's time, resource, details or status changes.
booking.cancelled Sent when a booking is cancelled, by the customer, the team or a system.
booking.conflict Sent when bookings from different sources overlap beyond a resource's capacity (for example a booking you pushed for a time that was already full). The team is alerted too.
availability.closed Sent when time is closed for online booking across a calendar (scope: online).
assistant.published Sent when an assistant's setup is published (from Studio or the API) and it starts answering with it.
assistant.paused Sent when an assistant stops answering customers; conversations go to the team.
assistant.resumed Sent when a paused assistant answers customers again.
business.updated Sent when the business's name, website, logo, colours, timezone or language change, from Settings or the API.
contact.created Sent when a contact is created anywhere: a new person messages the business, an order or booking is made, or the API.
contact.updated Sent when a contact's details, tags or attributes change.
contact.deleted Sent when a contact is deleted; contact is how it was.
contact.merged Sent when a duplicate is merged into a contact: contact is the one kept, merged_contact_id the one removed.
lead.created Sent when someone asks to be contacted (through the assistant) or a lead is added through the API.
lead.updated Sent when a lead's details, note, assignee or status change.
lead.status_changed Sent when a lead moves to another status.
lead.deleted Sent when a lead is deleted; lead is how it was.
conversation.handed_over Sent when a conversation moves from the assistant to the team (the assistant, a teammate or the API handed it over).
conversation.returned_to_assistant Sent when the team gives a conversation back to the assistant to answer.
conversation.closed Sent when a conversation is resolved or closed, from the Inbox or the API.
conversation.reopened Sent when a resolved or closed conversation is opened again by the team or the API.
conversation.updated Sent when a conversation's priority, assignee, team, labels or waiting status change.
conversation.reply_sent Sent when a teammate's reply is sent from the Inbox or the API (not the assistant's own answers).
member.invited Sent when an invitation is created, in Settings or by the API.
member.invitation_revoked Sent when a pending invitation is revoked.
member.role_changed Sent when a member's role changes.
member.removed Sent when someone is removed from the team (their account stays).
ping Sent when you press Send test on a webhook in Developers settings.

The API reference shows every event's full payload schema under Webhooks.

What a delivery looks like

Each delivery is an HTTP POST with a JSON body:

POST /webhooks/ordstage HTTP/1.1
Content-Type: application/json
User-Agent: Ordstage-Webhooks/1
X-Ordstage-Event: booking.created
X-Ordstage-Delivery: 6f1c2e4a-8b3d-4c5e-9f70-1a2b3c4d5e6f
X-Ordstage-Timestamp: 1793260800
X-Ordstage-Signature: sha256=<hex digest>

{"id":"6f1c2e4a-8b3d-4c5e-9f70-1a2b3c4d5e6f","event":"booking.created","created_at":"2026-10-29T10:00:00+00:00","data":{"booking":{"id":"…","reference":"BK-7Q2M","status":"confirmed","…":"…"}}}
Header Meaning
X-Ordstage-Event The event name.
X-Ordstage-Delivery The delivery id, also the body's id. The same on every retry of this delivery.
X-Ordstage-Timestamp Unix time (seconds) when this attempt was signed.
X-Ordstage-Signature sha256= followed by the hex HMAC-SHA256 signature (below).

Answering and retries

  • Answer with any 2xx status within 10 seconds. Do slow work after answering (for example on a queue).
  • Any other answer, a timeout or a connection error is retried after about 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 3 hours, 6 hours and 12 hours (about a day in all), then the delivery is marked failed.
  • An endpoint that fails 50 times in a row is paused. Fix it, then resume it in Settings → Developers.
  • Redirects are not followed. Ordstage only connects to public addresses.
  • The same delivery can arrive more than once, and deliveries can arrive out of order. Use the delivery id to skip ones you've handled, and the object's own fields (for example a booking's status) rather than the event order to decide its current state. When in doubt, read the object from the API.

Verifying signatures

Every delivery is signed with your endpoint's secret. Always verify the signature before trusting a delivery, and refuse old ones, so nobody can forge or replay events to your endpoint.

The signature is an HMAC-SHA256, keyed with the whole secret (including whsec_), of this exact string:

{X-Ordstage-Timestamp}.{X-Ordstage-Delivery}.{raw request body}

hex-encoded and prefixed with sha256=. To verify a delivery:

  1. Read the raw body bytes before any JSON parsing (re-serialised JSON won't match).
  2. Compute the signature from the timestamp header, the delivery header and the raw body.
  3. Compare it with X-Ordstage-Signature in constant time.
  4. Refuse deliveries whose timestamp is more than 5 minutes from your clock.
  5. Answer 2xx, then process the event (skipping delivery ids you've already handled).
import crypto from "node:crypto";
import express from "express";

const SECRET = process.env.ORDSTAGE_WEBHOOK_SECRET; // whsec_…
const TOLERANCE_SECONDS = 300;

export function verifyOrdstageSignature(secret, timestamp, deliveryId, rawBody, signature, now = Date.now() / 1000) {
  const expected = "sha256=" + crypto.createHmac("sha256", secret)
    .update(`${timestamp}.${deliveryId}.`)
    .update(rawBody)
    .digest("hex");
  const given = Buffer.from(String(signature ?? ""));
  const fresh = Math.abs(now - Number(timestamp)) <= TOLERANCE_SECONDS;
  return fresh && given.length === expected.length && crypto.timingSafeEqual(given, Buffer.from(expected));
}

const app = express();
// express.raw keeps the exact bytes Ordstage signed.
app.post("/webhooks/ordstage", express.raw({ type: "application/json" }), (req, res) => {
  const ok = verifyOrdstageSignature(SECRET, req.get("X-Ordstage-Timestamp"), req.get("X-Ordstage-Delivery"),
    req.body, req.get("X-Ordstage-Signature"));
  if (!ok) return res.sendStatus(400);
  const event = JSON.parse(req.body.toString("utf8"));
  res.sendStatus(200);
  // handle event.event / event.data here, skipping event.id values you've already seen
});
app.listen(3000);

Good practice

  • Keep the endpoint fast: verify, store, answer 2xx, then process.
  • Treat webhooks as a signal. For anything important, read the current state from the API.
  • Rotate a secret that may have leaked by deleting the endpoint and adding it again (the new endpoint gets a new secret).
  • Use a separate endpoint (and secret) for your test workspace.