Searching and booking stays

Look up destinations, search stays, pick a fare, and create a stay booking.

Stays follow the standard four-step flow: search → fares → build → book.
Search and fares stream results as Server-Sent Events (SSE); build and book
return JSON. Every request below carries the full consumer-HMAC and
traveler-bearer headers — see Authentication for how
$SIGNATURE is computed from $METHOD, the request path, $TIMESTAMP, and
the body.

(Stays is the platform's canonical name for lodging — hotels, apartments, and
other accommodation.)

1. Look up destinations

Resolve free-text input to destination and property identifiers.

curl -G "$TRAVELTECH_BASE_URL/v1/stays/destinations" \
  --data-urlencode "q=rome" \
  --data-urlencode "limit=5" \
  -H "TravelTech-Consumer: $CONSUMER" \
  -H "TravelTech-Timestamp: $TIMESTAMP" \
  -H "TravelTech-Signature: $SIGNATURE" \
  -H "Authorization: Bearer $TRAVELER_JWT"

(Sign with METHOD=GET, PATH=/v1/stays/destinations, and the empty-body hash
from Authentication.)

Queries shorter than two characters return an empty result. See the
API Reference for the response shape.

2. Search

POST /v1/stays/search streams stay solutions for a destination and date range
as SSE. If the requested stay exceeds the configured maximum nights, the stream
terminates with event: error and code MAX_NIGHTS_EXCEEDED; other provider
failures use PROVIDER_ERROR.

curl -N -X POST "$TRAVELTECH_BASE_URL/v1/stays/search" \
  -H "TravelTech-Consumer: $CONSUMER" \
  -H "TravelTech-Timestamp: $TIMESTAMP" \
  -H "TravelTech-Signature: $SIGNATURE" \
  -H "Authorization: Bearer $TRAVELER_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "destination": "rome-city-id",
    "checkIn": "2026-02-10",
    "checkOut": "2026-02-12",
    "rooms": [{ "adults": 2, "childrenAges": [] }],
    "passengers": { "adults": 2 },
    "nationality": "IT"
  }'

Request body (StaySearchRequest) — provide either a destination or a
specific property:

{
  "destination": "rome-city-id",
  "checkIn": "2026-02-10",
  "checkOut": "2026-02-12",
  "rooms": [{ "adults": 2, "childrenAges": [] }],
  "passengers": { "adults": 2 },
  "nationality": "IT"
}

A full trace of the response stream — two solutions, then done:

event: solution
data: {"id":"018f2c3e-1234-7abc-8def-000000000111","propertyId":"018f2c3e-1234-7abc-8def-000000000112","name":"Hotel Roma Centrale","city":"Rome","countryCode":"IT","rating":4,"totalPrice":{"amount":"320.00","currency":"EUR"},"isPolicyCompliant":true}

event: solution
data: {"id":"018f2c3e-1234-7abc-8def-000000000113","propertyId":"018f2c3e-1234-7abc-8def-000000000114","name":"Residenza Trastevere","city":"Rome","countryCode":"IT","rating":3,"totalPrice":{"amount":"215.00","currency":"EUR"},"isPolicyCompliant":true}

event: done
data: {}

Each streamed solution's id is the searchSolutionId for the next step.

When the requested stay exceeds the configured maximum nights, the stream ends
with a terminal event: error frame instead of done:

event: error
data: {"code":"MAX_NIGHTS_EXCEEDED","message":"The requested stay exceeds the maximum number of nights."}

catalog-item frames (flag-gated, additive)

When catalog-item streaming is enabled for your account, the stream begins
with a burst of event: catalog-item frames a few seconds before the first
priced event: solution frames: one frame per candidate hotel (relevance
order — negotiated tier first, then the curated catalog priority rank, with
static tiebreakers: review score, star rating, image presence, distance —
capped), carrying the static property subset — same field names and
casing as the solution shape, minus id and every price/fare-derived field:

event: catalog-item
data: {"name":"Hotel Roma Centrale","city":"Rome","countryCode":"IT","checkIn":{"localAt":"2026-02-10T14:00:00","timezone":"Europe/Rome","source":"provider"},"checkOut":{"localAt":"2026-02-12T11:00:00","timezone":"Europe/Rome","source":"provider"},"rating":4,"images":["https://cdn.example.com/hotels/roma-centrale/1.jpg"],"amenities":["wifi","parking"],"propertyId":"018f2c3e-1234-7abc-8def-000000000112","searchSolutionId":"018f2c3e-1234-7abc-8def-000000000111","distanceFromSearchMeters":420}

event: catalog-item
data: {"name":"Residenza Trastevere","city":"Rome","countryCode":"IT","checkIn":{"localAt":"2026-02-10T14:00:00","timezone":"Europe/Rome","source":"provider"},"checkOut":{"localAt":"2026-02-12T11:00:00","timezone":"Europe/Rome","source":"provider"},"rating":3,"images":["https://cdn.example.com/hotels/residenza-trastevere/1.jpg"],"amenities":[],"propertyId":"018f2c3e-1234-7abc-8def-000000000114","searchSolutionId":"018f2c3e-1234-7abc-8def-000000000113","distanceFromSearchMeters":1150}
  • Join key: the later solution frame for the same hotel carries the
    same propertyId — treat catalog-item as insert-if-absent and
    solution as upsert-by-propertyId to hydrate prices in place.
  • Tappable immediately: a catalog-item's searchSolutionId +
    propertyId are valid for POST /v1/stays/fares right away, before any
    price has streamed.
  • Amenities: amenities is always present (empty when none are
    known) and carries the same canonical keys as the solution frame, so
    amenity filters apply to unpriced cards too.
  • Photos: images carries at most two URLs to keep the unpriced burst
    small. For the full gallery, call
    GET /v1/stays/properties/{propertyId}.
  • Additive: clients that do not recognise the event type can ignore it;
    solution frames are unchanged. Some hotels may never receive a price
    during the search — their cards simply never hydrate.
  • Ranked catalog accounts: where the burst is served from the
    travel.tech stay catalog, every catalog-item also carries two
    booleans — isPreferred and isRecommended — and the
    order is preferred, then recommended, then negotiated, then catalog
    priority, then the same static tiebreakers as above: review score
    (unscored last), star rating, image presence, distance. On these
    streams searchSolutionId is set from the very first
    frame and is the same value on every catalog-item and solution frame
    of the stream. The two booleans are absent on other streams.
    catalog-item frames never carry isNegotiatedRate; it is true only
    on a hotel's solution frame, once a negotiated rate is priced.

Live pricing progress frames (flag-gated, additive)

On ranked catalog accounts with live pricing enabled, prices arrive as the
rate sources answer instead of at the end of a silent wait. The solution
frames are unchanged, but a property is sent again, whole, whenever its best
price changes — keep upserting by propertyId. Three counts-only frames
report progress; they never name a source:

event: provider-status
data: {"searchSolutionId":"018f2c3e-1234-7abc-8def-000000000111","sourcesTotal":9,"sourcesRunning":6,"sourcesAnswered":3,"sourcesFailed":0,"pricedProperties":42}

event: settled
data: {"searchSolutionId":"018f2c3e-1234-7abc-8def-000000000111","sourcesTotal":9,"sourcesRunning":2,"sourcesAnswered":7,"sourcesFailed":0,"pricedProperties":187}

event: complete
data: {"searchSolutionId":"018f2c3e-1234-7abc-8def-000000000111","sourcesTotal":9,"sourcesRunning":0,"sourcesAnswered":8,"sourcesFailed":1,"pricedProperties":203}

event: done
data: {}
  • provider-status is sent whenever the counts change.
  • settled is sent at most once, when enough sources have answered for
    the list to be worth sorting; later prices can still arrive. A search whose
    sources all finish early skips it.
  • complete is sent once, when no more prices will arrive, and is always
    followed by done. A search can complete with no priced properties; the
    cards stay unpriced.
  • Failure: when live pricing fails at any point — including after
    settled — the stream ends with a single event: error frame and no
    complete or done. Keep the cards and prices already shown; they remain
    valid for POST /v1/stays/fares.
  • Additive: clients that do not recognise these events can ignore them.

3. List fares

POST /v1/stays/fares streams the bookable room fares for a chosen solution.

curl -N -X POST "$TRAVELTECH_BASE_URL/v1/stays/fares" \
  -H "TravelTech-Consumer: $CONSUMER" \
  -H "TravelTech-Timestamp: $TIMESTAMP" \
  -H "TravelTech-Signature: $SIGNATURE" \
  -H "Authorization: Bearer $TRAVELER_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "searchSolutionId": "018f2c3e-1234-7abc-8def-000000000111",
    "checkIn": "2026-02-10",
    "checkOut": "2026-02-12",
    "rooms": [{ "adults": 2, "childrenAges": [] }],
    "isPersonal": false
  }'

Request body (StayFaresBody):

{
  "searchSolutionId": "018f...",
  "checkIn": "2026-02-10",
  "checkOut": "2026-02-12",
  "rooms": [{ "adults": 2, "childrenAges": [] }],
  "isPersonal": false
}

External fares are withheld by default. Pay-at-property (external) fares
are not surfaced unless your environment enables them. Fares returned here are
prepaid through BizAway.

4. Build the fare

POST /v1/stays/fares/build locks a specific fare and returns it as JSON
(201).

curl -X POST "$TRAVELTECH_BASE_URL/v1/stays/fares/build" \
  -H "TravelTech-Consumer: $CONSUMER" \
  -H "TravelTech-Timestamp: $TIMESTAMP" \
  -H "TravelTech-Signature: $SIGNATURE" \
  -H "Authorization: Bearer $TRAVELER_JWT" \
  -H "Content-Type: application/json" \
  -d '{ "fareId": "018f2c3e-1234-7abc-8def-000000000211", "searchSolutionId": "018f2c3e-1234-7abc-8def-000000000111", "isPersonal": false }'

Request body (StayBuildFareBody):

{ "fareId": "018f...", "searchSolutionId": "018f...", "isPersonal": false }

Response (StayFareOut):

{
  "fareId": "018f2c3e-1234-7abc-8def-000000000211",
  "solutionKey": "9a12c4e0...",
  "totalPrice": { "amount": "320.00", "currency": "EUR" },
  "pricePerNight": { "amount": "160.00", "currency": "EUR" },
  "numNights": 2,
  "boardBasis": "breakfast",
  "cancellationPolicy": "free_until_2026-02-08",
  "isRefundable": true,
  "isNegotiatedRate": false,
  "isBizawayRate": true,
  "merchant": "bizaway",
  "conditions": ["Free cancellation until 2026-02-08T23:59:00"],
  "bookingRequirements": {
    "travelerFields": [],
    "document": { "required": false, "acceptedTypes": null },
    "contact": { "email": false, "phone": false },
    "constraints": [],
    "source": "baseline"
  }
}

See the API Reference for the full fare shape.

If the fare is no longer available (fare locks are time-limited), the build
call returns 410 with the standard
error envelope:

{
  "error": {
    "code": "OFFER_EXPIRED",
    "message": "This fare is no longer available. Search again to get a current price.",
    "details": [],
    "recoveryAction": "CHANGE_SELECTION",
    "retryAfterSeconds": null
  }
}

5. (Optional) Protection plans

GET /v1/stays/fares/{fare_id}/protection-plans

6. Book

POST /v1/stays/bookings creates the booking from the built fareId. Send a
unique Idempotency-Key header (required — see
Bookings and trips).

curl -X POST "$TRAVELTECH_BASE_URL/v1/stays/bookings" \
  -H "TravelTech-Consumer: $CONSUMER" \
  -H "TravelTech-Timestamp: $TIMESTAMP" \
  -H "TravelTech-Signature: $SIGNATURE" \
  -H "Authorization: Bearer $TRAVELER_JWT" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 018f2c3e-1234-7abc-8def-000000000411" \
  -d '{
    "fareId": "018f2c3e-1234-7abc-8def-000000000211",
    "passengers": [
      { "firstName": "Ada", "lastName": "Lovelace", "type": "adult", "isHolder": true }
    ],
    "isPersonal": false,
    "tripId": null
  }'

Request body (BookStayRequest):

{
  "fareId": "018f...",
  "passengers": [
    {
      "firstName": "Ada",
      "lastName": "Lovelace",
      "type": "adult",
      "isHolder": true
    }
  ],
  "isPersonal": false,
  "tripId": null
}

For stays, the booking holder's phone number is taken from the authenticated
traveler's profile.

When the rate is out of travel policy, send the traveler's reason as the
optional policyJustification (at most 2000 characters; blank counts as
missing). travel.tech stores it on the booking and returns it as
policyJustification on the booking detail.

Response (BookingDetailOut, 201):

{
  "bookingId": "018f2c3e-1234-7abc-8def-000000000411",
  "vertical": "stay",
  "status": "pending",
  "providerName": null,
  "needsApproval": false,
  "totalPrice": { "amount": "320.00", "currency": "EUR" },
  "availableOperations": ["cancel_booking"],
  "providerData": {},
  "legs": [],
  "stayDetail": {
    "propertyName": "Hotel Roma Centrale",
    "checkIn": "2026-02-10",
    "checkOut": "2026-02-12",
    "nights": 2,
    "starRating": 4,
    "roomTypeName": "Superior Double Room",
    "boardType": "Breakfast included"
  },
  "travelers": [
    {
      "travelerId": "018f2c3e-1234-7abc-8def-000000000611",
      "displayName": "Ada Lovelace",
      "legAssignments": []
    }
  ],
  "bizAwayCode": "BA-445218",
  "providerConfirmationReferences": [],
  "tripId": null
}

Manage the booking lifecycle with the shared endpoints in
Bookings and trips. Stay bookings don't carry the
disruption block — that's populated for flight and rail bookings only — but
other booking-level events still push a notification to the traveler; see
Notification severity tiers for how to
interpret and render the severity field on those notifications.

Some fares require a birthday on every guest, children included — if one is
missing, the booking call returns 422 before any provider call is made,
naming the specific guest:

{
  "error": {
    "code": "GUEST_BIRTHDAY_REQUIRED",
    "message": "Guest 'Ada Lovelace' is missing a required birthday.",
    "details": [
      {
        "field": "passengers",
        "code": "GUEST_BIRTHDAY_REQUIRED",
        "message": "Guest 'Ada Lovelace' is missing a required birthday."
      }
    ],
    "recoveryAction": "FIX_INPUT",
    "retryAfterSeconds": null
  }
}

Multi-room bookings

If you searched and priced a fare across two or more rooms (step 2's
rooms array had more than one entry), send rooms on the booking too so
travel.tech knows which guest sleeps in which room. Each entry lists the
zero-based indices of passengers assigned to that room:

{
  "fareId": "018f...",
  "passengers": [
    { "firstName": "Ada", "lastName": "Lovelace", "type": "adult", "isHolder": true },
    { "firstName": "Grace", "lastName": "Hopper", "type": "adult", "isHolder": false }
  ],
  "rooms": [{ "passengerIndices": [0] }, { "passengerIndices": [1] }],
  "isPersonal": false
}

Every passenger in passengers — adults, children and infants — must be
referenced by exactly one room, and exactly one referenced passenger must have
isHolder: true (the holder must be an adult) — travel.tech
returns 422 (with PASSENGER_COUNT_MISMATCH, STAY_ROOM_COUNT_MISMATCH,
or STAY_ROOM_OCCUPANCY_MISMATCH as the field-level code) if the room split
doesn't add up or doesn't match the fare you built. Omit rooms entirely
for a single-room booking — behavior is unchanged.

Booking a stay with children

Search with each child's real age — rooms[].childrenAges, each 0 to 17 — so
the rates are priced for those ages. passengers.children can't carry ages, so
a search that sets it without rooms returns 422 with a field error on
rooms.0.childrenAges.

Then send every child as a passengers entry with "type": "child" (or
"infant" for a child aged 0 to 1), a name, and either age (whole years at
check-in) or birthday. A birthday is only required when the fare asks for
one; when you send it, the age is worked out from it at check-in:

{
  "fareId": "018f...",
  "passengers": [
    { "firstName": "Ada", "lastName": "Lovelace", "type": "adult", "isHolder": true },
    { "firstName": "Alex", "lastName": "Lovelace", "type": "child", "age": 7 }
  ],
  "rooms": [{ "passengerIndices": [0, 1] }],
  "isPersonal": false
}

Each room must carry the number of children, at the ages, the fare was searched
with. A mismatch returns 422 before any provider call is made:
STAY_ROOM_OCCUPANCY_MISMATCH for the wrong number of children in a room, and
STAY_CHILD_AGE_MISMATCH for a child whose age at check-in differs from the
searched age (or who has neither age nor birthday). A guest aged 16 or 17 is
a child for stays — book them as "type": "child" if you searched them as one.

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?