The full attraction record. Several columns are empty on every product measured because the upstream does not fill them — each says so.
Describes build 1.0.173 of rl1987/booking-api-scraper, deployed 2026-10-29. The column set last changed 2026-10-29. Earlier builds can return a different one.
Modes: attractionDetails
Dataset view: attractionDetails — ?view=attractionDetails on the items API, or the matching tab in the Console.
The full attraction record. Several columns are empty on every product measured because the upstream does not fill them — each says so.
57 columns, in the order the view shows them.
| # | Column | Label | Type | Description | Example |
|---|---|---|---|---|---|
| 1 | scrapedAt | Scraped at | string, nullable | When the response that produced this row came back - the PRICED response where the row has a price, not when the row was pushed, which is batched and later by seconds to minutes. ISO-8601 UTC, to milliseconds. Note Booking’s own date strings in the same row are NOT ISO (freeCancellationUntil is “2026-10-28 23:59:59 +0000”), so do not point one parser at both. Prices are fetched live - nothing in this Actor is cached or reused - so this is the age of the price, and a dataset read a week later does not refresh it. | 2026-10-27T12:53:42.118Z |
| 2 | scrapedAtUnix | Scraped at (epoch) | integer, nullable | The same instant as scrapedAt, in epoch SECONDS, for consumers that diff timestamps arithmetically. Two keys rather than one selectable format, so a column’s type never moves with a run setting and two datasets always join. | 1793105622 |
| 3 | attractionId | Attraction ID | string, nullable | Booking’s attraction product id. | PRDF8MpWklG6 |
| 4 | name | Name | string, nullable | Property name on a property row; the attraction name on an attraction row. | AIRINN Vilnius Airport Hotel RENOVATED 2025 |
| 5 | status | Status | string, nullable | Rates mode: “available”, “not_offered” or “lookup_failed” - the last means we could not ask, the middle means Booking answered and the rate is not on sale. attractionAvailability mode: null means dates were bookable inside your window; no_availability_in_window is the real negative - days were checked and none were bookable; window_outside_calendar means the attraction is real but your window falls outside the span Booking publishes, so nothing was checked at all and calendarFrom/calendarTo give that span; not_found means the id resolved to nothing. That last one is INFERRED rather than reported: Booking answers an unknown id with a successful but empty calendar rather than an error, so an empty calendar is the discriminator - and it also covers an attraction withdrawn from sale. Telling the three negatives apart matters, because only the middle one means the attraction has no dates for you. | available, not_offered |
| 6 | error | Error | string, nullable | Attractions modes: the upstream message when an id could not be looked up. | Not Found (attractions backend 404) |
| 7 | category | Category | string, nullable | The attraction’s taxonomy slug. Detail only. | |
| 8 | bookable | Bookable | boolean, nullable | Whether the attraction can be booked. | |
| 9 | slug | Slug | string, nullable | URL slug for the attraction. | |
| 10 | shortDescription | Short description | string, nullable | One-line description, and a CATALOGUE-row field: the attractions mode fills it (100 of 100 Rome products measured 2026-10-28) and the attractionDetails surface returns null for it (0 of 6 of those same products). Nothing is lost - a detail row carries the full, untruncated text in description, which on those products is the same prose the catalogue truncates into this column. Issue #58 reported it null on 5 of 5 detail rows and was right; a figure of 12 of 12 attractionDetails rows stood here until 2026-10-28 and had been measured on catalogue rows, so read description on a detail row and this column on a catalogue row. | |
| 11 | description | Description | string, nullable | Full description, attractionDetails only. | |
| 12 | currency | Currency | string, nullable | ISO 4217 code every money figure on the row is in. | EUR |
| 13 | provenance | Provenance | object, nullable | What produced this row’s price: the proxy exit it was fetched through, the generation of that exit, the currency you asked for and the endpoint that priced it. The point of sale follows the proxy exit country, not any input - bookerCountry was removed in this build because nothing read it. | |
| 14 | price | Price | object | number, nullable | On a property row, the price object: base, book/total, per_night and extra_charges. On a rate or attraction row, a plain number. Read total_with_charges to match what the buyer is shown - it is right on every path, which total is not. Where Booking excludes taxes from its display price, total understates: measured on a search row, display EUR 246.28 with EUR 82.50 of excluded charges beside it. Where the taxes are already inside gross_amount, the two are the same number and both match the screen: measured on a room-list rate, MYR 151 on the app’s price sheet against MYR 151.305 on the wire. discount_percent is not Booking’s badge: it is computed from Booking’s own figures and moves with the point of sale, so use it to rank savings within a run rather than to reproduce what a browser shows. | |
| 15 | priceBeforeDiscount | Price before discount | number, nullable | Attractions: the pre-discount price where one is stated. | |
| 16 | durationLabel | Duration | string, nullable | Human-readable duration. | 2 hours |
| 17 | durationValue | Duration (ISO-8601) | string, nullable | Duration as an ISO-8601 period string, NOT a number of minutes - “PT10H” is ten hours. Parse it, do not cast it. | PT10H, PT12H |
| 18 | reviewScore | Review score | number, nullable | Booking’s guest score, 0-10. | 9.3 |
| 19 | reviewCount | Reviews | integer | number, nullable | Review count for the attraction. | |
| 20 | reviewCountOnPlatform | Reviews on Booking | integer | number, nullable | Reviews left on Booking itself. | |
| 21 | reviewTotal | Reviews (detail) | integer | number, nullable | Review total from the detail surface. | |
| 22 | reviewCategories | Review categories | array, nullable | Per-category attraction scores. | |
| 23 | freeCancellation | Free cancellation | boolean, nullable | Whether a free-cancellation rate exists for the stay (property row) or the attraction is freely cancellable (attraction row). | |
| 24 | stillRefundable | Still refundable | boolean, nullable | Whether it can still be cancelled now. | |
| 25 | cancellationUntil | Cancellable until | string, nullable | ISO-8601 cancellation deadline. | |
| 26 | whatsIncluded | What’s included | array, nullable | What’s included for the attraction, attractionDetails only. Several of these are empty on every product measured - the upstream does not fill them. | |
| 27 | notIncluded | Not included | array, nullable | Not included for the attraction, attractionDetails only. Several of these are empty on every product measured - the upstream does not fill them. | |
| 28 | uniqueSellingPoints | Selling points | array, nullable | Selling points for the attraction, attractionDetails only. Several of these are empty on every product measured - the upstream does not fill them. Conditional on the product: filled on 4 of 12 rows measured across six cities. | |
| 29 | highlights | Highlights | array, nullable | Highlights for the attraction, attractionDetails only. Several of these are empty on every product measured - the upstream does not fill them. | |
| 30 | labels | Labels | array, nullable | Labels for the attraction, attractionDetails only. Several of these are empty on every product measured - the upstream does not fill them. | |
| 31 | restrictions | Restrictions | array, nullable | Restrictions for the attraction, attractionDetails only. Several of these are empty on every product measured - the upstream does not fill them. Conditional on the product: filled on 3 of 12 rows measured across six cities. | |
| 32 | additionalInfo | Additional info | string | array, nullable | Free text the operator adds, newline-joined into a single string on every product measured. | |
| 33 | accessibility | Accessibility | array, nullable | Accessibility for the attraction, attractionDetails only. Several of these are empty on every product measured - the upstream does not fill them. | |
| 34 | healthSafety | Health & safety | array, nullable | Health and safety for the attraction, attractionDetails only. Several of these are empty on every product measured - the upstream does not fill them. Conditional on the product: filled on 4 of 12 rows measured across six cities. | |
| 35 | dietOptions | Diet options | array, nullable | Diet options for the attraction, attractionDetails only. Several of these are empty on every product measured - the upstream does not fill them. Not observed filled on any product yet - 0 of 48 across seven cities. Kept rather than dropped because the upstream selects it and a food tour is the product type that would carry it; treat an empty value as ’this product does not state any’, not as ’this column works'. | |
| 36 | guideLanguages | Guide languages | array, nullable | Guide languages for the attraction, attractionDetails only. Several of these are empty on every product measured - the upstream does not fill them. | |
| 37 | audioGuideLanguages | Audio guide languages | array, nullable | Audio guide languages for the attraction, attractionDetails only. Several of these are empty on every product measured - the upstream does not fill them. Conditional on the product: filled on 4 of 12 rows measured across six cities. | |
| 38 | offers | Offers | array, nullable | Bookable offers with their availability type. | |
| 39 | operatedBy | Operated by | string, nullable | Operator name. | |
| 40 | poweredBy | Powered by | string, nullable | Platform behind the booking. | |
| 41 | ufi | UFI | integer | number, nullable | Booking’s numeric destination id for the city or area. | -2620663 |
| 42 | city | City | string, nullable | City name. | Vilnius |
| 43 | country | Country | string, nullable | Country of the destination. | |
| 44 | address | Address | string, nullable | Street line as the mobile API returns it. The product’s own street address, when Booking publishes one: filled on 5 of 12 rows measured. A tour or transfer usually has no fixed address and returns null; a ticketed venue usually has one. | |
| 45 | addressCity | Address city | string, nullable | City from the attraction address block. The product’s own city, when published: 6 of 12 rows measured. Not the same as city, which is the DESTINATION the product is listed under and is always present. | |
| 46 | postalCode | Postal code | string, nullable | Postal code where the property states one. The product’s own postcode, when published: 4 of 12 rows measured. | |
| 47 | cityLatitude | City latitude | number, nullable | Latitude of the DESTINATION this attraction is listed under - the city centre, not the product. It comes from Booking’s ufiDetails, beside the city name. Measured over 21 products in three cities: three distinct pairs, one per city. An attraction row’s own coordinates are latitude/longitude on the attractionDetails row, which fill on about a third of products. | 41.89587021 |
| 48 | cityLongitude | City longitude | number, nullable | Longitude of the destination the attraction is listed under. See cityLatitude: this is the city centre, not the product. | 12.48261738 |
| 49 | latitude | Latitude | number, nullable | The property’s own latitude on a property row. On an ATTRACTION row this is the destination’s centroid, not the attraction’s - every attraction in a city returns the same pair. On an attractionDetails row this is the PRODUCT’s own latitude, filled on 6 of 12 rows measured - a tour often has none. The destination centroid is cityLatitude, always present. Coerced to a number: the upstream sends it as a string on some products. | 54.6341 |
| 50 | longitude | Longitude | number, nullable | As latitude: the property’s own on a property row, the destination’s centroid on an attraction row. | 25.2858 |
| 51 | meetingPoint | Meeting point | string, nullable | Where the guest meets the operator. Null on every product measured - the upstream returns addresses.meeting as null. Where the product starts, when Booking publishes it: 3 of 36 products measured. Mostly null even on tours. | |
| 52 | meetingInstructions | Meeting instructions | string, nullable | Instructions for the meeting point. How to find the meeting point, when published. Null on every delivered row measured so far (0 of 12); seen only alongside a meetingPoint. | |
| 53 | url | URL | string, nullable | Web page for the property or attraction. Populated on search, hotel_ids, rooms and details rows; absent on reviews, reviewScores and surroundings rows, which carry only the id, the stay and what you asked for - join those back on id. | |
| 54 | termsUrl | Terms | string, nullable | Operator terms URL. | |
| 55 | privacyPolicyUrl | Privacy policy | string, nullable | Operator privacy policy URL. | |
| 56 | images | Photos | array, nullable | Photo URLs, attractionDetails only. | |
| 57 | recordType | Record type | string, nullable | Which kind of row this is. Absent on property rows (search, rooms, details, surroundings, reviews, reviewScores); “rate” in mode rates; “attraction”, “attractionDetail” or “attractionAvailability” in the attractions modes. One dataset can hold several kinds, so filter on this before reading kind-specific columns. | rate, attraction |
Some columns above carry an object, or a list of objects, with documented members of their own. Each table below lists the members documented at ONE level; a member that is itself an object names the table it continues in, and [] in a path means a list.
row
├─ provenance
└─ price
provenance — Provenance
Parent chain: row → provenance
What produced this row’s price: the proxy exit it was fetched through, the generation of that exit, the currency you asked for and the endpoint that priced it. The point of sale follows the proxy exit country, not any input - bookerCountry was removed in this build because nothing read it.
| # | Field | Label | Type | Description | Example |
|---|---|---|---|---|---|
| 1 | exitCountry | Exit country | string, nullable | ISO country of the proxy exit this row was fetched through, sampled once per proxy generation from Apify’s own request echo. This is the market that priced the row: on the hotel_ids path taxes come back inside the display price from an EU exit and excluded through a US one. Null when the run’s pool rotates per request (a residential proxy with no session), when the probe failed, or past the per-run probe budget - null means not established, not “no market”. Pin country-XX or a session in the proxy configuration to make it exact for every row. | US, GB |
| 2 | proxyGeneration | Proxy generation | integer, nullable | Which exit fetched this row, counted from 0. The retry ladder draws a fresh exit when one fails - six generations in 30 seconds on one measured run - so two rows of one dataset can honestly carry different tax treatment. RUN_SUMMARY.request.exitCountries lists every generation and its country. | 0 |
| 3 | currencyRequested | Currency requested | string, nullable | The currency you asked for, beside the honoured currency. Not recoverable from the row otherwise: HOTEL resolves to the property’s own code, which the row then states as its own currency - so a row quoting MYR could have been asked for as HOTEL or as MYR. | EUR, HOTEL |
| 4 | surface | Surface | string, nullable | The endpoint that produced this row: search (the destination search), roomList (the per-property rate list) or hotelPage (the by-id path). The surfaces disagree in documented ways - roomList carries the structured meal price and hotelPage does not - so a buyer comparing two rows needs to know which answered. | roomList, hotelPage |
price — Price
Parent chain: row → price
On a property row, the price object: base, book/total, per_night and extra_charges. On a rate or attraction row, a plain number. Read total_with_charges to match what the buyer is shown - it is right on every path, which total is not. Where Booking excludes taxes from its display price, total understates: measured on a search row, display EUR 246.28 with EUR 82.50 of excluded charges beside it. Where the taxes are already inside gross_amount, the two are the same number and both match the screen: measured on a room-list rate, MYR 151 on the app’s price sheet against MYR 151.305 on the wire. discount_percent is not Booking’s badge: it is computed from Booking’s own figures and moves with the point of sale, so use it to rank savings within a run rather than to reproduce what a browser shows.
| # | Field | Label | Type | Description | Example |
|---|---|---|---|---|---|
| 1 | base | Base | number, nullable | Net rate before any listed charge. | |
| 2 | book | Book | number, nullable | What you pay for the stay, as Booking displays it. | |
| 3 | total | Total | number, nullable | Display price BEFORE the listed extra charges. | |
| 4 | total_with_charges | Total with charges | number, nullable | The all-in amount: total plus everything Booking reports as charged on top. This is the figure that matches the app. | |
| 5 | per_night | Per night | number, nullable | Total divided by nights. | |
| 6 | before_discount | Before discount | number, nullable | Booking’s struck-through price. | |
| 7 | before_discount_with_charges | Before discount, all-in | number, nullable | The struck-through price on the same basis as total_with_charges. | |
| 8 | discount | Discount | number, nullable | Total saving against before_discount. | |
| 9 | discount_percent | Discount % | number, nullable | discount / before_discount from Booking’s own figures. Not the badge the web page shows - the point of sale and the comparator both move it. Use it to rank savings within a run. | |
| 10 | mobile_discount | Mobile discount | number, nullable | The mobile-only cut itself. | |
| 11 | without_mobile_discount | Without mobile discount | number, nullable | The same rate without the mobile-only cut - what a desktop browser shows. | |
| 12 | discounts | Discount lines | array, nullable | Every discount with its code, name and amount. | |
| 13 | extra_charges | Extra charges | object, nullable | Taxes and fees. details[] itemises each one; excludedTotal / includedTotal / unknownTotal / depositTotal roll them up. chargesCompleteness says whether the itemisation is all of it: under “incalculable” the all-in figure is a floor, not a total. inclusion and mode are two different vocabularies and both contain “conditional” - inclusion is whether the guest owes it (included, excluded, conditional, deposit; anything else becomes unknown, with Booking’s own token kept in inclusionRaw), while mode is how it is computed (percentage, incalculable, not_applicable, variable_amount, conditional, per_stay, per_person_per_stay, per_night, per_person_per_night, per_item, per_item_per_period - passed through raw). A line reading “conditional” means nothing until you know which field it came from. There is no conditional bucket and no conditionalTotal - removed 2026-10-28 after it proved empty on every surface this Actor reads: 0 of 4,934 roomList charge lines and 0 of 405 search lines. The VALUE survives where it is true - a charge line’s own inclusion still reads conditional where Booking says so - but the always-empty array and always-zero rollup are gone from every level. If such a line ever arrives it groups with excluded, which is conservative (the guest may owe it) and never with unknown, which would assert Booking said nothing. depositTotal stays, and the asymmetry is the whole reason: grouping a conditional line into excluded is true, while grouping a refundable deposit there would overstate the bill. Measured empty too - 0 of 4,934, including 615 lines from 50 vacation rentals and entire homes across five markets, the segment a damage deposit comes from - but kept, because ARB found the app really does pass DEPOSIT at a call site (queue item 149) and renders it in the checkout price summary, which is reached only after a room is selected. A deposit line, if one ever arrives, is the damage deposit - the same figure as details.damageDeposit reached by a second route (the app maps BpPriceAdditionalInfo.damageDeposit onto the DEPOSIT inclusion), not a further charge to add to it. |
Captured from a live run. Lists are cut to two entries and deep nesting is elided — the shape is real, the bulk is not.
{
"recordType": "attractionDetail",
"attractionId": "PRUyqhO4YB1B",
"name": "Kuala Lumpur Airport To Kuala Lumpur Hotels",
"slug": "pruyqho4yb1b-kuala-lumpur-airport-to-kuala-lumpur-hotels",
"url": "https://www.booking.com/attractions/my/pruyqho4yb1b-kuala-lumpur-airport-to-kuala-lumpur-hotels.html",
"bookable": true,
"category": "transfers-services",
"shortDescription": null,
"description": "Pick-Up From Kuala Lumpur International Airport By An Air-Conditioned Vehicle With English Speaking Professional Driver And Drop-Off At Kuala Lumpur City Hot...",
"currency": "EUR",
"price": 31.26,
"priceBeforeDiscount": 31.26,
"durationLabel": null,
"durationValue": null,
"whatsIncluded": [
"All Taxes, Fees And Handling Charges",
"Pick-Up From Kuala Lumpur Airport Drop-Off At Kuala Lumpur City",
"... 3 more"
],
"notIncluded": [
"Food And Drinks, Unless Specified",
"Pick-Up Between 11pm To 6am 50% Surcharge Will Be Apply / Charges Pay To Driver Upon Drop-Off"
],
"uniqueSellingPoints": null,
"restrictions": null,
"additionalInfo": "Suitable for all physical fitness levels\n\n\n\nPick-Up From Kuala Lumpur International Airport (Klia1 / Klia2)\n\nNot wheelchair accessible\n\nBring your ticket wit...",
"highlights": [
null,
null
],
"labels": null,
"accessibility": [
"Accessible to strollers",
"Infants must sit on an adult's lap"
],
"healthSafety": null,
"dietOptions": null,
"guideLanguages": null,
"audioGuideLanguages": null,
"reviewScore": null,
"reviewCount": null,
"reviewCountOnPlatform": null,
"reviewCategories": null,
"reviewTotal": 0,
"freeCancellation": false,
"stillRefundable": false,
"cancellationLabel": null,
"cancellationUntil": null,
"offers": [
{
"offerId": "OF9idvreQ6n9",
"availabilityType": "date_time"
}
],
"operatedBy": "RIYAS TRAVEL BLOG",
"poweredBy": "Powered by Viator",
"ufi": -2403013,
"city": "Petaling Jaya",
"country": "my",
"address": null,
"addressCity": null,
"postalCode": null,
"latitude": null,
"longitude": null,
"meetingPoint": null,
"meetingInstructions": null,
"termsUrl": "https://www.viator.com/support/termsAndConditions",
"privacyPolicyUrl": "https://www.viator.com/support/privacyPolicy",
"images": [
"https://r-xx.bstatic.com/xdata/images/xphoto/2500x1600/231937516.webp?k=8f27a35c95f2b29f456e4bf663708c34979f16de88d75e11e9f6c2eda58288dc&o=",
"https://r-xx.bstatic.com/xdata/images/xphoto/2500x1600/231937585.webp?k=2e5e77ecb1bf6fb9176403f1041a8bafa2a675e07255b1a69e744fa7b1bcd843&o=",
"... 7 more"
]
}