Integration Guide

KrawlX: Expedia Hotel Data API

Live hotel search and property detail from Expedia storefronts in 25 countries. Two endpoints, one API key.

Base URL
https://api.krawlx.io
Auth
X-API-Key header
Format
JSON over HTTPS
Measured uptime
99.8–99.9%

Read this before you write any code

Two client-side settings account for nearly every integration problem we have seen. Both are about how your HTTP client behaves, not about our API:

Both are shown in the worked example at the end.

Quick start

Two calls to confirm your key works before you write anything. Both were run against production while writing this page; the timings below are those runs.

Hotel search — 200 in 9.9s, 100 hotels of 496
curl -X POST https://api.krawlx.io/v1/hotels-search   -H "X-API-Key: krwlx_live_YOUR_KEY_HERE"   -H "Content-Type: application/json"   -H "Accept-Encoding: gzip" --compressed   -d '{
        "regionId":     "178279",
        "check_in":     "2026-11-10",
        "check_out":    "2026-11-12",
        "country_code": "GB",
        "page":         1
      }'
Property detail — 200 in 4.4s, 928 KB of HTML
curl -X POST https://api.krawlx.io/v1/hotel-details   -H "X-API-Key: krwlx_live_YOUR_KEY_HERE"   -H "Content-Type: application/json"   -H "Accept-Encoding: gzip" --compressed   -d '{
        "url": "https://www.expedia.com/Hotel-Name.h124299825.Hotel-Information?chkin=2026-09-17&chkout=2026-09-18",
        "country_code": "US"
      }'

On Windows PowerShell, send the body from a file

PowerShell strips the inner quotes from a single-quoted string before handing it to a native executable, so an inline -d '{…}' arrives as malformed JSON and returns 400. Write the body out first:

PowerShell
'{"regionId":"178279","check_in":"2026-11-10","check_out":"2026-11-12","country_code":"GB","page":1}' |
  Set-Content body.json -Encoding ascii -NoNewline

curl.exe -X POST https://api.krawlx.io/v1/hotels-search `
  -H "X-API-Key: krwlx_live_YOUR_KEY_HERE" `
  -H "Content-Type: application/json" `
  --compressed -d "@body.json"

Note curl.exe, not curl — bare curl in PowerShell is an alias for Invoke-WebRequest and takes entirely different arguments.

Authentication

Send your key in the X-API-Key header on every request.

HTTP
X-API-Key: krwlx_live_…

Authorization: Bearer <key> also works. An apikey field in the JSON body is accepted for compatibility, but the header is strongly preferred — a key in the body ends up in request logs.

Keep the key secret. It identifies your account and meters your usage. If it leaks, tell us and we will issue a replacement.

Hotel search

POST /v1/hotels-search 100 results per page

Returns hotels in a region for a date range.

Request body
{
  "regionId":     "178279",
  "check_in":     "2026-11-10",
  "check_out":    "2026-11-12",
  "country_code": "GB",
  "page":         1,
  "starRating":   5
}
FieldRequiredNotes
regionIdyesExpedia region id — e.g. 178279 is London
check_inyesYYYY-MM-DD
check_outyesYYYY-MM-DD
country_codeyesSelects the storefront, and therefore the currency
pageno1-based, defaults to 1
starRatingnoFilter, 1–5

Response

200 OK
{
  "status": 200,
  "message": "Success",
  "regionId": "178279",
  "hotels": { "data": { "propertySearch": { … } } },
  "pagination_info": {
    "current_page":        1,
    "total_pages":         6,
    "total_properties":    507,
    "properties_per_page": 100
  }
}

Hotel records live under hotels.data.propertySearch.propertySearchListings. That array also carries sponsored placements and banners, so filter on __typename === "LodgingCard" to get actual properties.

Property detail

POST /v1/hotel-details rooms, rates and full page

Returns full detail, rooms and rates for a single hotel.

Request body
{
  "url": "https://www.expedia.co.uk/Some-Hotel.h91827909.Hotel-Information?chkin=2026-11-10&chkout=2026-11-12",
  "country_code": "GB"
}

The URL must contain the Expedia property id in .h<digits>. form. Check-in and check-out are read from its query string when present; without them, pricing detail is limited.

Returns propertyId, html (the rendered page, typically ~2.5 MB), json (the batched GraphQL results) and has_room_offers.

Where the room details are

json is an array of three results, and two of them are data.propertyOffers. Only one carries rooms.

IndexPayloadSizeRooms?
[0]data.propertyOffers — ancillary~6 KBno
[1]data.offersTravelerSelector~11 KBno
[2]data.propertyOffers — rooms and rates~370 KByes
Room details
json[2].data.propertyOffers.categorizedListings[]
  ├── header.text        → room name, e.g. "Double Room"
  ├── unitId
  ├── features / roomAmenities
  └── primarySelections[0].propertyUnit   → rates

The array is categorizedListings, not units. Reading index [0] returns a valid propertyOffers object with no rooms in it — the most common mistake with this endpoint.

Prefer locating it by content rather than position, so your code survives any upstream reordering:

Python
def rooms(body):
    for result in body.get("json") or []:
        offers = (result.get("data") or {}).get("propertyOffers") or {}
        if offers.get("categorizedListings"):
            return offers["categorizedListings"]
    return []          # no offers -- check has_room_offers

has_room_offers

Every response carries this boolean. Check it before parsing.

ValueMeaning
trueRooms are present at the path above
falseNo room offers. The html is still returned and usable

false is frequently a correct answer, not an error. Each hotel publishes its own rate calendar, and Expedia returns no offers for dates beyond it. This is per-property: one hotel stops returning rooms from 2027-01-01 while another returns them normally into late January 2027. There is no fixed cut-off to filter on — has_room_offers is the signal.

We retry automatically when the upstream returns an empty offers payload, so a false you receive has already survived several attempts. Those retries push the latency tail higher, which is another reason for the 120 s+ timeout.

If the page loads but the GraphQL call fails entirely, you get 200 with "json": null, "has_room_offers": false and a Partial success message. Treat that as usable-but-incomplete rather than a failure.

If the page loads but the pricing call fails, you get 200 with "json": null and a message of Partial success — HTML fetched but JSON unavailable. Treat that as usable-but-incomplete rather than a failure.

If you only need availability and pricing across many hotels, hotel search is far more efficient — one call returns 100 properties.

Storefronts

country_code selects which Expedia storefront is queried, which determines currency and pricing. GB returns GBP from expedia.co.uk; US returns USD from expedia.com.

Supported codes
AR  AU  BE  BR  CA  DE  DK  ES  FR  GB  HK  IE  IN
IT  JP  KR  MX  MY  NL  NO  NZ  SE  SG  TH  US

An unsupported code returns 400 listing the valid options.

Errors

Every response — success or failure — carries an X-Request-Id header. Quote it in any support request. It is the key to the full server-side trace of your call.

400 Bad Request
{
  "status": 400,
  "error": "BadRequest",
  "message": "Unsupported country code: 'ZZ'. Supported: AR, AU, BE, …",
  "request_id": "3f6fa683d2514e888adc234753dab62d"
}
StatusMeaningRetryWhat to do
400Invalid parameters NoFix the request — the answer will not change
401Missing or invalid key NoCheck the X-API-Key header
404Hotel page not found NoThe listing no longer exists on Expedia. Expected in bulk runs — skip it
429Rate limited or at capacity YesBack off briefly, then retry. Reduce concurrency if sustained
502Upstream request failed YesRetry with exponential backoff and jitter
503Upstream unavailable YesRetry after a short pause
504Upstream timed out YesRetry with backoff

Retry policy

Retry only 429 and 5xx, with exponential backoff and jitter. Never retry 4xx — the answer will not change, and it still counts against your usage.

Jitter matters more than it sounds. A fixed backoff makes every worker retry in lockstep, so a brief upstream wobble turns into a synchronised wave that is far harder to recover from than the original problem.

A 429 is deliberately fast: we shed load rather than queue you behind a long timeout.

Usage and billing

ResponseCounts toward usageWhy
2xxYesYou received data
4xxYesYou received a definitive answer
5xxNoA failure on our side
429NoWe declined to serve you

Worked example

This is the shape we recommend. The three commented lines are the ones that matter.

Python
import random, time
import requests
from requests.adapters import HTTPAdapter

API_KEY = "krwlx_live_…"

session = requests.Session()
# 1. Reuse connections. Without this, every request opens a new TCP+TLS
#    connection and your own network will start dropping them under load.
session.mount("https://", HTTPAdapter(pool_connections=64, pool_maxsize=64))
session.headers.update({"X-API-Key": API_KEY, "Accept-Encoding": "gzip"})


def search(payload, attempts=4):
    for attempt in range(attempts):
        r = session.post(
            "https://api.krawlx.io/v1/hotels-search",
            json=payload,
            timeout=180,          # 2. NOT the default. Well above p99.
        )
        if r.status_code < 500 and r.status_code != 429:
            return r              # 2xx or a definitive 4xx: done either way

        # 3. Exponential backoff WITH jitter, so concurrent workers do not
        #    retry in lockstep and turn a wobble into a thundering herd.
        time.sleep((2 ** attempt) * (0.5 + random.random()))
    r.raise_for_status()


body = {
    "regionId": "178279",
    "check_in": "2026-11-10",
    "check_out": "2026-11-12",
    "country_code": "GB",
    "page": 1,
}
result = search(body).json()

listings = result["hotels"]["data"]["propertySearch"]["propertySearchListings"]
hotels = [x for x in listings if x.get("__typename") == "LodgingCard"]
print(len(hotels), "of", result["pagination_info"]["total_properties"])

Compression

Responses are large — roughly 2 MB uncompressed for a full page of hotels. Send Accept-Encoding: gzip and they arrive about 13× smaller. Most HTTP clients do this by default; the example sets it explicitly.

Support

Include the X-Request-Id and the approximate time of the request. With those two things we can trace exactly what happened on our side, down to the individual upstream call.