Bookings and trips

Manage the booking lifecycle and organise bookings into trips.

Once you have built a fare (see the per-vertical guides), you create and manage
bookings through a shared set of endpoints, and organise them into trips.
All requests require the consumer HMAC and a traveler bearer
token
.

Throughout, {vertical} is one of flights, stays, trains, or cars.

Idempotency

Every booking mutation and cost-calculation endpoint — create, confirm,
cancel, calculate-costs, and calculate-cancel-costs — requires a unique
Idempotency-Key request header. Reusing the same key for a retried request
returns the original result instead of performing the action twice; omitting the
header returns 422. Generate a fresh key (for example a UUID) per logical
operation and reuse it only when retrying that same operation.

Idempotency-Key: 018f2c3e-1234-7abc-8def-000000000001

Booking lifecycle

Create

POST /v1/{vertical}/bookings creates a booking from a built fareId. See the
per-vertical guides for the request body (flights,
stays, trains,
cars).

Calculate costs

POST /v1/{vertical}/bookings/calculate-costs returns the priced breakdown for
a fare before you commit — including any selected ancillaries, extras, seats,
and payment method.

{ "fareId": "018f...", "locale": "en_us", "isPersonal": false }

Confirm

POST /v1/{vertical}/bookings/{booking_id}/confirm confirms a pending booking.
The request body is optional.

Get and list

Retrieve a single booking:

GET /v1/{vertical}/bookings/{booking_id}

List bookings with filters:

GET /v1/bookings?vertical=flights&status=confirmed&limit=20&offset=0

This always returns the legacy JSON array. For an offset-paginated response,
use the additive page operation, which returns an items envelope with
meta.totalCount and meta.nextOffset:

GET /v1/bookings/page?limit=20&offset=0

meta.totalCount counts rows in Mercury's booking mirror — it is not a live
upstream total. A booking made outside Mercury appears once the mirror
receives it (change feed, or the hourly background sync at the latest).

Supported query parameters include vertical, status (repeatable),
tripId, createdBy, travelers (repeatable), sort (ASC/DESC by
departure), departureDateFrom, departureDateTo, limit, and offset.
Both list operations accept the same filters. createdBy exactly matches
Mercury's locally stored booking owner id (normally the Core booker; when Core
omitted that field, Mercury's deterministic syncing-user fallback).
travelers matches any supplied opaque traveler user id found in the locally
projected passengers, stay guests, or car drivers. Traveler values use OR
semantics; all filter dimensions compose with AND semantics before counting
and pagination. These two filters are local-only and are not forwarded to the
provider; unknown, well-formed ids return an empty result. Traveler membership
is projected only from complete snapshots Mercury receives after this feature
is deployed. Historical rows without a later complete snapshot remain excluded
indefinitely, so traveler-filter items and counts cover only known projections.

A booking's status is one of
pending, confirming, confirmed, issued, cancelled, failed, or
voided.

Itinerary legs on list rows and trip stages

Flight and train items on GET /v1/bookings/page and every booking embedded in
a trip stage carry a legs array. Each leg names itself, says when it arrives,
and reports its own phase, so a client can show a completed outbound and a
remaining return as separate rows (London–Amsterdam round trip, read at
2026-08-21T01:00Z):

"legs": [
  {
    "legId": "1d771894-5f45-5aeb-86a1-77af07ee2c89",
    "journeyUnitIndex": 0,
    "originAirportCode": "LHR",
    "destinationAirportCode": "AMS",
    "departureAt": { "localAt": "2026-08-20T20:55:00", "timezone": "Europe/London", "source": "provider" },
    "arrivalAt":   { "localAt": "2026-08-20T23:05:00", "timezone": "Europe/Amsterdam", "source": "provider" },
    "timelinePhase": "completed"
  },
  {
    "legId": "8a47c297-07c8-5bcf-91d0-dfde9d838934",
    "journeyUnitIndex": 1,
    "originAirportCode": "AMS",
    "destinationAirportCode": "LHR",
    "departureAt": { "localAt": "2026-08-21T07:20:00", "timezone": "Europe/Amsterdam", "source": "provider" },
    "arrivalAt":   { "localAt": "2026-08-21T07:40:00", "timezone": "Europe/London", "source": "provider" },
    "timelinePhase": "imminent"
  }
]
  • legId equals legId on the booking detail leg; use it for leg-scoped
    resources and to open the detail on that leg.
  • legs is null for stays and cars and [] for a flight or train whose
    itinerary has not been mirrored yet; the booking itself still returns.
  • departureAt / arrivalAt are null when the location's zone is unresolved
    and fill in on the next sync (same localAt / timezone / source shape as
    the real-time section below).
  • timelinePhase uses the same ladder as the booking and is derived from the
    leg's clock anchors only, so it does not reflect cancellation; a flight leg
    enters imminent 48h before departure (its check-in window), so it can lead
    the booking's phase by a day.
  • The same leg shape is embedded on every trip-stage booking, so a trip read
    carries per-leg phases without a detail fetch.

When the account has custom checkout fields configured, the single-booking
response carries the stored answers under customFields, joined with the
account's definitions at read time — labels, type (the
GET /v1/custom-fields vocabulary), and definition ordering come from the
definitions; answers themselves are returned as stored:

"customFields": [
  {
    "key": "cost_center",
    "label": "Cost Center",
    "type": "single_select",
    "values": [{ "key": null, "label": "Milan HQ" }]
  }
]

Each value's key is present only when the source can vouch for it
(multi-select answers always; text and date answers as their own value;
single-select only when the stored string matches an option key). The block is
omitted entirely when the feature is disabled, the booking has no answers, or
on create responses and outage fallbacks — re-fetch the booking detail for the
joined block. If the definitions cannot be read, answers still render with the
key doubling as the label and type: null; an answer is never dropped.

Cancel

Preview the refund first:

POST /v1/{vertical}/bookings/{booking_id}/calculate-cancel-costs

Then cancel, echoing the amount you expect to be charged/refunded so the server
can guard against a stale quote:

{ "estimatedTotalAmount": "149.90" }
POST /v1/{vertical}/bookings/{booking_id}/cancel

Trips

A trip groups related bookings (for example an outbound flight, a stay, and
a return flight) into one itinerary the traveler can browse.

Create

POST /v1/trips — supply either purpose (to name a fresh trip) or
bookingId (to seed the trip from an existing booking, deriving the name from
its destination). At least one is required; startsAt / endsAt are optional.

{
  "purpose": "Rome client visit",
  "bookingId": "018f...",
  "startsAt": "2026-02-10T08:00:00Z",
  "endsAt": "2026-02-14T20:00:00Z"
}

The response (TripOut) includes the trip id, status (one of planning,
booked, active, disrupted, completed, cancelled), and its segments.

List, get, and rename

GET   /v1/trips
GET   /v1/trips/{trip_id}
PATCH /v1/trips/{trip_id}

GET /v1/trips streams the traveler's trips as Server-Sent Events by default —
one event: trip frame per trip (each data: payload is a TripOut), closing
with event: done. Consume it like the
notification stream. In environments where
streaming is disabled it instead returns a plain JSON array of TripOut, so
branch on the response Content-Type (text/event-stream vs
application/json) and parse accordingly. GET /v1/trips/{trip_id} always
returns a single TripOut as JSON. Each stage booking embeds the per-leg
legId, arrivalAt, and timelinePhase described under
Itinerary legs on list rows and trip stages.

Associate and detach bookings

Attach an existing booking to a trip (atomically moving it from any current
trip):

POST /v1/trips/{trip_id}/bookings
{ "bookingId": "018f..." }

Detach a booking (the booking row is preserved; only the trip link is removed —
idempotent, returns 204):

DELETE /v1/trips/{trip_id}/bookings/{booking_id}

Timeline

GET /v1/trips/{trip_id}/timeline returns the ordered sequence of trip events
for rendering an itinerary view.

Real-time disruptions

When live-status coverage is available for a carrier, these endpoints surface
delays, cancellations, gate/platform changes, and equipment swaps:

GET /v1/trips/{trip_id}/disruptions
GET /v1/trips/{trip_id}/segments/{segment_id}/disruptions
GET /v1/trips/{trip_id}/segments/{segment_id}/realtime

Live-status coverage varies by carrier and region. Absence of a disruption
does not guarantee an on-time departure — treat these as best-effort signals.

A segment can hold more than one leg (a round trip or a connection). Every
leg-scoped field on these responses — affectedLegId, scheduledDepartureAt,
liveFields, originCode/destinationCode/flightNumber — describes the
affected leg: the one the latest disruption event names, otherwise the next
leg that has not departed yet.

/realtime also returns the affected leg's schedule as local wall-clock time
with its IANA zone:

"departureAt": { "localAt": "2026-09-22T22:00:00", "timezone": "Europe/Madrid", "source": "provider" },
"arrivalAt":   { "localAt": "2026-09-23T00:20:00", "timezone": "Europe/Rome",   "source": "provider" }

Both are null when the segment has no transport leg or the zone is unknown.
The top-level timezone field is deprecated (sunset 2026-11-30): it is an
alias of departureAt.timezone and will be removed — read the zone from
departureAt instead.

For real-time push of new disruption alerts, see the
notification stream.

See the API Reference for the complete request and
response schemas.

Reporting issues

This page is maintained by the BizAway integration team. To request changes or
report an error, contact your BizAway integration contact. Edits made directly
in the ReadMe dashboard are overwritten on the next publish.


Did this page help you?