Booking.com API — FetchLayer Docs
Documentation menu
FetchLayer FetchLayer API

Booking.com API v1

Booking.com properties, priced room options and guest reviews as JSON or Markdown, from https://api.fetchlayer.dev/booking. Search prices every bookable room, not just the property. Reviews keep what a guest liked and what they disliked in separate fields and say how complete they are in the response itself. Money arrives as a number and a currency code, never a display string.

OpenAPI JSON

Search prices every room, not just the property

A results page that only prices the cheapest room answers half the question. Every property in a search carries price for its cheapest bookable stay and roomOptions[], each option priced on its own:

"price": { "amount": 642.75, "currency": "EUR" },
"roomOptions": [
  {
    "roomId": "1425667101",
    "occupancy": 2,
    "price":         { "amount": 642.75, "currency": "EUR" },
    "originalPrice": { "amount": 642.75, "currency": "EUR" },
    "freeCancellationUntil": null,
    "mealPlan": null,
    "hasCrib": false
  }
]

Money is {amount, currency} — a number and a code — so comparing two cities is arithmetic rather than a regex over "€642.75". originalPrice above price is a discount, equal means none. freeCancellationUntil, mealPlan, occupancy and hasCrib sit on each offer, so a room with breakfast and free cancellation can be told from a bare rate without opening the page.

Every property came back priced across the runs measured on 17 September 2026 — 21 of 21 in Paris, 26 of 26 in Lisbon.

Dates are optional, and the response says which stay it priced

Leave checkIn and checkOut off a search and it still prices — for a default stay window it chooses — and then tells you which one, both on the response's own checkIn/checkOut and in words:

"notes": [
  "No check-in/check-out was given, so this search used a default stay
   this service chose (2026-10-17 to 2026-10-18). Pass checkIn and
   checkOut to price a specific stay."
]

So a price is never silently for dates you did not ask about, and two requests made a week apart can be compared by reading two fields rather than by trusting that nothing moved.

Route What dates do What you get
/search-hotels Price the stay. With none, a default window is used and reported. Every property and every room option priced
/hotel-detail Nothing. Accepted, answered with a note. The property's catalogue — rooms, sizes, photos, amenities

Passing dates to /hotel-detail returns this, verbatim: “This route does not return a live price for this stay. Use search-hotels with the same destination and dates to get this property's priced room options.” It is the documented answer, and it names the route that does price.

Reviews tell you how complete they are — and split liked from disliked

Ask for ten reviews and you get ten, and you get told there are 3,854, in the same response:

"totalReviewCount": 3854,
"reviewCount": 10,
"hasMore": true,
"notes": [
  "These are 10 of the 3854 reviews the source reports for this property.",
  "More reviews are available. Continue with nextCursor."
]

An average over ten of 3,854 reviews and an average over all of them are different numbers, and everywhere else in this category they look identical on the wire. Here the response draws the distinction for you.

Each review keeps what the guest liked and what they disliked in separate fieldspositiveText and negativeText, either possibly null — in the language they wrote them, with a language code beside them. Nothing is machine-translated over the guest's own words. So “how many people complained about the bathroom” is a count over one field instead of sentiment analysis over a paragraph.

Every review is anchored to a real stay: stayCheckIn, stayCheckOut, nights and the roomType the guest actually slept in, plus their public first name, country and travelerType (“Couple”, “Family”). A complaint can be attributed to a room type, and a one-night opinion separated from a six-night one.

A scored review with no text is ordinary. Of the ten measured on 17 September 2026, one carried a score of 9 with title, positiveText and negativeText all null. Three languages turned up in those ten — Italian, Turkish and German — so branch on language before running anything over the text.

Where the review score lives, and where the breakdown lives

Two different answers on two different routes, and worth knowing before you build on either.

Route Headline score Seven category scores
/search-hotels {average: 8.1, count: 3854, label: "Very Good"}
/hotel-detail {average: null, count: null, label: null} reviewScoreBreakdown, all seven
/hotel-reviews totalReviewCount categoryScores, all seven

The category detail is what makes a score usable. The property measured sits at 8.1 overall, which hides a 9.44 on Location against a 7.82 on Facilities:

[
  { "category": "hotel_staff",     "label": "Staff",          "average": 8.94 },
  { "category": "hotel_services",  "label": "Facilities",     "average": 7.82 },
  { "category": "hotel_clean",     "label": "Cleanliness",    "average": 8.51 },
  { "category": "hotel_comfort",   "label": "Comfort",        "average": 8.57 },
  { "category": "hotel_value",     "label": "Value for money","average": 8.09 },
  { "category": "hotel_location",  "label": "Location",       "average": 9.44 },
  { "category": "hotel_free_wifi", "label": "Free Wifi",      "average": 8.79 }
]

Read a search row or a reviews call for the single number; read a detail call or a reviews call for the seven. Measured on two unrelated properties, so it is the route's behaviour rather than one property's gap.

Read hasMore, not resultsTotal

resultsTotal is how many properties Booking.com says match the destination — 2,403 for Lisbon, 3,000 for Paris. It is a match count, not a page plan: it does not promise that all of them can be walked. One Lisbon query returned 26 properties with hasMore: true and a cursor, and the same query later returned 21 with hasMore: false. Asking for pages: 2 on that second query still reported pagesFetched: 1 — and billed one credit, not two.

So: read hasMore and follow nextCursor. Never divide resultsTotal by a page size to plan a walk.

Available endpoints

Paging and billing

One credit per page fetched, stated on every response as pagesFetched. A search page is about 25 properties — Booking.com's own page size, not ours — and hotelCount is what actually arrived; up to 20 pages in one request. Reviews walk up to 20 pages and 500 reviews in one request, 25 per page by default, and a walk stops early when limit is reached. Pass -1 on pages or limit to mean “the maximum”.

/resolve-url fetches nothing at all and is free, reporting pagesFetched: 0.

Measured against production on 17 September 2026, three runs each: a one-page Lisbon search answered in 3.61–3.94s with 21 properties, Paris in 3.60–3.82s, a property read in 3.62–3.93s, ten reviews in 4.28–4.49s, and sixty reviews across three pages in 6.14s. A link resolved in 0.065–0.13s.

Errors

  • 400 — the request is wrong, and the message names the problem: a missing required field, or an unknown one. The body is a strict object on every route, so sortBy is refused by name — {"error":"Unrecognized key: \"sortBy\""} — rather than accepted and ignored. A typo never returns silently wrong data.
  • 401 — missing or invalid key: {"error":"Unauthorized"}.
  • 404 — on /hotel-detail and /hotel-reviews, an id that names no property: {"error":"no property was found for \"zz/no-such-hotel-xyz\""}. A 404 with a message, not a 503 — a wrong id is a wrong request to fix, not a degraded service to retry.