When to use webhooks
Without webhooks, an integration has to keep asking the API whether anything has changed. With a webhook, OB calls your server as soon as the event happens, so your system stays up to date without polling and without using up the API rate limit.
Webhooks are part of the API custom feature, so that feature must be enabled. Each business has one webhook endpoint, and your developer needs a server with a public HTTPS address to receive the requests.
Set up the webhook
- Go to Admin → Custom Features → API → Webhooks.
- Enter your server's address in Webhook URL. It must start with
https://and be reachable from the public internet. Redirects are not followed. - Leave Enabled on.
- Under Webhook events, switch on the events you want. A new endpoint starts with all of them on.
- Select Save.
- Under the address, select Copy below Signing secret and give the secret to your developer. It stays masked until you select the eye icon, and you can view or copy it again at any time, even before an address is saved.
To pause delivery later, turn Enabled off and save. The address and event choices are kept.
Each time the address is changed, OB sends it one test request and shows the result under the field: the response code in green for a 2xx answer, or a warning with the code or connection error. The test is a ping event with empty data; it is signed with your signing secret, or unsigned if the secret has never been shown and no address has been saved. A warning does not stop you saving. Your server should ignore the ping type.
Events you can receive
| Event | Sent when | Data |
|---|---|---|
reservation.created | A booking is confirmed. A booking waiting for payment is sent once it is confirmed. | Reservation |
reservation.cancelled | A booking is cancelled, by the customer or by staff, including when a whole session is cancelled. | Reservation |
reservation.attendance | The attendance status of a booking changes, for example at check-in. | Reservation |
calendar.created | A session is added to the schedule. | Calendar entry |
calendar.updated | Staff edit a session, for example its time, room or size. | Calendar entry |
calendar.cancelled | A session is cancelled. | Calendar entry |
user.created | A customer registers or is added by staff. | User |
user.updated | Staff edit a user, or a customer edits their own profile. | User |
user-pass.created | A customer holds a new pass. Sent once the payment is verified, so a bank transfer is announced after staff confirm it. | User pass |
user-pass.updated | Staff edit a pass a customer holds, for example its expiry date or quantity. Not sent when a booking uses the pass. | User pass |
payment.created | A payment is recorded, including a bank transfer that is still waiting to be verified. | Payment with its items and total |
payment.updated | Staff edit a payment, or the payment is verified. | Payment with its items and total |
class.created, class.updated, class.deleted | Staff create, edit or delete a class. | Class |
pass.created, pass.updated, pass.deleted | Staff create, edit or delete a pass. | Pass |
The Data is exactly the object the API returns for that record, for example the same object as GET /v1/reservations/{id} or GET /v1/payments/{id}, so one set of code can read both.
What each request contains
Every event is one POST request with a JSON body:
{
"id": "2b4f1c0e-8a7d-4c1e-9f3a-6d5e4c3b2a10",
"type": "reservation.created",
"createdAt": "2026-11-10T09:30:00Z",
"data": { ... }
}id— unique for this delivery. Use it to ignore one you have already handled.type— the event name.createdAt— when the event happened, in UTC.data— the record the event is about.
Three headers are included: X-OmceanBooking-Event (the event name), X-OmceanBooking-Delivery (the same value as id), and X-OmceanBooking-Signature.
The full field list for every object is in the API documentation.
Verify the signature
Anyone who learns your address could send it a request, so always check the signature before trusting one. The X-OmceanBooking-Signature header looks like t=1794300000,v1=5f2b…, where t is a timestamp and v1 is the signature.
- Take the timestamp, a full stop, and the raw request body exactly as received:
t + "." + body. - Calculate an HMAC-SHA256 of that text using your signing secret.
- Compare the result with
v1. If they differ, reject the request. - Reject requests whose timestamp is more than a few minutes old.
Use the raw body, not a re-encoded copy of the parsed JSON, or the signature will not match. A code sample is in the API documentation.
Respond, retries, and duplicates
Reply with any 2xx status within 10 seconds to confirm receipt. Do slow work after replying.
If your server replies with another status, times out, or cannot be reached, OB tries again after 1 minute, then 5 minutes, then 15 minutes. After those three retries the delivery is marked as failed and is not sent again.
- Duplicates can happen. A retry may arrive after a request your server did process. Store the
idand ignore one you have seen. - Order is not guaranteed. A retried event can arrive after a newer one. Use
createdAt, or fetch the current record from the API, when order matters. - Each event is sent as it happens. Events are not batched.
Replace the signing secret
If the secret may have been exposed, go to Admin → Custom Features → API → Webhooks, select Regenerate below Signing secret, and confirm. The new secret is shown straight away. A secret that was only mislaid does not need replacing: select the eye icon to see it again.
The old secret stops working straight away. Requests sent after that are signed with the new one, so your server will reject them until it has been updated. Plan the change with your developer.
Troubleshooting
- The address is rejected when saving. It must start with
https://, its host name must resolve, and it must not point to a private or internal network. - Nothing arrives. Check that the API custom feature is enabled, that Enabled is on, and that the event you expect is switched on.
- Check what was sent. Admin → Custom Features → API → Usage → Webhooks lists every event with its status, the response code your server returned, the number of attempts, how long the last attempt took, and any error.
- A pass purchase did not send
user-pass.created. A pass paid by bank transfer is announced only after staff verify the payment.payment.createdis sent straight away. - The signature does not match. Use the raw request body, and make sure the server holds the latest secret after a regeneration.
- Test tokens. The API sandbox used by test tokens never sends webhooks.
