Guest checkout
Let a traveler search, hold a fare, and book before they have an account.
Guest checkout lets a traveler search, capture their details, and lock a fare
before they sign in or create an account. A checkout session — not a
traveler bearer token — carries the traveler's identity for the duration of
the flow. Account creation happens only at the very end, through a magic
link, and it hands back an access token that authenticates every subsequent
/v1 request the same way a traveler bearer token does.
The flow at a glance
- Start a session —
POST /v1/checkout/sessions(no credential
required). Returns a signed guest session token. - Search, get fares, and lock a fare — the standard per-vertical flow
(Flights, Stays,
Cars, Trains), sending the guest
session token as the bearer credential instead of a traveler JWT. - Capture traveler details —
PATCH /v1/checkout/sessions/currentwith
passengers, contact info, and consents. - Poll session state —
GET /v1/checkout/sessions/currentto check
progress or recover after a page reload. - Email a magic link —
POST /v1/checkout/sessions/current/magic-link. - Exchange the magic link for an account —
POST /v1/auth/magic-link. This creates the traveler's account and
returns an access token. - Book — with the returned access token, call the vertical's normal
POST /v1/{vertical}/bookingslike any authenticated traveler.
Start a session
curl -X POST "$TRAVELTECH_BASE_URL/v1/checkout/sessions" \
-H "TravelTech-Consumer: $CONSUMER" \
-H "TravelTech-Timestamp: $TIMESTAMP" \
-H "TravelTech-Signature: $SIGNATURE"Consumer HMAC signing still applies (see Authentication);
no bearer token exists yet because there is no traveler identity until this
call mints one.
{
"sessionToken": "eyJhbGciOiJIUzI1NiJ9...",
"session": {
"id": "018f2c3e-1234-7abc-8def-000000000001",
"status": "draft",
"contactEmail": null,
"contactPhone": null,
"passengers": [],
"frequentFlyer": {},
"verticalDetail": null,
"company": null,
"billingContact": null,
"termsAcceptedAt": null,
"privacyAcceptedAt": null,
"lockedSelectionId": null,
"vertical": null,
"bookingId": null,
"selectionHoldExpiresAt": null,
"createdAt": "2026-05-25T10:30:00Z"
}
}Send sessionToken as Authorization: Bearer <sessionToken> on every
subsequent request in this flow — both the /v1/checkout/sessions/* routes
below and the vertical search/fares/build routes in step 2.
Guest, traveler, and consumer-query tokens are not interchangeable
All three credential types are presented the same way — an
Authorization: Bearer <token> header — but each is scoped to a narrow,
non-overlapping route set, and none of them can be substituted for another:
| Token | Identifies | Reachable routes |
|---|---|---|
| Guest session token (this guide) | One checkout session, no Core account yet | /v1/checkout/sessions/*, plus search/fares/fares/build on every vertical (never bookings — see below) |
| Traveler bearer JWT (Authentication) | A signed-in traveler's Core account | Every /v1 route scoped to that traveler, including bookings |
Consumer query-only token (minted via POST /v1/consumers/tokens/query) | A registered API consumer, no traveler at all | A narrower read-only subset of search/fares/destinations/locations — rejected on fares/build, bookings/calculate-costs, and protection-plans |
A guest session token is accepted on search, fares, and fares/build for
every vertical precisely so a traveler can hold a fare before creating an
account — but it is never accepted on POST /v1/{vertical}/bookings.
Booking requires the traveler bearer token the magic-link exchange (below)
returns. Presenting the wrong token family on a route fails the same way an
unknown or forged credential does: the guest, traveler, and consumer-query
verifiers each check a private typ claim before anything else, so a
token from the wrong family is rejected outright rather than silently
downgraded to a narrower scope.
Capture traveler details
PATCH /v1/checkout/sessions/current applies a partial update — send only
the fields you have. Call it as many times as you need while the traveler
fills in a multi-step form.
curl -X PATCH "$TRAVELTECH_BASE_URL/v1/checkout/sessions/current" \
-H "Authorization: Bearer $GUEST_SESSION_TOKEN" \
-H "TravelTech-Consumer: $CONSUMER" \
-H "TravelTech-Timestamp: $TIMESTAMP" \
-H "TravelTech-Signature: $SIGNATURE" \
-H "Content-Type: application/json" \
-d '{
"contactEmail": "[email protected]",
"contactPhone": "+15551234567",
"passengers": [
{
"firstName": "Jane",
"lastName": "Doe",
"dateOfBirth": "1990-04-12",
"isHolder": true
}
],
"consents": {
"termsAccepted": true,
"privacyAccepted": true
}
}'| Field | Notes |
|---|---|
contactEmail, contactPhone | Part of the session shape (null until set). Required before a magic link can be sent. |
passengers | Up to 20 entries. Shared shape across all four verticals; mark the lead traveler with isHolder: true. issuingCountry is the travel document's issuing country, optional, as an uppercase ISO 3166-1 alpha-2 code (IE, not ie or IRL). It is distinct from nationality. travel.tech does not trim or uppercase it; a value that is not exactly two uppercase letters returns 422 with the field passengers.<index>.issuingCountry. Collect it with a country selector rather than free text. |
frequentFlyer | Optional, flight-specific. |
verticalDetail | Optional per-vertical slice — car driver licence or train rail cards. Must match whichever vertical the session has locked a fare for; a mismatch returns 422. |
company, billingContact | Optional invoicing details. An Italian company must also give company.sdiCode or company.pec — see Italian e-invoicing below. |
consents | Both termsAccepted and privacyAccepted must be true before a magic link can be sent. |
A 409 means the session has moved past the details-capture phase (see
Session states below) and no longer accepts updates. A
422 on a signup email means the address is malformed or ineligible for
account creation — the response's details array names the offending field.
Italian e-invoicing
Italy routes invoices through the Sistema di Interscambio, so a company
registered there is identified by a recipient code or a certified mailbox
rather than by a plain email address. A billing address for an Italian
business or freelance company is rejected unless it carries one of them, and
a company with a VAT number is always treated as a business.
Collect whichever the company uses and send it on the session's company
object:
| Field | Rule |
|---|---|
sdiCode | The recipient code. Trimmed and uppercased, then required to be exactly seven letters or digits. |
pec | A certified email address (Posta Elettronica Certificata), accepted in place of sdiCode. Trimmed, then required to be a valid email address of at most 254 characters. |
Either one satisfies Italy's requirement; sending both is fine. A value that
fails its rule returns 422 with the field named as company.sdiCode or
company.pec and the code INVALID_FORMAT; the response never echoes the
value you sent.
curl -X PATCH "$TRAVELTECH_BASE_URL/v1/checkout/sessions/current" \
-H "Authorization: Bearer $SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"company": {
"name": "Analytical Engines Srl",
"vatNumber": "IT12345678901",
"country": "IT",
"sdiCode": "ABC1234"
}
}'Both fields follow the same PATCH rules as the rest of company: omitting
one (or sending null) keeps whatever is stored, and sending "" clears it.
GET /v1/checkout/sessions/current returns both, so a traveler who reloads
mid-checkout does not have to type the code again. Copy them onto the billing
address you create for the company after checkout.
Correct details after the account is created
A traveler who confirmed their email and reached Payment can still go back and
fix what they entered as a guest, before the booking exists. Call the same
PATCH /v1/checkout/sessions/current, but authenticate with the traveler's
account bearer token and the consumer signature headers instead of the guest
session token.
curl -X PATCH "$TRAVELTECH_BASE_URL/v1/checkout/sessions/current" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "TravelTech-Consumer: $CONSUMER" \
-H "TravelTech-Timestamp: $TIMESTAMP" \
-H "TravelTech-Signature: $SIGNATURE" \
-H "Content-Type: application/json" \
-d '{ "contactPhone": "+15551234567" }'- The session is resolved from the signed-in traveler, so another traveler on
the same account cannot read or change it. - Only
passengers,contactPhone,frequentFlyer,companyand
billingContactare accepted.contactEmailis now the account identity and
consentswere already recorded, so sending either (orverticalDetail)
returns422. - The session stays
account_created; the held fare and its hold expiry do
not change. - Once the session has a
bookingId(or any status other than
account_created) the call returns409with the stable error code
CHECKOUT_DETAILS_LOCKED. Resume at confirm instead of editing.
Poll the current session
GET /v1/checkout/sessions/current returns the same session shape as create
and update. Use it to recover state after a page reload, or to check whether
a booking created asynchronously by the magic-link exchange has settled:
curl "$TRAVELTECH_BASE_URL/v1/checkout/sessions/current" \
-H "Authorization: Bearer $GUEST_SESSION_TOKEN" \
-H "TravelTech-Consumer: $CONSUMER" \
-H "TravelTech-Timestamp: $TIMESTAMP" \
-H "TravelTech-Signature: $SIGNATURE"This route also accepts the traveler bearer token returned by the
magic-link exchange, so the same poll works before and after account
creation without the client needing to track which credential it is
currently holding.
Session states
| Status | Meaning |
|---|---|
draft | Session created; no details captured yet. |
details_captured | Contact info and consents recorded. |
link_sent | A magic link has been emailed and not yet exchanged. |
account_created | The magic link was exchanged; the traveler now has a Core account. |
booked | A booking was created against this session's held selection. |
expired | The session sat idle too long and was retired. Start a new one. |
Send a magic link
POST /v1/checkout/sessions/current/magic-link requires a contact email,
both consents accepted, and a locked selection on the session (from step 2
of the flow). Resending invalidates every previously emailed link for this
session, so only the most recently sent link is ever valid.
The link always goes to the session's contactEmail, and that address is
locked once the first link has been sent: the address the first link reaches
is the identity the account is created for, and nothing proves its owner agreed
to a replacement. Changing contactEmail on a link_sent session returns
409 (re-sending the stored address is accepted). Correct a mistyped address
before the first send; after it, start a new checkout session.
The link expires when the selection hold does: magicLinkExpiresAt equals
selectionHoldExpiresAt, and the email tells the traveler that time. If the
hold is renewed after the email is sent, the link still expires at the
original deadline; resend to get a link that lasts the renewed hold.
curl -X POST "$TRAVELTECH_BASE_URL/v1/checkout/sessions/current/magic-link" \
-H "Authorization: Bearer $GUEST_SESSION_TOKEN" \
-H "TravelTech-Consumer: $CONSUMER" \
-H "TravelTech-Timestamp: $TIMESTAMP" \
-H "TravelTech-Signature: $SIGNATURE"{
"magicLinkExpiresAt": "2026-05-25T10:45:00Z",
"selectionHoldExpiresAt": "2026-05-25T10:45:00Z"
}| Status | Condition |
|---|---|
409 | Session no longer accepts a magic-link send (already past this phase). |
422 | Missing a required detail (contact email, consents, or a locked selection) — details names each missing field. |
429 | Rate limit exceeded, per session and per recipient email. |
502 | The email provider rejected delivery (BAD_GATEWAY). The session stays link_sent with a valid token and its selection hold unchanged; resend to recover. |
503 | The send cannot run on this deployment. The body error.code says why — see below. Nothing is emailed and the session, including its selection hold, is unchanged. |
A 503 carries one of these codes:
error.code | Meaning | Client action |
|---|---|---|
EMAIL_DELIVERY_NOT_CONFIGURED | The deployment has no email sender address or magic-link base URL. | Do not retry; recoveryAction is CONTACT_SUPPORT. |
DATABASE_UNAVAILABLE | The database is not available. | Retry later (RETRY_LATER). |
RATE_LIMITER_UNAVAILABLE | The limiter that guards outbound email is not available, so the send fails closed. | Retry later (RETRY_LATER). |
Exchange the magic link
The traveler clicks the emailed link, which carries a one-time token.
Exchange it at POST /v1/auth/magic-link — this route needs no guest
session token; the one-time token itself is the credential:
curl -X POST "$TRAVELTECH_BASE_URL/v1/auth/magic-link" \
-H "TravelTech-Consumer: $CONSUMER" \
-H "TravelTech-Timestamp: $TIMESTAMP" \
-H "TravelTech-Signature: $SIGNATURE" \
-H "Content-Type: application/json" \
-d '{"token": "<one-time-token-from-the-emailed-link>"}'{
"accessToken": "<account-session-token>",
"session": {
"status": "account_created",
"selectionHoldExpiresAt": "2026-05-25T10:45:00Z",
"selectionExpired": false,
"vertical": "flight"
},
"bizawayToken": "<bizaway-session-token>"
}bizawayToken is the traveler's BizAway session token for the card form at
payment. Treat it as a credential: never log or store it.
session.vertical is the travel vertical that owns the held selection
(flight, stay, train or car), so a client opening the link on a
different device knows which checkout to resume. It is null when nothing is
held; null never means a default vertical.
accessToken authenticates every subsequent /v1 request the same way a
traveler bearer JWT does — same Authorization: Bearer header, same
routes, including the booking call in step 7. But it is a separate
travel.tech-issued credential (not the BizAway-issued JWT
Authentication describes), with its own ~1 hour
lifetime and no refresh endpoint: once it expires, the traveler needs a
fresh magic link to sign in again. Re-posting the same magic-link token
after a successful exchange returns the already-created signed-in state
again rather than creating a second account, so a client that retries a
timed-out request is safe.
If the held selection lapsed or could not be transferred to the new account
during the exchange, the account is still created (selectionExpired: true) — the
traveler is signed in, but must re-search and re-lock a fare before
booking.
A 410 means the link is invalid, expired, already used, or that the
signup email could not be claimed for the new account — travel.tech does not
distinguish these causes in the response so a client cannot use this
endpoint to probe which one applies. Send the traveler back through the
flow to request a fresh link.
A 503 with error.code BIZAWAY_SESSION_UNAVAILABLE means the account was
created but the BizAway session token could not be issued, so the traveler is
not signed in and no accessToken is returned. Re-post the same magic-link
token to retry (recoveryAction is RETRY_LATER): it does not create a second
account, and it works until the link expires.
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 7 days ago