Guide to Ventrata Webhooks: Features and Best Practices
Ventrata is a ticketing and booking platform for tours, attractions and experiences. Partners connect to it through the OCTO API, an open standard for the tours and activities industry, and webhooks are the part of that API that pushes changes to you: an order is updated, a booking is confirmed or cancelled, availability shifts, or a product is edited.
This guide covers how Ventrata webhooks are created and authenticated, what a delivery contains, the gaps in Ventrata's documentation, and the practices that keep a Ventrata receiver reliable.
What are Ventrata webhooks?
Ventrata webhooks belong to the octo/webhooks capability of the OCTO API. Ventrata documents them as API resources: you create, list, update and delete webhooks through the API, with octo/webhooks in the Octo-Capabilities header of each request. Each webhook has one event, a URL, a map of HTTP headers to send with every delivery, and two switches, retryOnError and useContactLanguage.
When the event happens, Ventrata sends a POST to the URL with a JSON body. The body carries the object that changed, serialised exactly as the matching GET endpoint would return it, plus a diff of what changed for orders, bookings and products.
Ventrata does not sign webhooks. You authenticate them with a header of your own: put a secret in the webhook's headers map, such as X-Webhook-Secret, and compare it on your side.
Ventrata webhook features
| Feature | Details |
|---|---|
| Configuration | OCTO API: POST /webhooks, GET /webhooks, PATCH /webhooks/{webhookId}, DELETE /webhooks/{webhookId} or DELETE /webhooks by url |
| Capability | octo/webhooks in the Octo-Capabilities header |
| Events | order_update, booking_update, availability_update, product_update |
| Method and format | Always POST, JSON body |
| Body | event, webhook, supplier, and order, booking, product or availability, plus diff |
| Authentication | Headers you set in the webhook's headers map |
| Signature and timestamp | None |
| Retries | Per webhook with retryOnError; schedule not documented |
| Timeout and source IPs | Not documented |
| Payload shape | The same serialisation as the GET endpoints, with the webhook's stored capabilities applied |
Events and payloads
Ventrata's webhooks page lists four events, one per resource: order_update, booking_update, availability_update and product_update. The schema examples on the same page write the event as booking.updated, though, so check which spelling the API accepts when you create a webhook and which one arrives in event, and dispatch on it accordingly.
A delivery carries the event, the webhook it belongs to, the supplier and the objects the event concerns. This is the example from Ventrata's schema, cut down to a few fields per object:
{
"event": "booking.updated",
"webhook": {
"id": "webhook_booking_update",
"event": "booking.updated",
"url": "https://www.city-sightseeing.com"
},
"supplier": {
"id": "3d6f0a3a-59d4-4b16-a0c5-11d2d8a4e6b7",
"name": "City Sightseeing Group",
"reference": "CITY_SIGHTSEEING_GROUP"
},
"booking": {
"uuid": "89fe0192-ddcd-430a-b285-e1396a4725d2",
"status": "ON_HOLD",
"optionId": "94cdd032-3d32-416d-b0a4-abf8b7495b8b",
"id": "89fe0192-ddcd-430a-b285-e1396a4725d2"
},
"diff": [
{
"op": "add",
"path": "path-example",
"was": "was-example",
"value": "value-example"
}
]
}
The objects are full API objects, not summaries:
orderhas the shape ofGET /orders/:orderId.bookinghas the shape ofGET /bookings/:uuid, includingstatus,contact,utcUpdatedAtandtestMode.producthas the shape ofGET /products/:productId.availabilityis one item fromGET /availability.
Fields added by other capabilities, such as pricing, content, extras or questions, appear in the payload the same way they appear in those responses, according to the capabilities stored on the webhook. One exception: with octo/cardPayments active, Ventrata does not generate card payment session data for webhooks, so detailed cardPayment payloads are left out.
diff is sent for order, booking and product updates. Each entry has an op of add, remove or replace, a path, the previous value in was and the new one in value.
Inspect Ventrata webhooks as they arrive. Create a webhook pointing at a Hookdeck Inspect URL to see the full payload your capabilities produce, with no account or setup required.
Setting up Ventrata webhooks
Generate a long random secret first, for example with openssl rand -hex 32. Then create the webhook through the OCTO API at https://api.ventrata.com/octo, authenticated as your API connection, with octo/webhooks in the Octo-Capabilities header:
POST /octo/webhooks
Octo-Capabilities: octo/webhooks
Content-Type: application/json
{
"event": "booking_update",
"url": "https://api.yourcompany.com/webhooks/ventrata",
"headers": {
"X-Webhook-Secret": "<long random secret>"
},
"retryOnError": true
}
The webhook stores the capabilities from that request's Octo-Capabilities header, not from the body, and uses them to serialise every payload. Add any other capabilities whose fields you want in the payload, such as octo/pricing, to the same header. For order_update, octo/cart is always included.
The response is the webhook object with its id. Use GET /webhooks to list the webhooks for your connection, PATCH /webhooks/{webhookId} to change one, and DELETE /webhooks/{webhookId} or DELETE /webhooks with a url to remove them.
To route through Hookdeck, create a Hookdeck source with the Ventrata type and use its URL as the webhook's url. Ventrata webhooks are not signed, so add a secret header when you create the webhook through the OCTO API and enter it in the source:
- API Key Header: the header name from the
headersmap, such asX-Webhook-Secret. Hookdeck looks it up case-insensitively. - API Key: the header's whole value, exactly as you set it. If you put a
Bearerprefix in the value, include it here too.
The source accepts POST only, which is the method Ventrata always uses.
Securing Ventrata webhooks
Read the header you chose, compare its whole value with the one you stored in constant time, and reject the request if either is missing. Do the check before you parse the body.
const crypto = require('crypto');
const express = require('express');
const HEADER_NAME = process.env.VENTRATA_WEBHOOK_HEADER || 'X-Webhook-Secret';
const HEADER_VALUE = process.env.VENTRATA_WEBHOOK_SECRET;
function safeEqual(received, expected) {
const a = Buffer.from(received, 'utf8');
const b = Buffer.from(expected, 'utf8');
// timingSafeEqual throws on unequal lengths, so check first.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
app.post('/webhooks/ventrata', express.raw({ type: '*/*' }), (req, res) => {
const received = req.get(HEADER_NAME);
if (!HEADER_VALUE || typeof received !== 'string' || !safeEqual(received, HEADER_VALUE)) {
return res.sendStatus(401);
}
let payload;
try {
payload = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
// Store payload.event and the changed object, respond, then process asynchronously.
res.sendStatus(200);
});
The same check in Python:
import hmac
import os
HEADER_NAME = os.environ.get("VENTRATA_WEBHOOK_HEADER", "X-Webhook-Secret")
HEADER_VALUE = os.environ.get("VENTRATA_WEBHOOK_SECRET", "")
def verify_ventrata_request(headers) -> bool:
if not HEADER_VALUE:
return False # never accept when unconfigured
received = headers.get(HEADER_NAME) # case-insensitive in Starlette and Flask
if not received:
return False
# Compare bytes: compare_digest raises TypeError on a non-ASCII str.
return hmac.compare_digest(received.encode("utf-8"), HEADER_VALUE.encode("utf-8"))
The header value is static. It authenticates the sender, not the body, and with no timestamp a captured request can be replayed. Serve the endpoint over HTTPS only, and if the secret might have leaked, replace it with PATCH /webhooks/{webhookId}.
Ventrata says the webhook field in a delivery is the same object its webhook endpoints return, and that object's schema includes the headers map. Its example payload shows only id, event and url, so check a real delivery: if the headers are there, your secret is in the body as well, and you should redact it before logging or forwarding payloads.
Make Ventrata webhooks production-ready. Hookdeck Event Gateway checks the header you configure, stores every booking change durably, and retries delivery to your service on a schedule you control.
Ventrata webhook limitations and pain points
Retry behaviour is a yes or no
The Problem: You cannot tell how long Ventrata keeps trying after your endpoint fails, or whether a booking change was dropped.
Why It Happens: Each webhook has a retryOnError switch, but Ventrata does not document the schedule, the number of attempts, the timeout or what counts as an error. There is no documented way to list past deliveries or resend one.
Workarounds:
- Set
retryOnErrortotrueon every webhook. - Respond with a
2xxquickly and process afterwards. - Reconcile bookings and orders against the OCTO API after an outage, using
utcUpdatedAt.
How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives and stores it, then delivers to your service with retries you configure. You can replay any event after a fix, and issues alert you when deliveries start failing.
Updates can silently drop capabilities
The Problem: After a change to a webhook, payloads stop including pricing, content or other fields your code relies on.
Why It Happens: A webhook's capabilities come from the Octo-Capabilities header, not the body, and Ventrata recalculates them on every PATCH. An update sent with only octo/webhooks in the header leaves the webhook with no extra capabilities, even if you only meant to change the URL.
Workarounds:
- Send the full
Octo-Capabilitiesheader on every create and update. - After each change, call
GET /webhooksand check thecapabilitiesarray.
How Hookdeck Can Help: Hookdeck stores the payloads it receives, so you can compare deliveries from before and after a change and see exactly which fields went missing.
The event name is ambiguous
The Problem: A handler that switches on booking_update never matches, or one that expects booking.updated does not.
Why It Happens: Ventrata's webhooks page lists the events as order_update, booking_update, availability_update and product_update, while its schema examples, including the example delivery, use booking.updated.
Workarounds:
- Log
eventfrom your first real delivery and code against that. - Until you have, accept both spellings.
How Hookdeck Can Help: Hookdeck filters can route on event and transformations can normalise it to one spelling before it reaches your service.
Large payloads with personal data
The Problem: Each delivery carries a whole order or booking, including customer contact details.
Why It Happens: Ventrata serialises the full object the same way its GET endpoints do, with every active capability applied, rather than sending an ID for you to fetch.
Workarounds:
- Subscribe each webhook with only the capabilities its consumer needs.
- Treat stored payloads as personal data and apply your retention rules to them.
- Use
testModeon bookings and orders to keep test traffic out of production data.
How Hookdeck Can Help: Hookdeck transformations can strip fields such as contact before a payload reaches a downstream service that does not need them.
Best practices
Always set a secret header
A webhook without one sends nothing your endpoint can check. Use a long random value and give each environment its own webhook and secret.
Compare the whole value
Store the exact value from the headers map and compare against it in full, without trimming or parsing a prefix.
Make handlers idempotent
With retries switched on, the same change can arrive more than once. Use the booking uuid or order id together with utcUpdatedAt to skip changes you have already applied. See our guide to webhook idempotency.
Acknowledge fast and process asynchronously
Check the header, store the payload, return 200, and run your booking logic afterwards. See why to process webhooks asynchronously.
Check your webhooks after every change
Call GET /webhooks after any create or update and confirm the event, URL and capabilities are what you expect.
Conclusion
Ventrata sends OCTO webhooks for orders, bookings, availability and products as POST requests carrying the full API object and a diff, with no signature and an undocumented retry schedule. Set a long random secret in each webhook's headers map, compare it in constant time over HTTPS, send the full Octo-Capabilities header whenever you create or update a webhook, and process every delivery idempotently.
Hookdeck Event Gateway checks the header, keeps every delivery, and retries and replays it to your service, so a short outage does not cost you a booking change.
Get started with Hookdeck for free and handle Ventrata webhooks reliably in minutes.