Book your first stay

Sign, search, price, build, book, and retrieve one stay end to end in curl, Python, and TypeScript.

This recipe takes you from a signed request to a retrieved booking in six
calls: sign, search, read fares, build a fare, book, and retrieve. The same
flow is shown in curl, Python, and TypeScript — copy each language's snippets
in order into one file to get a complete, runnable script for that language.

Before you start

  • A registered consumer (acme-travel below) with a signing secret — see
    Authentication.
  • A traveler bearer token issued by BizAway sign-in — see
    Authentication § Obtaining a token.
  • The travel.tech base URL, exported as TRAVELTECH_BASE_URL (production is
    https://api.travel.tech).
  • A REST client or runtime for your chosen language (curl, Python 3 with
    httpx, or Node.js with fetch).

Every request below carries the three consumer-HMAC headers plus the
traveler bearer token. The worked signing example uses Acme Travel Ltd's
consumer slug (acme-travel) and traveler Ada Lovelace
(018f2c1a-9b3e-7c4d-8a1f-3e5b7c9d1a2f), the dataset every page in these
guides shares.

Steps

1. Sign each request

Every call below needs the three consumer-HMAC headers computed from the
canonical signing string {METHOD}\n{PATH}\n{TIMESTAMP}\n{SHA256_HEX(BODY)}.
See Authentication for the full algorithm, worked
example, and troubleshooting. Define the signing helper once and reuse it for
every step below.

curl

export TRAVELTECH_BASE_URL="https://api.travel.tech"
export CONSUMER="acme-travel"
export TRAVELTECH_CONSUMER_SECRET="<your-consumer-secret>"
export TRAVELER_JWT="<traveler-jwt>"

sign_request() {
  local method="$1" path="$2" body="$3"
  TIMESTAMP="$(date +%s)"
  local body_hash
  body_hash="$(printf '%s' "$body" | openssl dgst -sha256 | awk '{print $NF}')"
  local canonical
  canonical="$(printf '%s\n%s\n%s\n%s' "$method" "$path" "$TIMESTAMP" "$body_hash")"
  SIGNATURE="$(printf '%s' "$canonical" | openssl dgst -sha256 -hmac "$TRAVELTECH_CONSUMER_SECRET" | awk '{print $NF}')"
}

Python

import hashlib
import hmac
import json
import os
import time

import httpx

BASE_URL = os.environ.get("TRAVELTECH_BASE_URL", "https://api.travel.tech")
CONSUMER = "acme-travel"
SECRET = os.environ["TRAVELTECH_CONSUMER_SECRET"]
TRAVELER_JWT = os.environ["TRAVELER_JWT"]


def signed_headers(method: str, path: str, body: bytes) -> dict[str, str]:
    timestamp = str(int(time.time()))
    body_hash = hashlib.sha256(body).hexdigest()
    canonical = f"{method}\n{path}\n{timestamp}\n{body_hash}"
    signature = hmac.new(SECRET.encode(), canonical.encode(), hashlib.sha256).hexdigest()
    return {
        "TravelTech-Consumer": CONSUMER,
        "TravelTech-Timestamp": timestamp,
        "TravelTech-Signature": signature,
        "Authorization": f"Bearer {TRAVELER_JWT}",
        "Content-Type": "application/json",
    }

TypeScript

import crypto from "node:crypto";

const BASE_URL = process.env.TRAVELTECH_BASE_URL ?? "https://api.travel.tech";
const CONSUMER = "acme-travel";
const SECRET = process.env.TRAVELTECH_CONSUMER_SECRET!;
const TRAVELER_JWT = process.env.TRAVELER_JWT!;

function signedHeaders(method: string, path: string, body: string): Record<string, string> {
  const timestamp = String(Math.floor(Date.now() / 1000));
  const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
  const canonical = `${method}\n${path}\n${timestamp}\n${bodyHash}`;
  const signature = crypto.createHmac("sha256", SECRET).update(canonical).digest("hex");
  return {
    "TravelTech-Consumer": CONSUMER,
    "TravelTech-Timestamp": timestamp,
    "TravelTech-Signature": signature,
    Authorization: `Bearer ${TRAVELER_JWT}`,
    "Content-Type": "application/json",
  };
}

2. Search for stays

POST /v1/stays/search (search_stays) streams
normalised stay solutions as Server-Sent Events. Search Milan for one adult,
four nights:

curl

BODY='{
  "destination": "milan-IT",
  "checkIn": "2026-02-10",
  "checkOut": "2026-02-14",
  "rooms": [{ "adults": 1, "childrenAges": [] }],
  "bookingProfile": "business"
}'
sign_request POST /v1/stays/search "$BODY"
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 "$BODY"

Python

search_body = {
    "destination": "milan-IT",
    "checkIn": "2026-02-10",
    "checkOut": "2026-02-14",
    "rooms": [{"adults": 1, "childrenAges": []}],
    "bookingProfile": "business",
}
body_bytes = json.dumps(search_body).encode()
with httpx.stream(
    "POST",
    f"{BASE_URL}/v1/stays/search",
    content=body_bytes,
    headers=signed_headers("POST", "/v1/stays/search", body_bytes),
) as response:
    for line in response.iter_lines():
        print(line)

TypeScript

const searchBody = JSON.stringify({
  destination: "milan-IT",
  checkIn: "2026-02-10",
  checkOut: "2026-02-14",
  rooms: [{ adults: 1, childrenAges: [] }],
  bookingProfile: "business",
});
const searchResponse = await fetch(`${BASE_URL}/v1/stays/search`, {
  method: "POST",
  body: searchBody,
  headers: signedHeaders("POST", "/v1/stays/search", searchBody),
});

The stream emits one event: solution frame per priced property, closing
with event: done:

event: solution
data: {"id":"018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0a","propertyId":"018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0b","name":"Grand Stay Milan","city":"Milan","countryCode":"IT","rating":4,"totalPrice":{"amount":"599.60","currency":"EUR"},"isPolicyCompliant":true}

event: done
data: {}

A solution's id is the searchSolutionId for the next step, and
propertyId identifies the property. If the requested stay exceeds the
account's configured maximum nights, the stream ends with event: error
and code MAX_NIGHTS_EXCEEDED instead of done. See
search_stays for the complete frame shape.

3. Read the fares

POST /v1/stays/fares (search_stay_fares)
streams the bookable room fares for the chosen solution. Pass the same
checkIn/checkOut/rooms you searched with, plus the solution's
searchSolutionId and propertyId:

curl

BODY='{
  "searchSolutionId": "018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0a",
  "propertyId": "018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0b",
  "checkIn": "2026-02-10",
  "checkOut": "2026-02-14",
  "rooms": [{ "adults": 1, "childrenAges": [] }],
  "isPersonal": false
}'
sign_request POST /v1/stays/fares "$BODY"
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 "$BODY"

Python

fares_body = {
    "searchSolutionId": "018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0a",
    "propertyId": "018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0b",
    "checkIn": "2026-02-10",
    "checkOut": "2026-02-14",
    "rooms": [{"adults": 1, "childrenAges": []}],
    "isPersonal": False,
}
body_bytes = json.dumps(fares_body).encode()
with httpx.stream(
    "POST",
    f"{BASE_URL}/v1/stays/fares",
    content=body_bytes,
    headers=signed_headers("POST", "/v1/stays/fares", body_bytes),
) as response:
    for line in response.iter_lines():
        print(line)

TypeScript

const faresBody = JSON.stringify({
  searchSolutionId: "018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0a",
  propertyId: "018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0b",
  checkIn: "2026-02-10",
  checkOut: "2026-02-14",
  rooms: [{ adults: 1, childrenAges: [] }],
  isPersonal: false,
});
const faresResponse = await fetch(`${BASE_URL}/v1/stays/fares`, {
  method: "POST",
  body: faresBody,
  headers: signedHeaders("POST", "/v1/stays/fares", faresBody),
});

Each streamed fare carries a fareId you can lock in the next step:

event: solution
data: {"fareId":"018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0c","totalPrice":{"amount":"599.60","currency":"EUR"},"pricePerNight":{"amount":"149.90","currency":"EUR"},"numNights":4,"isRefundable":true}

event: done
data: {}

The fare is only reserved for 30 minutes. Lock it with build (next step)
before that window closes, or you will need to search and read fares again.
See search_stay_fares for the full fare
shape.

4. Build the fare

POST /v1/stays/fares/build
(build_stay_fare) locks the chosen fareId
and returns it as JSON (201). It needs the fareId from the fares stream
and the searchSolutionId from search, for lineage:

curl

BODY='{ "fareId": "018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0c", "searchSolutionId": "018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0a", "isPersonal": false }'
sign_request POST /v1/stays/fares/build "$BODY"
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 "$BODY"

Python

build_body = {
    "fareId": "018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0c",
    "searchSolutionId": "018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0a",
    "isPersonal": False,
}
body_bytes = json.dumps(build_body).encode()
response = httpx.post(
    f"{BASE_URL}/v1/stays/fares/build",
    content=body_bytes,
    headers=signed_headers("POST", "/v1/stays/fares/build", body_bytes),
)
locked_fare = response.json()

TypeScript

const buildBody = JSON.stringify({
  fareId: "018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0c",
  searchSolutionId: "018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0a",
  isPersonal: false,
});
const buildResponse = await fetch(`${BASE_URL}/v1/stays/fares/build`, {
  method: "POST",
  body: buildBody,
  headers: signedHeaders("POST", "/v1/stays/fares/build", buildBody),
});
const lockedFare = await buildResponse.json();

A successful build returns the locked fare:

{
  "fareId": "018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0c",
  "totalPrice": { "amount": "599.60", "currency": "EUR" },
  "pricePerNight": { "amount": "149.90", "currency": "EUR" },
  "numNights": 4,
  "isRefundable": true
}

If the fare's 30-minute hold has lapsed, build returns 410 OFFER_EXPIRED
instead — see Edge cases below. See
build_stay_fare for the full response shape.

5. Book the stay

POST /v1/stays/bookings
(create_stay_booking) creates the
booking from the locked fareId. This call requires a unique
Idempotency-Key header — resending the same key returns the original
result instead of creating a duplicate booking (see Bookings and
trips
); omitting it returns 422.

curl

BODY='{
  "fareId": "018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0c",
  "passengers": [
    { "firstName": "Ada", "lastName": "Lovelace", "type": "adult", "isHolder": true }
  ],
  "isPersonal": false
}'
sign_request POST /v1/stays/bookings "$BODY"
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: 018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0d" \
  -d "$BODY"

Python

import uuid

booking_body = {
    "fareId": "018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0c",
    "passengers": [
        {"firstName": "Ada", "lastName": "Lovelace", "type": "adult", "isHolder": True},
    ],
    "isPersonal": False,
}
body_bytes = json.dumps(booking_body).encode()
headers = signed_headers("POST", "/v1/stays/bookings", body_bytes)
headers["Idempotency-Key"] = str(uuid.uuid4())
response = httpx.post(f"{BASE_URL}/v1/stays/bookings", content=body_bytes, headers=headers)
booking = response.json()

TypeScript

const bookingBody = JSON.stringify({
  fareId: "018f2c5a-1a2b-7c3d-8e4f-5a6b7c8d9e0c",
  passengers: [
    { firstName: "Ada", lastName: "Lovelace", type: "adult", isHolder: true },
  ],
  isPersonal: false,
});
const bookingHeaders = signedHeaders("POST", "/v1/stays/bookings", bookingBody);
bookingHeaders["Idempotency-Key"] = crypto.randomUUID();
const bookingResponse = await fetch(`${BASE_URL}/v1/stays/bookings`, {
  method: "POST",
  body: bookingBody,
  headers: bookingHeaders,
});
const booking = await bookingResponse.json();

A successful booking returns 201 with the new booking's detail. Its
status is pending immediately after creation — it advances as the
booking is confirmed:

{
  "bookingId": "018f2c1e-3c4d-7e5f-9a6b-7c8d9e0f1a2b",
  "vertical": "stay",
  "status": "pending",
  "totalPrice": { "amount": "599.60", "currency": "EUR" },
  "availableOperations": ["cancel_booking"]
}

See create_stay_booking for the full
response shape, and Bookings and trips for
managing the booking afterward.

6. Retrieve the booking

GET /v1/stays/bookings/{bookingId} (get_booking)
reads the booking back, refreshed from the provider:

curl

BOOKING_PATH="/v1/stays/bookings/018f2c1e-3c4d-7e5f-9a6b-7c8d9e0f1a2b"
sign_request GET "$BOOKING_PATH" ""
curl "$TRAVELTECH_BASE_URL$BOOKING_PATH" \
  -H "TravelTech-Consumer: $CONSUMER" \
  -H "TravelTech-Timestamp: $TIMESTAMP" \
  -H "TravelTech-Signature: $SIGNATURE" \
  -H "Authorization: Bearer $TRAVELER_JWT"

Python

path = "/v1/stays/bookings/018f2c1e-3c4d-7e5f-9a6b-7c8d9e0f1a2b"
response = httpx.get(f"{BASE_URL}{path}", headers=signed_headers("GET", path, b""))
booking_detail = response.json()

TypeScript

const detailPath = "/v1/stays/bookings/018f2c1e-3c4d-7e5f-9a6b-7c8d9e0f1a2b";
const detailResponse = await fetch(`${BASE_URL}${detailPath}`, {
  headers: signedHeaders("GET", detailPath, ""),
});
const bookingDetail = await detailResponse.json();

The response repeats the same status field — pending until the booking
is confirmed or cancelled — plus availableOperations, telling you which
actions (like cancel_booking) are valid next. See
get_booking for the full shape, and
Bookings and trips for the booking lifecycle.

Verify it worked

  • Search ends with an event: done frame after one or more
    event: solution frames.
  • Fares ends with event: done after one or more fare frames, each
    carrying a fareId.
  • Build returns 201 with the locked fareId and totalPrice.
  • Book returns 201 with a bookingId and status: "pending".
  • Retrieve returns 200 with the same bookingId and current status.

Edge cases

The fare expired before you built it. Build returns 410 with
OFFER_EXPIRED once the 30-minute fare hold lapses:

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

Search and read fares again to get a fresh fareId.

The search session expired. If you wait too long between search and
fares, fares also returns 410 OFFER_EXPIRED, with a different message —
"Search session has expired. Please refresh the search." Start again from
step 2.

The signature didn't verify. Any missing, malformed, or forged consumer
signature is rejected with 401:

{
  "error": {
    "code": "CONSUMER_AUTH_REQUIRED",
    "message": "Consumer authentication required",
    "details": [],
    "recoveryAction": "CONTACT_SUPPORT",
    "retryAfterSeconds": null
  }
}

Recheck the canonical string you signed against Authentication §
Troubleshooting
— a re-serialized
body or an unsigned query string are the most common causes.

The Idempotency-Key header was missing. Book returns 422 naming the
missing header. See Bookings and trips for the
full error shape.

Related guides

  • Authentication — the full consumer-HMAC signing
    algorithm, worked examples, and troubleshooting.
  • Searching and booking stays — the complete stays
    flow, including destination lookup, multi-room bookings, and children.
  • Bookings and trips — managing a booking after
    it's created: confirm, cancel, and idempotency.
  • Errors — the full error
    envelope shape and recoveryAction vocabulary.

Did this page help you?