Booking API

List and cancel a contact's bookings with the customer-facing Booking endpoints.

8 min read

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

IdentifierWhat it isWho 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 statusMeaning
CONFIRMEDThe booking is confirmed.
WAITLISTThe contact is on the waitlist for a full slot.
CANCELLEDThe 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:fetchContactBookings

Returns every booking for one contact, with pagination and optional filters.

Query parameters

ParameterRequiredDescription
external_idYesThe contact's external ID in your system.
public_keysNoScope to a single campaign by its public key. Only one value is supported. Cannot be combined with interactions.
interactionsNoScope to a single campaign by its resource name (interactions/<uuid>). Only one value is supported. Cannot be combined with public_keys.
slot_date_range.fromNoLower bound for the slot start time, RFC 3339 (for example 2024-12-10T12:30:00Z).
slot_date_range.toNoUpper bound for the slot start time, RFC 3339.
booking_statusNoFilter by status. Repeat the parameter to combine values (CONFIRMED, WAITLIST, CANCELLED).
languageNoLanguage code used to translate campaign text in the response.
page_sizeNoResults per page. Default 100.
page_numberNoPage to return, starting at 1. Default 1.

Omit both public_keys and interactions to return the contact's bookings across all campaigns.

Fetch confirmed bookings for one contact
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

FieldTypeDescription
bookingsarrayThe contact's bookings for this page.
page_numbernumberThe page returned.
page_sizenumberResults per page.
total_countnumberTotal 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.

FieldTypeDescription
activityobjectThe booked touchpoint, with id (the touchpoint ID), title (localised name) and location_hint (localised location text).
interactionobjectThe campaign, with public_key and timezone (IANA name, for example Europe/London).
groupobjectThe organisation the campaign belongs to, with unique_key and display_name.
slot_starting_atstringSlot start time, RFC 3339 in UTC (ends in Z).
slot_duration_in_minutesintegerSlot length in minutes. Omitted when the slot has no set duration — see below.
booking_statusstringCONFIRMED, WAITLIST or CANCELLED.
booked_atstringTime of the booking's current status (confirmation, waitlist, or cancellation), RFC 3339 in UTC.
ticket_typeobjectThe ticket booked, with display_name and has_limited_capacity.
hostobjectThe 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.
bookingstringThe booking's unique identifier (a UUID). Pass this exact value to the cancel endpoint.
Example response (one booking)
{
  "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

FieldFormat and behaviour
slot_starting_atRFC 3339 timestamp in UTC, ending in Z (for example 2024-12-10T12:30:00Z).
slot_duration_in_minutesInteger 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_atRFC 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.timezoneIANA 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_hintLocalised 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:cancelInteractionBooking

Cancels one booking and its tickets. Pass the booking value returned by the fetch call, exactly as received.

FieldRequiredDescription
bookingYesThe booking identifier, exactly as returned in the booking field of fetchContactBookings (a UUID, with no prefix).
Cancel a booking
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:

Error body
{
  "code": 9,
  "message": "Ticket is already canceled"
}
StatusWhenWhat to do
400The booking can't be cancelled: its ticket is already cancelled, or its attendee has checked inDon't retry. Tell the customer the booking can no longer be cancelled.
401The token is missing, expired or malformed, or belongs to another accountRequest a new token, then retry once
429Your IP address passed a rate limitBack off, then retry
500 with a message about your requestNo external_id, more than one public_keys or interactions value, both together, an unknown public key, or an unknown booking IDFix the request. Retrying won't help.
Any other 5xxA temporary problem on the platformRetry 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:

  1. On your backend, call fetchContactBookings with the customer's external_id and booking_status=CONFIRMED.
  2. Page through the results if total_count is larger than your page_size.
  3. Group bookings by campaign (interaction.public_key) and touchpoint (activity.id), since activity.id repeats across campaigns.
  4. Show each slot with slot_starting_at, slot_duration_in_minutes, ticket_type.display_name, and host.display_name.
  5. Display every time in interaction.timezone.
  6. Offer a "Cancel" action that asks the customer to confirm.
  7. Call cancelInteractionBooking with the booking value.
  8. 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.

Next steps

On this page