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" }amountis 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'spriceDifference, for
example, isnewTotalminusoldTotaland is negative when the change
costs less.currencyis 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_currenciesand read
decimalDigits— don't infer it from how anamountstring 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:
| Category | Wire shape | Used for |
|---|---|---|
Instant | ISO-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
amountas a decimal, never a float. - Don't infer a currency's minor-unit count from how many digits
amount
shows — look updecimalDigitsinstead. - Expect negative
amountvalues on fields documented as a difference. - Never apply a timezone conversion to a
CalendarDate— it has none. - Convert a
LocatedTimeto an absolute instant using its owntimezone
field; never assume UTC. - Treat a
LocatedTimewithsource: "default"as not provided by the
provider — it's a documented fallback. - Persist
bookingId,tripId,travelerId, andpropertyId; don't
persistfareIdpast the step that consumes it. - Treat
solutionKeyand similar lineage fields as an opaque string you
forward unchanged — never parse or construct one. - Generate a fresh
Idempotency-Keyper logical operation; reuse it only to
retry that same operation. - Don't assume currency conversion happens automatically — call
convert_currencyexplicitly when you need it.
Related guides
- Searching and booking flights — money and
LocatedTimefields in a real search-to-book flow. - Bookings and trips — the
Idempotency-Key
contract and the booking identifiers you persist. - Managing traveler profiles —
defaultCurrencyand
language preferences. - Errors — the error
envelope used forOFFER_EXPIREDand validation failures.
Updated 8 days ago