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"
}
]legIdequalslegIdon the booking detail leg; use it for leg-scoped
resources and to open the detail on that leg.legsisnullfor stays and cars and[]for a flight or train whose
itinerary has not been mirrored yet; the booking itself still returns.departureAt/arrivalAtarenullwhen the location's zone is unresolved
and fill in on the next sync (samelocalAt/timezone/sourceshape as
the real-time section below).timelinePhaseuses 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
entersimminent48h 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.
Updated 3 days ago