Booking API
List and cancel a contact's bookings with the customer-facing OmniLab Booking endpoints.
The Booking API lets your backend read and manage the bookings a contact has made on an OmniLab campaign — for example to power an account page that shows a customer's upcoming bookings and lets them cancel.
All endpoints are server-to-server: request an access token on your own backend, then call OmniLab with a bearer token. Never expose the client secret in a browser, kiosk page, or mobile app bundle.
Credentials come from your Customer Success Manager
The client_id, client_secret, audience, and the exact API host for each environment are not self-service. Request them from your OmniLab Customer Success Manager and confirm which endpoints are enabled for your tenant.
Base URLs and environments
Every call uses the /v1/ base path on a tenant-specific host. The host follows this pattern, where <tenant> is your tenant key and <env> is the environment:
| Environment | API host pattern |
|---|---|
| Development | https://<tenant>.api.omnilab-dev.21-digital.com |
| Staging (UAT) | https://<tenant>.api.omnilab-uat.21-digital.com |
| Production | https://<tenant>.api.omnilab-prod.21-digital.com |
Keep staging and production credentials separate, and build and test against staging first. See Base URLs and environments for the wider rules.
Authenticate
Get a token with the client-credentials flow, then send it as Authorization: Bearer <token> on every Booking API call.
curl -X POST "https://<api-host>/v1/oauth:token" \
-H "Content-Type: application/json" \
-d '{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"audience": "YOUR_AUDIENCE",
"grant_type": "client_credentials"
}'{
"access_token": "YOUR_ACCESS_TOKEN",
"token_type": "Bearer",
"expires_in": 3600
}Treat expires_in as the token lifetime in seconds: cache the token on your backend and refresh it before it expires, or after a 401 response. The full token guidance lives in Authentication.
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>) | OmniLab's internal identifier for the same person. OmniLab resolves it from your external ID for you. | OmniLab internally. 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 in OmniLab 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. |
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 50. |
page_number | No | Page to return, starting at 1. Default 1. |
Filter by one campaign at a time
You can narrow results to a single campaign with either public_keys or interactions, but not both, and only one value each. Omit both 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. |
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 (localized name), and location_hint (localized location text). |
interaction | object | The campaign, with public_key and timezone (IANA name, for example Europe/London). |
group | object | The collection 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": "example-brand",
"display_name": "Example Brand"
},
"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 behavior |
|---|---|
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. |
booking_status | One of CONFIRMED, WAITLIST, or CANCELLED. |
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 | Localized 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 serialized 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 OmniLab returns 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": "82ce03a0-939e-423f-811d-0345eacdc137"
}'A successful cancellation returns an empty JSON object ({}).
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 simply 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:
Fetch the contact's confirmed bookings
On your backend, call fetchContactBookings with the customer's external_id and booking_status=CONFIRMED. Page through the results if total_count is larger than your page_size.
Group and render the list
Group bookings by campaign (interaction.public_key) and touchpoint (activity.id) — remember activity.id repeats across campaigns. Show each slot using slot_starting_at, slot_duration_in_minutes, ticket_type.display_name, and host.display_name, and display every time in interaction.timezone.
Offer "Cancel"
Ask the customer to confirm, call cancelInteractionBooking with the booking value, then refresh the list so the cancelled slot disappears.
Postman collections
OmniLab maintains a Postman collection per environment (Development, 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.
Good practices
- Run the token exchange on your backend and call the Booking API server-to-server.
- Cache the access token and refresh it before
expires_inelapses or after a401. - Send date filters (
slot_date_range.from/slot_date_range.to) in UTC. - Display slot times in
interaction.timezone, which is an IANA timezone name. - Confirm with the customer before cancelling, and refresh the list afterwards.
- Handle an empty
bookingsarray gracefully — a contact may have no bookings.
Related
Authentication
Request and reuse the machine-to-machine access token.
Base URLs and environments
Keep staging and production hosts and credentials separated.
Common integration patterns
See how the booking flow fits with CRM and analytics patterns.
Access bookings and tickets
Manage the same bookings from the OmniLab Studio.