OpheliaDocs
Search docs⌘K
DashboardCreate an account
Reference

Webhooks

Get told when a booking changes instead of polling for it.

Webhooks go to your organization's webhook URL, which you set on the Settings page. Every booking your organization makes is delivered there, whichever API key made it. With no URL set, nothing is sent. Delivery history is on the Webhooks page of the dashboard.

webhook_url on POST /v1/bookingsis accepted and stored, but deliveries do not use it: they always go to the organization's URL.

Events

TypeSent when
booking.created
A booking was accepted. Its status is usually processing.
booking.requires_action
The booking is waiting on the person or on you (a code, a payment authorization). next_action is included.
booking.processing
You answered with /continue and the booking is running again. Not sent on create.
booking.confirmed
The provider confirmed the booking.
booking.failed
The booking failed. error_code and error_message say why.
booking.cancelled
The booking was cancelled.
booking.modified
A modification succeeded. A refused modification sends no event; the modify call answers 422.

No event is sent when a booking expires (a booking left in requires_action for more than 20 minutes). Read the booking to see it.

Payload

A POST with a JSON body. data.object is a summary of the booking: id, object, status, provider, and when present error_code, error_message, next_action and last_modification. For everything else, read GET /v1/bookings/{id}.

json
{
"id": "evt_Xk3pQ9vR2mA",
"type": "booking.confirmed",
"created": "2026-11-14T15:02:41Z",
"data": {
"object": {
"id": "bkg_12345678",
"object": "booking",
"status": "confirmed",
"provider": "opentable"
}
}
}

Verifying the signature

Each delivery carries an Ophelia-Signature header of the form t=<unix seconds>,v1=<hex>. v1 is the HMAC-SHA256 of <t>.<raw body>, keyed with your organization's webhook signing secret, hex-encoded. Compute it over the raw bytes you received, before parsing the JSON, and compare in constant time. Every attempt is signed afresh, so t is the time of that attempt.

javascript
import crypto from 'node:crypto';
 
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));
const expected = crypto.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
return parts.v1 && parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}
Your signing secret is on the dashboard's Settings page, under Webhook signing secret: press Reveal, then Copy.

Delivery and retries

Deliveries are sent by a background worker that runs every 30 seconds, so an event usually arrives within half a minute. Any 2xx answer within 10 seconds counts as delivered. Anything else (another status, a timeout, a connection error) is retried after 30 seconds, then 2 minutes, then 10 minutes; after the fourth failed attempt the delivery is abandoned.

Delivery is at least once and events can arrive out of order. Deduplicate on the event id, and treat data.object.status as a hint: when order matters, read the booking.