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)
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
solutionframe for the same hotel carries the
samepropertyId— treatcatalog-itemas insert-if-absent and
solutionas upsert-by-propertyIdto hydrate prices in place. - Tappable immediately: a
catalog-item'ssearchSolutionId+
propertyIdare valid forPOST /v1/stays/faresright away, before any
price has streamed. - Amenities:
amenitiesis always present (empty when none are
known) and carries the same canonical keys as thesolutionframe, so
amenity filters apply to unpriced cards too. - Photos:
imagescarries 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;
solutionframes 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, everycatalog-itemalso carries two
booleans —isPreferredandisRecommended— 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
streamssearchSolutionIdis set from the very first
frame and is the same value on everycatalog-itemandsolutionframe
of the stream. The two booleans are absent on other streams.
catalog-itemframes never carryisNegotiatedRate; it istrueonly
on a hotel'ssolutionframe, 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-statusis sent whenever the counts change.settledis 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.completeis sent once, when no more prices will arrive, and is always
followed bydone. 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 singleevent: errorframe and no
completeordone. Keep the cards and prices already shown; they remain
valid forPOST /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.
Updated 16 days ago