For the technically minded

Outbound webhooks

Doris can send a signed HTTP POST to a URL you control the moment a booking is confirmed, moved, or cancelled. Wire it into Zapier, n8n, or a script of your own and build whatever your organisation needs: entries in your accounts package, a door that unlocks for the session, heating that comes on an hour before, a display board in the hall.

Doris does its job, publishes what it knows, and gets out of the way. What the receiving system does with an event is yours to decide - and yours to maintain.

Where to set it up

An owner or admin creates the endpoint in the Doris app under Settings, in the Webhooks card. You choose which events to send and whether to include member contact details (off by default - see the payload rules below).

You are shown a signing secret exactly once at creation. Store it like a password. If you lose it, rotate the secret from the same card. A Send test event button fires a signed test.ping so you can check your wiring end to end, and a delivery log shows recent deliveries, their response codes and attempt counts.

Events

Version 1 sends four event types. An event only ever fires on a transition your receiver could have observed: booking.confirmed fires when a booking enters the confirmed state, and booking.cancelled fires only for bookings previously announced as confirmed. A pending request that gets rejected, or an acceptance invitation that gets declined, was never announced - so its demise produces nothing.

booking.confirmed

A booking entered the confirmed state: created and confirmed immediately, approved by an admin, or accepted by the member. Recurring series send one event per occurrence.

booking.cancelled

A previously confirmed booking was cancelled - by the member, by an admin, by a series cancellation, or by Doris housekeeping (for example when a departed member's future bookings are released).

booking.moved

A confirmed booking changed room, date, or time. The payload carries the previous values so your automation can reconcile. A room swap between two bookings sends two booking.moved events.

invoice.created

A billing statement was generated for a member - one event per member per period, carrying the full line items. This is the event that drives period invoicing into an accounts package.

invoice.voided

A previously announced statement was voided in Doris and its bookings released back to the unbilled pool. Reverse or credit the matching invoice in your accounts package.

invoice.paid

A statement was marked paid in Doris, with the payment method (bacs, cash, or other) and timestamp.

test.ping

A test event fired from the settings card. Same envelope and signature as the real thing; safe to ignore in production recipes.

Payload

Every request body is a JSON envelope with a stable shape. The version field is your compatibility contract: parse defensively and pin your recipe to version 1 semantics.

{
  "event_id": "01JZXK7Q2M3N4P5R6S7T8V9WXY",
  "version": 1,
  "type": "booking.confirmed",
  "occurred_at": "2026-07-10T09:00:12.000Z",
  "data": {
    "booking_id": "01JZXK6H8K2M4N6P8R0T2V4X6Z",
    "booking_reference": "DOR-2V4X6Z",
    "room_id": "01JZX0AC3E5G7J9L1N3Q5S7V9X",
    "room_name": "Main Hall",
    "booking_date": "2026-07-15",
    "start_time": "18:00",
    "end_time": "21:00",
    "status": "confirmed",
    "booking_type": "standard",
    "member_ref": "01JZWZZY1B3D5F7H9K1M3P5R7T",
    "cost_pence": 2400,
    "currency": "GBP"
  }
}

The data object carries business facts only. By default no personal data ever leaves Doris: the member is identified by member_ref, a stable opaque ID. If (and only if) your organisation enables the member contact option on the endpoint, member_name and member_email are added - useful when a recipe must match members to contacts in an accounts package. Phone numbers, notes, and custom field values are never included, under any setting.

booking.moved events additionally carry previous_room_id, previous_room_name, previous_booking_date, previous_start_time and previous_end_time.

cost_pence and currency appear only when the room's pricing produces a charge for the slot.

Billing events - period invoicing

If your organisation uses Doris statements, the invoice events give you consolidated period invoicing for free: at the end of each billing period, generate statements in Doris and every member's statement fires one invoice.created event - complete with line items priced from the locked-in statement snapshots - so a Zapier or n8n recipe can raise one invoice per member in QuickBooks, Xero, or FreeAgent in a single run.

{
  "event_id": "01JZXM9T4V6X8Z0B2D4F6H8K0M",
  "version": 1,
  "type": "invoice.created",
  "occurred_at": "2026-08-01T10:02:41.000Z",
  "data": {
    "invoice_id": "01JZXM8R2T4V6X8Z0B2D4F6H8K",
    "invoice_reference": "DOR-INV-4F6H8K",
    "status": "draft",
    "billing_period_start": "2026-07-01",
    "billing_period_end": "2026-07-31",
    "amount_pence": 7200,
    "currency": "GBP",
    "member_ref": "01JZWZZY1B3D5F7H9K1M3P5R7T",
    "lines": [
      {
        "booking_id": "01JZXK6H8K2M4N6P8R0T2V4X6Z",
        "booking_reference": "DOR-2V4X6Z",
        "date": "2026-07-08",
        "start_time": "18:00",
        "end_time": "21:00",
        "room_name": "Main Hall",
        "description": "Evening rate",
        "amount_pence": 2400
      }
    ]
  }
}

invoice.created fires at generation, while the statement is still a draft in Doris. If you void a statement before (or after) sending it, invoice.voided follows - your recipe should delete or credit the matching invoice. When you record payment in Doris, invoice.paid carries the payment method so the accounts entry can be settled. The invoice_reference (DOR-INV-XXXXXX) is the stable key to match on, and event_id deduplication applies as everywhere else.

As with booking events, member_name and member_email appear only when the endpoint's member contact option is enabled - for invoicing recipes you will almost certainly want it on, since the accounts package needs a contact name to raise an invoice against.

Verifying the signature

Every delivery is signed with your endpoint's secret using HMAC-SHA256, in the same format Stripe uses. The X-Doris-Signature header carries a timestamp and a hex digest of "{timestamp}.{raw body}". Recompute the digest with your secret and compare; reject anything older than five minutes to rule out replays.

POST /your-endpoint HTTP/1.1
Content-Type: application/json
X-Doris-Event: booking.confirmed
X-Doris-Event-Id: 01JZXK7Q2M3N4P5R6S7T8V9WXY
X-Doris-Delivery-Attempt: 1
X-Doris-Signature: t=1752138012,v1=5f8a…e3c1
// Node.js - verify X-Doris-Signature ("t=<unix>,v1=<hex>")
const crypto = require("node:crypto");

function verifyDorisSignature(rawBody, signatureHeader, secret) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(signatureHeader);
  if (!m) return false;
  const [, t, theirs] = m;
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // replay guard
  const ours = crypto.createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(ours), Buffer.from(theirs));
}

Honesty note: Zapier's Catch Hook does not verify signatures unless you add a code step, and most people won't. If you skip verification, the unguessable endpoint URL is your effective credential - use the long random URL Zapier generates and share it with no one.

Receiver checklist

  • Verify the signature - or, if you can't, treat the endpoint URL itself as a secret.
  • Deduplicate on event_id. Delivery is at-least-once, so the same event can arrive twice; the event_id never changes across retries.
  • Discard stale events: compare occurred_at against the state you already hold, so a retried booking.moved arriving after a booking.cancelled doesn't resurrect a dead booking.
  • Respond with a 2xx quickly and do your processing afterwards. Anything else (including a redirect) counts as a failed delivery and will be retried.

Delivery and retries

Delivery is at-least-once and best-effort. The first attempt goes out within seconds of the event; failures are retried with growing backoff (5 minutes, 15 minutes, 1 hour, 4 hours, 12 hours, then daily) for up to 8 attempts across roughly three and a half days.

If an endpoint keeps failing for days, Doris pauses it and notifies your admins in the app - a dead endpoint should not burn retries forever. Re-enabling it is one click in the settings card.

A health warning for the physical world

Webhook delivery is best-effort. An event can arrive minutes late after backoff, or - if your receiving service is down past the retry window - not at all. Webhooks are therefore unsuitable as the sole mechanism for anything safety- or security-critical. A door-lock recipe must fail safe: a late or missing unlock event should mean the door stays locked and someone uses a key. A missing lock event must never mean the building stays open overnight - the lock's own schedule or auto-relock is the backstop, not Doris.