Money, dates, and identifiers

The money shape, the three datetime categories, and identifier formats used consistently across every endpoint.

What this covers

Amounts, timestamps, and identifiers are the three things an integration
tends to get wrong first — not because the API is inconsistent, but because
the conventions aren't obvious until you've been bitten once. This page
defines the shared money shape, the three categories every datetime field
falls into, how identifiers behave, and how locale and currency selection
work, so you can parse and render every field correctly without guessing.

How it works

Money

Every monetary field on the wire — totalPrice, price, nightly,
priceDifference, and the rest — uses the same two-field shape:

{ "amount": "320.00", "currency": "EUR" }
  • amount is a decimal serialized as a JSON string, not a number —
    parse it with your language's decimal type so you never lose precision to
    float rounding. It always carries at least two digits after the decimal
    point, and more when the underlying value needs them (for example
    "199.999"), but that padding is not currency-aware: a zero-decimal
    currency amount still renders with two decimals on this field. Some fields
    can also be negative — a train fare-change quote's priceDifference, for
    example, is newTotal minus oldTotal and is negative when the change
    costs less.
  • currency is a three-letter uppercase ISO 4217 code ("EUR",
    "USD", …).
  • The amount comes back in whatever currency the fare was actually priced
    in — the API does not convert it for you. See "Locale and currency" below
    for how to request a conversion.
  • To know how many decimal places a given currency actually uses for
    display (most use 2; "JPY" uses 0; a few use 3), call
    list_display_currencies and read
    decimalDigits — don't infer it from how an amount string looks.

The three datetime categories

Every datetime field falls into exactly one of three categories, published
in full under the info.x-datetime-conventions extension in the OpenAPI
reference so it never drifts from the server:

CategoryWire shapeUsed for
InstantISO-8601 with a Z offset, e.g. "2026-02-10T09:00:00Z"Absolute, location-independent moments: audit timestamps like createdAt, updatedAt, and cancellation deadlines.
LocatedTime{ "localAt": "2026-02-10T09:15:00", "timezone": "Europe/Rome", "source": "provider" }Wall-clock time at a physical place: flight/train departure and arrival, car pickup/dropoff, and a stay's check-in/check-out while you're still searching or pricing it.
CalendarDate"2026-02-10"A date with no time or timezone at all: date of birth, document expiry, and a stay's check-in/check-out once the booking is confirmed.

A LocatedTime's localAt has no UTC offset — it's the wall clock at
the location named by timezone. Convert it to an absolute instant yourself
using that timezone; never assume it's UTC. source tells you where the
value came from: "provider" means the provider supplied it; "default"
means travel.tech filled it in from a documented fallback because the
provider didn't — for example, a stay's search result defaults to a 15:00
local check-in and an 11:00 local check-out when the property doesn't
return a specific time.

This is also where the category can change across the booking lifecycle for
the same real-world concept: a stay's check-in and check-out are a
LocatedTime while you're searching or pricing it (the catalog's best
guess at a time), but once the stay is booked, the confirmed booking's
checkIn/checkOut fields are a CalendarDate — the property's own
arrival time isn't guaranteed at that point, only the date is. Never apply
a timezone conversion to a CalendarDate value; it doesn't have one.

If a field isn't bound to a physical location and has no natural
association with a local time at all, it's an Instant in UTC — that's the
tiebreaker the server itself applies.

Identifiers

Resource identifiers the API mints for you — booking IDs, trip IDs,
property IDs, traveler IDs, document IDs, and fare IDs — are UUIDs. Booking,
trip, traveler, and property IDs are stable for the life of the
resource
: persist them and use the same value in every later request that
references that resource.

fareId is also a UUID, but it isn't long-lived the same way: fare locks
expire, so a build call can return 410 with code: "OFFER_EXPIRED" if you
wait too long to book (see
Searching and booking flights for the full flow).
Don't persist a fareId past the booking step that consumes it.

Not every identifier-shaped field is a UUID, though. solutionKey — the
provider-lineage token search results and fares carry — is an opaque string
(for example "sk_ba117_20270315"), not a UUID. Treat it, and fields like
it, as a pass-through value: forward it exactly as you received it, and
never try to parse or construct one yourself. It's also nullable; a fare
built without one simply omits it.

Separately, every booking and cost-calculation mutation requires an
Idempotency-Key request header that you generate, not the server.
Create a fresh one per logical operation and reuse it only when retrying
that same operation — see
Bookings and trips for the full contract.

Locale and currency

GET /v1/currencies (list_display_currencies)
lists the currencies supported for display, each with its decimalDigits.
A traveler's defaultCurrency on their profile (see
Managing traveler profiles) records a display
preference — it does not change what currency search and fare prices
come back in; those are always denominated in the currency the fare itself
was priced in.

To convert an amount you already have, call
GET /v1/currencies/convert (convert_currency)
with amount, from_currency, and to_currency, or
GET /v1/currencies/rates (get_currency_rate)
for just the multiplier. Conversion is never applied implicitly.

Send an Accept-Language header if you want locale-aware behavior: the API
echoes the resolved locale back in a Content-Language response header and
uses it to format the handful of fields that are locale-aware, such as the
display block on convert_currency's response. Locale has no effect on
pricing or on which currency a response uses.

What you need to handle

  • Parse amount as a decimal, never a float.
  • Don't infer a currency's minor-unit count from how many digits amount
    shows — look up decimalDigits instead.
  • Expect negative amount values on fields documented as a difference.
  • Never apply a timezone conversion to a CalendarDate — it has none.
  • Convert a LocatedTime to an absolute instant using its own timezone
    field; never assume UTC.
  • Treat a LocatedTime with source: "default" as not provided by the
    provider — it's a documented fallback.
  • Persist bookingId, tripId, travelerId, and propertyId; don't
    persist fareId past the step that consumes it.
  • Treat solutionKey and similar lineage fields as an opaque string you
    forward unchanged — never parse or construct one.
  • Generate a fresh Idempotency-Key per logical operation; reuse it only to
    retry that same operation.
  • Don't assume currency conversion happens automatically — call
    convert_currency explicitly when you need it.

Related guides


Did this page help you?