If you search for a Hotels.com API, you will not find a developer portal. There is no public Hotels.com API: developer.hotels.com does not resolve, and Hotels.com publishes no developer docs (checked November 2026). Hotels.com is part of Expedia Group, and Expedia's own hotel API, Rapid, is partner-only: you apply, and your site is reviewed before you get a key (what Expedia Rapid requires).
So if you want Hotels.com prices in your app, the practical route is a hotel search API that returns Hotels.com as one of the sellers, and lets you filter to it.
Disclosure: I work on Ktemia, a travel search API that does exactly that. It searches booking sites for a stay and returns each seller's price with a link that opens that seller's site with the stay filled in. It compares; it does not book. Below is the whole flow in Python, with Hotels.com as the only seller, and then with every seller side by side.
What you need
- Python 3 and
requests(pip install requests) - A Ktemia API key from app.ktemia.com. The free plan includes 100 requests a month, and every call below counts as one.
export KTEMIA_API_KEY="your-key" Keep the key on a server. Never put it in frontend JavaScript.
How the flow works
Every Ktemia vertical works the same way, and hotels take four calls:
-
GET /hotels/locations: turn "London" into a location id. A place name on the search is rejected. -
POST /hotels/search: start the search. It returns asearchToken, not hotels. -
GET /hotels/results: read a page of up to 30 hotels with that token. Each hotel has abestOption: the cheapest seller and its price. -
GET /hotels/details: expand one hotel into every seller's price, each with aproviderUrl.
The filter that matters here is providers. Send ["Hotels.com"] and every price in the search comes from Hotels.com only.
The code
import os import requests API = "https://api.ktemia.com" HEADERS = {"X-API-Key": os.environ["KTEMIA_API_KEY"]} def call(method, path, **kwargs): r = requests.request(method, API + path, headers=HEADERS, timeout=120, **kwargs) body = r.json() if not r.ok: # Every error has the same shape: {"success": false, "error": {"code", "message", "param"}} raise RuntimeError(f"{r.status_code} {body['error']['code']}: {body['error']['message']}") return body # 1. Turn a place name into a location id places = call("GET", "/hotels/locations", params={"query": "London", "country": "GB"}) print(places) # pick the place you mean; ask the user if several match location_id = "..." # the locationId of the place you picked # 2. Start a search, Hotels.com only search = call("POST", "/hotels/search", json={ "location": location_id, "checkin": "2027-03-10", "checkout": "2027-03-12", "adults": 2, "rooms": 1, "providers": ["Hotels.com"], "country": "GB", # the country picks the market and the currency (GBP here) }) token = search["searchToken"] # valid for 60 minutes # 3. Read the first page of hotels page = call("GET", "/hotels/results", params={"token": token}) for hotel in page["hotels"]: best = hotel.get("bestOption") or {} total = (best.get("totalPrice") or {}).get("formatted") print(f"{hotel['name']}: {total} on {best.get('provider')}") # 4. Expand one hotel into every seller and its booking link details = call("GET", "/hotels/details", params={"token": token, "resultId": page["hotels"][0]["id"]}) for option in details.get("bookingOptions", []): print(option["provider"], (option.get("totalPrice") or {}).get("formatted"), option.get("providerUrl")) A few details that save you a failed call:
- Seller ids are matched exactly. It is
"Hotels.com", with the capital and the dot. The ids that exist for your search are inavailableFilters.bookingSiteson a results page. Read them from there rather than hard-coding a list, because coverage changes by city and date. - There is no
currencyfield. Sendcountry, and prices come back in that country's currency. Without it, the market is the US and prices are in USD. -
pricevstotalPrice:priceis the stay before taxes (what a price sort uses).totalPriceincludes taxes. - More pages: if
hasMoreis true, sendnextTokenas thetokenon the next/hotels/resultscall. There are no page numbers. - Common errors:
400 INVALID_LOCATION(send a location id, not a name),404 NO_RESULTS(nothing published for those dates),410 SEARCH_EXPIRED(the token is older than 60 minutes; start a new search).
Hotels.com vs every other booking site
Filtering to Hotels.com answers "what does Hotels.com charge?". Usually the more useful question is "is Hotels.com the cheapest?". Drop the providers filter and run the same search:
search = call("POST", "/hotels/search", json={ "location": location_id, "checkin": "2027-03-10", "checkout": "2027-03-12", "adults": 2, "country": "GB", }) Now /hotels/details returns every seller for the same room side by side: in the cities we tested in November 2026, that typically included Booking.com, Expedia, Hotels.com, Priceline, Agoda, and Trip.com. Show them as a list and let the person pick, and open providerUrl exactly as the API returned it.
The gap between sellers is often bigger than people expect. We measured it for flights across 57 booking sites (the data is here), and this London hotel comparison prices one stay on six sites with the same code.
Going further
- Hotel API reference: every field, with examples in seven languages
- Hotel price API: every booking site in one call
- Best travel API in 2026: which travel APIs you can get a key for
- Using Claude Code, Codex, or Cursor? The open-source
hotel-searchskill runs this whole flow from a chat: Ktemiaa/ktemia-skills
What are you building with hotel prices? I'm curious whether people mostly need one site's prices or the full comparison.
