Skip to main content

Booking Webhooks

External booking systems push reservations into Vuoro over webhooks. Bookings appear as a Bookings row on the roster grid to help with staffing decisions. They never create shifts. Administrators configure mappings and filters under Settings > Booking Sources (see Booking Sources). This page describes the wire format.

Endpoints​

Each tenant has an unguessable tenantToken and a webhook signing secret. A Company Admin can view both with GET /api/v1/webhook-config and rotate the secret with POST /api/v1/webhook-config/regenerate-secret.

RoutePayloadAuthentication
POST /api/v1/webhooks/bookings/:tenantTokenNormalized JSON (below)Token in the URL and an HMAC signature header
POST /api/v1/webhooks/bookings/:tenantToken/planyoPlanyo notification payloadToken in the URL

Both routes are public (no user session) and exempt from rate limiting. Both feed the same processing pipeline, so deduplication, updates, cancellations, filters and location mapping behave the same way.

Use the normalized route for any new integration, such as the in-house Sessiq booking platform that is replacing Planyo. The Planyo route is kept for the legacy integration.

Normalized Payload​

{
"source": "sessiq",
"externalId": "ext-12345",
"externalSiteId": "site-001",
"title": "Birthday party — 20 guests",
"customerName": "Acme Oy",
"startsAt": "2026-10-10T10:00:00Z",
"endsAt": "2026-10-10T14:00:00Z",
"notes": "Requires extra staff",
"status": "active",
"addOns": [
{ "name": "Pizza", "quantity": 12 },
{ "name": "Birthday package" }
]
}
FieldRequiredNotes
sourceYesIdentifies the sending system. Use a value other than planyo.
externalIdYesYour booking ID. (source, externalId) is the permanent key.
externalSiteIdYesYour site identifier. It is mapped to a Vuoro location through source maps.
titleYesUp to 500 characters. Used by the allow and deny filters.
customerName, notes, endsAtNo
startsAtYesISO 8601
statusNoactive (default) or cancelled
addOnsNoThe booking's full current list of add-ons, at most 50 items. Each item is { "name": string, "quantity"?: integer }. name is 1–200 characters. Leave out quantity for yes/no add-ons; when sent, it must be a positive integer, and null is rejected.

Unknown fields are rejected with HTTP 400, including unknown keys inside an add-on item.

Signature​

Send the header X-Webhook-Signature: sha256=<hex>, where <hex> is the HMAC-SHA256 of the request body, keyed with the tenant's webhook secret. The server re-serializes the parsed body with JSON.stringify before verifying it, so sign compact JSON (no extra whitespace) with the same key order you send. A missing or invalid signature is rejected.

Response​

  • 201 { "id": "<uuid>", "created": true, "status": "active" } — a new booking was stored.
  • 201 { "id": "<uuid>", "created": false, "status": "..." } — an existing booking was updated or unchanged.
  • 201 { "ignored": true, "reason": "filter_rule:deny_match" } (or filter_rule:no_term_match) — a title filter dropped the booking and nothing was stored.

Processing Rules​

Update on conflict. Delivering the same (source, externalId) again updates title, customerName, startsAt, endsAt, notes and externalSiteId, and resolves the location again. Re-deliver whenever a booking changes.

  • If the start time, end time or site changes, the booking's handled flag is reset so a planner reviews it again.
  • Changes to the title, customer or notes only keep the existing handled state.

Add-ons. Always send the complete list, never a diff.

  • Leaving out addOns keeps the stored add-ons unchanged.
  • [] clears them.
  • Any other list replaces them.
  • A change to the add-ons (names or quantities, ignoring order) clears the handled flag, like a time change. Sending [] for a booking that never had add-ons is not counted as a change.

Cancellations. Send the booking again with "status": "cancelled". There is no delete endpoint.

  • Cancelled bookings disappear from the roster grid and the unmapped queue, but the record is kept.
  • A cancellation for a booking Vuoro has never seen is stored as a hidden tombstone.
  • A later active delivery re-activates the booking.

Location mapping. (source, externalSiteId) is matched against the tenant's source maps, ignoring case. Unmapped bookings are stored without a location and counted on the Booking Sources settings page. When a map is added later, matching existing bookings are linked to it automatically.

Title filters are per tenant, case-insensitive substring matches:

  1. Deny list — checked first. If the title contains any deny term, the booking is dropped. The deny list always wins over the allow list.
  2. Allow list — if any allow terms are configured, the booking is kept only when its title contains at least one of them.

With only a deny list configured, everything except the denied titles is kept. Administrators manage the terms through /api/v1/booking-filter-terms and /api/v1/booking-filter-deny-terms (Company Admin).

Planyo Specifics​

  • Accepted notification types: new_reservation, reservation_confirmed, and the cancellation types reservation_cancelled, reservation_canceled and cancellation. Any other type is acknowledged and ignored.
  • externalSiteId is built as <calendar>:<res_site> (for example 68900:Redi), because the site name alone is ambiguous across calendars.