Booking API
List and cancel a contact's bookings with the customer-facing Booking endpoints.
The Booking API lets your backend read and manage the bookings a contact has made on a campaign. For example, it can power an account page that shows a customer's upcoming bookings and lets them cancel.
Before you begin
- A token from the client-credentials flow, requested on your own backend and sent as
Authorization: Bearer <token>on every call. - Your API host, from Base URLs and environments. Every path below starts with
/v1/. - Confirmation from your Customer Success Manager that the Booking API is enabled for your account.
Key concepts
Before the endpoint reference, three ideas explain how identifiers fit together.
How a contact is identified
| Identifier | What it is | Who uses it |
|---|---|---|
External ID (external_id) | The ID your own system already uses for this customer. You pass it to OmniLab when fetching bookings. | You — this is your handle for the contact. |
Contact ID (contacts/<uuid>) | The platform's own identifier for the same person, resolved from your external ID. | The platform. You normally never send it. |
Public key (public_key) | The public-facing key of a campaign (interaction). It appears in experience URLs and lets you scope a fetch to a single campaign. | Identifies the campaign, not the person. |
In short: the external ID identifies the person, and the public key identifies the campaign. You only ever need to store the external ID you already have.
Campaigns, touchpoints, and the activity ID
A campaign is an interaction, identified by its public_key. An interaction can contain one or more bookable activities — also called touchpoints. Each booking in the response carries an activity object with its own id. That activity.id is the touchpoint identifier, and it is the key to telling bookings apart when a campaign has more than one bookable activity.
activity.id is unique within a campaign, not across campaigns — the same id can appear under two different campaigns. To identify a touchpoint uniquely, use activity.id together with the campaign's interaction.public_key. The two use cases below show this in practice.
The booking identifier and statuses
Each booking is identified by the booking field — a unique identifier (a UUID). Pass that exact value back to cancel the booking; don't add a prefix or transform it.
| Booking status | Meaning |
|---|---|
CONFIRMED | The booking is confirmed. |
WAITLIST | The contact is on the waitlist for a full slot. |
CANCELLED | The booking was cancelled. |
The API never returns CHECKED_IN, although the filter accepts it. Check-in is recorded on the attendee, not the booking, so a checked-in booking still reads CONFIRMED. A CHECKED_IN filter matches no bookings, and cancelling a checked-in booking returns a 400.
Endpoints
List a contact's bookings
GET /v1/interactions:fetchContactBookingsReturns every booking for one contact, with pagination and optional filters.
Query parameters
| Parameter | Required | Description |
|---|---|---|
external_id | Yes | The contact's external ID in your system. |
public_keys | No | Scope to a single campaign by its public key. Only one value is supported. Cannot be combined with interactions. |
interactions | No | Scope to a single campaign by its resource name (interactions/<uuid>). Only one value is supported. Cannot be combined with public_keys. |
slot_date_range.from | No | Lower bound for the slot start time, RFC 3339 (for example 2024-12-10T12:30:00Z). |
slot_date_range.to | No | Upper bound for the slot start time, RFC 3339. |
booking_status | No | Filter by status. Repeat the parameter to combine values (CONFIRMED, WAITLIST, CANCELLED). |
language | No | Language code used to translate campaign text in the response. |
page_size | No | Results per page. Default 100. |
page_number | No | Page to return, starting at 1. Default 1. |
Omit both public_keys and interactions to return the contact's bookings across all campaigns.
curl -X GET "https://<api-host>/v1/interactions:fetchContactBookings?external_id=9b33d8c1-64bd-4eff-b0e4-d2fb29ecbe83&booking_status=CONFIRMED&page_size=50&page_number=1" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Response
| Field | Type | Description |
|---|---|---|
bookings | array | The contact's bookings for this page. |
page_number | number | The page returned. |
page_size | number | Results per page. |
total_count | number | Total bookings matching the filters. |
When the contact has no bookings, or the external ID matches no contact, the answer is still 200 but has no bookings and no total_count. Treat a missing field as empty or zero. Each booking in bookings contains the fields below. See Field formats and conventions for the exact format of each.
| Field | Type | Description |
|---|---|---|
activity | object | The booked touchpoint, with id (the touchpoint ID), title (localised name) and location_hint (localised location text). |
interaction | object | The campaign, with public_key and timezone (IANA name, for example Europe/London). |
group | object | The organisation the campaign belongs to, with unique_key and display_name. |
slot_starting_at | string | Slot start time, RFC 3339 in UTC (ends in Z). |
slot_duration_in_minutes | integer | Slot length in minutes. Omitted when the slot has no set duration — see below. |
booking_status | string | CONFIRMED, WAITLIST or CANCELLED. |
booked_at | string | Time of the booking's current status (confirmation, waitlist, or cancellation), RFC 3339 in UTC. |
ticket_type | object | The ticket booked, with display_name and has_limited_capacity. |
host | object | The specific host the contact booked with, when the activity offers a choice of hosts (for example a named station or staff member), with display_name. The location is in activity.location_hint. |
booking | string | The booking's unique identifier (a UUID). Pass this exact value to the cancel endpoint. |
{
"bookings": [
{
"activity": {
"id": "b8b2a0e2-1f3c-4a7d-9c52-0c8f2d1a6e44",
"title": "Santa Photo Experience",
"location_hint": "Level 1, near the fountain"
},
"interaction": {
"public_key": "winter-festival",
"timezone": "Europe/London"
},
"group": {
"unique_key": "lindenhall-shopping-centre",
"display_name": "Lindenhall Shopping Centre"
},
"slot_starting_at": "2024-12-10T12:30:00Z",
"slot_duration_in_minutes": 30,
"booking_status": "CONFIRMED",
"booked_at": "2024-12-01T09:15:00Z",
"ticket_type": {
"display_name": "Standard entry",
"has_limited_capacity": true
},
"host": {
"display_name": "Photo Station 2"
},
"booking": "2394d604-afcc-4f7f-98c1-ef3676c20d6b"
}
],
"page_number": 1,
"page_size": 50,
"total_count": 1
}Field formats and conventions
| Field | Format and behaviour |
|---|---|
slot_starting_at | RFC 3339 timestamp in UTC, ending in Z (for example 2024-12-10T12:30:00Z). |
slot_duration_in_minutes | Integer minutes, from the booked slot's configuration (the chosen host's slot length, or the activity's when there's no host choice). Omitted from the response when the slot has no set duration — never defaulted to 60, so type it as optional. |
booked_at | RFC 3339 timestamp in UTC. Holds the time of the current status: confirmation for CONFIRMED, waitlist for WAITLIST, cancellation for CANCELLED. Check-in doesn't change it. |
interaction.timezone | IANA timezone name (for example Europe/London), not an offset. Convert the UTC slot times into it to show the contact their local time. |
activity.title, activity.location_hint | Localised plain strings in the campaign's resolved language. Edge case: when the chosen or default language has no value for that field, you may instead receive a raw language map serialised as a string, for example {"fr":"…"}. Parse defensively — if the value parses as a JSON object, take the entry for your language or the first available one. |
language (query param) | Base language code such as en or fr (region subtags like fr-CA are reduced to fr). Omit it, or pass one the campaign doesn't offer, and the response uses the campaign's default language. |
Pass the slot_date_range.from and slot_date_range.to filters as RFC 3339 in UTC as well.
Cancel a booking
POST /v1/interactions:cancelInteractionBookingCancels one booking and its tickets. Pass the booking value returned by the fetch call, exactly as received.
| Field | Required | Description |
|---|---|---|
booking | Yes | The booking identifier, exactly as returned in the booking field of fetchContactBookings (a UUID, with no prefix). |
curl -X POST "https://<api-host>/v1/interactions:cancelInteractionBooking" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"booking": "2394d604-afcc-4f7f-98c1-ef3676c20d6b"
}'A successful cancellation returns an empty JSON object ({}).
Error responses
Errors come back as JSON with a numeric code and a message:
{
"code": 9,
"message": "Ticket is already canceled"
}| Status | When | What to do |
|---|---|---|
400 | The booking can't be cancelled: its ticket is already cancelled, or its attendee has checked in | Don't retry. Tell the customer the booking can no longer be cancelled. |
401 | The token is missing, expired or malformed, or belongs to another account | Request a new token, then retry once |
429 | Your IP address passed a rate limit | Back off, then retry |
500 with a message about your request | No external_id, more than one public_keys or interactions value, both together, an unknown public key, or an unknown booking ID | Fix the request. Retrying won't help. |
Any other 5xx | A temporary problem on the platform | Retry with backoff |
A call with no token at all also gets a 401, but its body has error and message fields instead: {"error": "Authentication required", ...}.
Use cases
Campaign with a single activity touchpoint
When a campaign has only one bookable activity, every booking in the response shares the same activity.id. You can list the bookings directly without grouping — each row is one slot the contact booked for that activity.
Campaign with multiple activity touchpoints
When a campaign offers several bookable activities (for example a Santa photo and a craft workshop in the same winter campaign), each booking carries the activity.id of the touchpoint it belongs to. Use activity.id to group or label bookings by activity, so the customer sees "Santa Photo Experience" and "Craft Workshop" as separate lines rather than a flat list. The activity.title gives you a ready-made label for each group.
Display bookings on an account page
A typical account-page flow:
- On your backend, call
fetchContactBookingswith the customer'sexternal_idandbooking_status=CONFIRMED. - Page through the results if
total_countis larger than yourpage_size. - Group bookings by campaign (
interaction.public_key) and touchpoint (activity.id), sinceactivity.idrepeats across campaigns. - Show each slot with
slot_starting_at,slot_duration_in_minutes,ticket_type.display_name, andhost.display_name. - Display every time in
interaction.timezone. - Offer a "Cancel" action that asks the customer to confirm.
- Call
cancelInteractionBookingwith thebookingvalue. - Refresh the list so the cancelled slot disappears.
Postman collections
A Postman collection exists for each environment (Staging/UAT and Production) covering all of the endpoints above. Request the collection for your environment from your Customer Success Manager.
Never commit credentials
The collections ship with placeholder credentials only. Use the client_id, client_secret, and audience your Customer Success Manager provisions for you, store them in a secret manager, and keep them out of source control and shared links.