Agoda Hotel API
Live per-room hotel rates for a specific stay, as structured JSON. Give it a destination and a check-in date and every property comes back with its full offer table, one row per bookable rate, each carrying the room type, the nightly price, the pre-discount price shown next to it, whether tax is included, and the cancellation terms for that offer alone. The cheapest rate is lifted to the top of the record so a monitor can diff one number. There is no partner application and no key to request, and the boundaries of the data are documented below rather than glossed over.
MEDIAN LAGon demand
FIELDS55
Input parameters
| PARAMETER | TYPE | REQ | DEFAULT | DESCRIPTION |
|---|---|---|---|---|
| mode | enum | yes | search | search finds properties with live rates from a destination and dates, and needs no URL. property collects full detail from hotel URLs but returns no rate table. reviews collects guest reviews from hotel URLs. |
| locations | string[] | no | — | Cities, regions or landmarks to search, for example "Singapore" or "Bangkok". Required in search mode, ignored in the other two. Up to 20 per run. |
| checkIn | string | no | — | Stay start date as YYYY-MM-DD. Required in search mode and validated before anything leaves the Actor, because a room rate only exists for a specific stay. |
| checkOut | string | no | — | Stay end date as YYYY-MM-DD, and must be later than check-in. Required in search mode, but see the tip below, the source does not always price the length of stay you asked for. |
| adults | integer | no | 2 | Number of adults, 1 to 20. Occupancy changes which offers exist and what they cost, so this is a real filter and not a cosmetic one. |
| currency | string | no | — | Three-letter currency code for the rates, for example USD or CNY. Leave empty for the source default. Pin it whenever two runs need to be comparable. |
| country | string | no | — | Two-letter point-of-sale country code. Rates differ by market, so leaving it empty makes a parity check meaningless. |
| hotelUrls | string[] | no | — | Agoda property URLs. Required in property and reviews modes, ignored in search. Up to 200 per run. |
| maxResultsPerInput | integer | no | 20 | Properties per destination in search mode, or reviews per hotel in reviews mode, 1 to 2000. The cap is applied at the source rather than by discarding rows you already paid for, so it is a real ceiling on spend. — drives your bill |
| sortReviewsBy | string | no | — | Sort order passed through to the source in reviews mode, for example "Most recent". Tested with "Most recent" and the returned order did not change, so treat it as best effort and leave it empty unless you have verified it for your own case. |
Output schema
| FIELD | TYPE | DESCRIPTION | NULLABLE |
|---|---|---|---|
| result_type | string | property, review or error. Every row is one of the three, so an input that found nothing is visible in the data instead of silently absent. Filter on this first. | no |
| propertyId | string | Stable identifier for the property. This is the key to diff two runs on. | no |
| propertyName | string | Hotel or property name. | no |
| propertyUrl | string | Link to the property page, with the stay parameters the source used still attached to the query string. | no |
| location | string | Full street address as published. | yes |
| city | string | City the property is in. | yes |
| country | string | Country the property is in, as a name rather than a code. | yes |
| latitude | number | Property latitude, for mapping and radius work. | yes |
| longitude | number | Property longitude. | yes |
| description | string | The property's own marketing description, several paragraphs joined into one string. | yes |
| checkIn | string | Stay start date the returned rates are priced for, in ISO form. Matches what you asked for. | yes |
| checkOut | string | Stay end date the source actually priced. In live testing this came back as check-in plus eight nights regardless of the requested check-out, so read it off the row rather than assuming your input. | yes |
| available | boolean | Whether the property has rooms for the stay. A false here is a checked and genuinely sold-out property, which is its own signal. | yes |
| lowestPricePerNight | number | Cheapest nightly rate across every room and every offer, lifted to the top level so a rate monitor diffs one number instead of walking the table. | yes |
| currency | string | Currency the rates are quoted in, taken from the offers rather than echoed from your input. | yes |
| roomRates | object[] | One entry per bookable offer, flattened out of the source's nested room and offer tree. Each carries roomType, bedConfiguration, adults, children, pricePerNight, originalPricePerNight, currency, taxesAndFeesIncluded, nights and policies. A single property routinely returns dozens to hundreds of offers and still counts as one billable result. | no |
| roomsAvailable | object[] | Room types on offer with roomType, roomSize and beds, plus view where the source publishes one. This is the room catalogue, not the price list. | yes |
| reviewScore | number | Overall guest score on the source's 10-point scale. | yes |
| reviewCount | integer | How many guest reviews back that score. | yes |
| reviewScoreBreakdown | object | Scores split by cleanliness, comfort, location, facilities, staff and value_for_money. | yes |
| topReviews | string[] | Highlighted guest reviews shown on the property page, seven to ten of them in live testing, as free text with no author, score or date attached. For structured review rows, use reviews mode. | yes |
| propertyHighlights | string[] | Short marketing phrases the source uses to describe what the property is known for. | yes |
| popularFacilities | string[] | The facilities guests most often mention. Strings arrive with the source's own spacing and bracket conventions, so normalise before matching. | yes |
| amenities | object[] | Full facilities list as name and value pairs, where value is itself a list, for example a "Languages spoken" group holding five languages. Not a flat string array. | yes |
| locationRating | number | Numeric location score, parsed out of the object the source wraps it in. | yes |
| locationRatingLabel | string | Word form of the location score, for example Excellent or Exceptional. | yes |
| whatsNearby | object[] | Landmark groups as name and value pairs, for example "Popular landmarks" holding ten attraction names. | yes |
| walkablePlaces | object[] | Places within walking distance as name and value pairs, where value is a distance in kilometres delivered as a string. | yes |
| metroAccess | array | Nearby rail and metro stations. In live testing this came back as the placeholder [false] on every property, so do not build a transit feature on it. | yes |
| images | string[] | Property photo URLs, several hundred on a large hotel, at multiple sizes of the same image. | yes |
| goodToKnow | array | Practical notes such as check-in and check-out times. Frequently absent, and absent on every row of both live test runs, so treat it as a bonus rather than a column. | yes |
| finePrint | string | Published as booking conditions, but in live testing it carried only the source's straight-line-distance map disclaimer. Do not read it as cancellation terms, those live in roomRates policies. | yes |
| faq | object[] | Questions and answers from the property page, as name and value pairs. Some question strings still contain an unsubstituted [hotel_name] token, so clean before display. | yes |
| reviewId | string | Stable identifier for a review. Review rows only. | yes |
| propertyLocation | string | Where the reviewed property is. Review rows only. | yes |
| propertyType | string | Property category, for example Hotel. Review rows only. | yes |
| propertyStarRating | number | Official star rating of the reviewed property. Review rows only. | yes |
| propertyReviewScore | number | The reviewed property's overall guest score, repeated on every review row. | yes |
| propertyReviewCount | integer | How many reviews the property has. Review rows only. | yes |
| propertyRatingLabel | string | Word form of the property's score. Review rows only. | yes |
| propertyScoreBreakdown | object | The property's category scores, repeated on every review row. | yes |
| rating | number | This guest's own score on the 10-point scale. Review rows only. | yes |
| reviewPositive | string | What the guest liked, kept separate because the source splits it. Populates per property rather than universally, so expect gaps. | yes |
| reviewNegative | string | What the guest disliked. Measured across five probe runs it never populated, so plan for a one-sided corpus rather than a balanced one. | yes |
| reviewDate | string | When the review was posted. Review rows only. | yes |
| reviewLanguage | string | Language the review was written in. Review rows only. | yes |
| authorName | string | Display name of the reviewer. Review rows only. | yes |
| nightsStayed | integer | How many nights the guest stayed, useful for weighting a score. Review rows only. | yes |
| ownerReply | string | The property's reply to this review. Populates per property rather than universally. | yes |
| reviewImages | string[] | Photos the guest attached to the review. | yes |
| summary | string | One-line plain-language summary of the row, so an agent can read a record without post-processing it. | no |
| sourceInput | string | The destination or URL an error row relates to. Error rows only. | yes |
| error_message | string | Why an input produced no results, in plain language. Error rows only. | yes |
| error_type | string | Machine-readable error category, for example CollectionError. Error rows only. | yes |
| fetched_at | string | UTC timestamp when the row was collected. This is what turns repeated runs into a price time series. | no |
Worked examples
{
"mode": "search",
"locations": ["Singapore"],
"checkIn": "2027-06-15",
"checkOut": "2027-06-18",
"adults": 2,
"currency": "USD",
"country": "SG",
"maxResultsPerInput": 3
}
{
"mode": "search",
"locations": ["Singapore"],
"checkIn": "2027-06-15",
"checkOut": "2027-06-18",
"maxResultsPerInput": 2
}
{
"mode": "search",
"locations": ["Bangkok"],
"checkIn": "2027-02-09",
"checkOut": "2027-02-12",
"adults": 2,
"currency": "USD",
"country": "US",
"maxResultsPerInput": 3
}
{
"mode": "search",
"locations": ["Bali"],
"checkIn": "2027-07-05",
"checkOut": "2027-07-08",
"adults": 2,
"currency": "USD",
"country": "US",
"maxResultsPerInput": 3
}
{
"mode": "search",
"locations": ["New York"],
"checkIn": "2027-03-16",
"checkOut": "2027-03-19",
"adults": 2,
"currency": "USD",
"country": "US",
"maxResultsPerInput": 3
}
{
"mode": "search",
"locations": ["Osaka"],
"checkIn": "2027-05-20",
"checkOut": "2027-05-23",
"adults": 2,
"currency": "CNY",
"country": "CN",
"maxResultsPerInput": 3
}
{
"mode": "reviews",
"hotelUrls": [
"https://www.agoda.com/v-hotel-lavender/hotel/singapore-sg.html"
],
"maxResultsPerInput": 5
}
{
"mode": "property",
"hotelUrls": [
"https://www.agoda.com/v-hotel-lavender/hotel/singapore-sg.html"
]
}
POWER-USER TIP
Trust checkOut on the row, not the one you sent — check-in is honoured exactly. The check-out is not. Two live runs, a three-night Singapore stay and a two-night Osaka stay, both came back with checkOut set to check-in plus eight days and nights of 8 on every single offer, while the property URL still carried the requested dates. So pricePerNight is a real nightly figure for the stay the source priced, which may be longer than the one you asked for. Read checkOut and roomRates[].nights off each row, and compare like with like by holding your input constant across runs rather than assuming the length of stay came through.
POWER-USER TIP
Empty fields are dropped, not nulled — A field with nothing behind it upstream is removed from the row entirely rather than emitted as null. In the two live test runs goodToKnow was absent from every row and faq was present on one property and missing from another. Read with .get() and a default, and never assume a fixed column set from one sample row. The upside is that a present key always means real data.
POWER-USER TIP
One property is one charge, however many offers it holds — The offer table is free. A single Osaka property returned 300 bookable offers in one row and billed as one result at $0.0038. That makes maxResultsPerInput the only number that moves your bill, and it makes a deep-offer property the cheapest data on the platform per row of usable rate detail.
POWER-USER TIP
Free cancellation is a string match inside policies — There is no refundable flag. Each offer's policies array carries phrases such as "Cancel for free before September 12, 2026", "No payment until September 10, 2026" or "Non-refundable (Low price!)". Match on "Cancel for free" to keep refundable stock, and note the date in the string is the real deadline, so it is worth parsing rather than discarding.
POWER-USER TIP
The two prices on an offer are the whole point — pricePerNight is what you would pay and originalPricePerNight is the pre-discount figure displayed beside it. Both sit on every offer, so discount depth is a subtraction rather than a scrape of marketing copy. In one live row two offers on the same room type shared an original of 323.51 while the non-refundable rate was 91.17 and the free-cancellation rate was 107.26, which prices flexibility at about 16 a night on that room.
POWER-USER TIP
Reviews mode is slower, thinner and one-sided — Each property's review history is paged through, so a reviews run takes minutes rather than the minute a search takes. Across five probe runs reviewNegative never populated, and reviewPositive and ownerReply populate per property rather than universally. sortReviewsBy is passed through but the returned order did not change when it was tested with "Most recent". Budget for a positive-skewed corpus and verify the sort yourself before you depend on it.
POWER-USER TIP
Property mode gives you detail, never a rate table — A hotel URL carries no stay, and a rate only exists for a stay, so property mode returns the property record with no offers in it. Every rate workflow uses search mode. The normal chain is search first, take propertyUrl from the results, then run reviews mode against those URLs.
POWER-USER TIP
Volume changes the per-result price — The per-result fee runs from $0.004 on the free tier down to $0.00343 at gold, with $0.0038 in between, and properties and reviews are billed at the same rate. The cap in maxResultsPerInput is applied at the source rather than by throwing away rows you already paid for, so it is the first setting worth getting right.
Coverage
3
modes — search · property · reviews
20
destinations per run — search mode
200
hotel URLs per run — property and reviews modes
2,000
results per input — hard cap, applied at the source
3
named dataset views — rates · properties · reviews
6
guest score axes — cleanliness · comfort · location · facilities · staff · value for money
Tasks
Saved runs with the inputs already filled in — each one a standalone page and a working configuration example.
Code
curl
curl -X POST "https://api.apify.com/v2/acts/johnvc~agoda-hotel-api/run-sync-get-dataset-items" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mode":"search","locations":["Singapore"],"checkIn":"2027-06-15","checkOut":"2027-06-18","currency":"USD","country":"SG","maxResultsPerInput":3}'
Python
from datetime import date, timedelta
from apify_client import ApifyClient
client = ApifyClient("APIFY_TOKEN")
# Compute the stay from today so a scheduled job never asks for a date in the past.
check_in = date.today() + timedelta(days=45)
check_out = check_in + timedelta(days=3)
run = client.actor("johnvc/agoda-hotel-api").call(
run_input={
"mode": "search",
"locations": ["Singapore"],
"checkIn": check_in.isoformat(),
"checkOut": check_out.isoformat(),
"adults": 2,
"currency": "USD",
"country": "SG",
"maxResultsPerInput": 20,
}
)
for row in client.dataset(run.default_dataset_id).iterate_items():
if row.get("result_type") != "property":
continue
print(row["propertyName"], row.get("lowestPricePerNight"), row.get("checkOut"))
Refundable offers only
# There is no refundable flag, so match the policy text on each offer.
refundable = [
{
"property": row["propertyName"],
"room": offer.get("roomType"),
"price": offer.get("pricePerNight"),
"was": offer.get("originalPricePerNight"),
"policy": offer.get("policies"),
}
for row in items
if row.get("result_type") == "property"
for offer in row.get("roomRates", [])
if any("Cancel for free" in p for p in offer.get("policies", []))
]
Room rates dataset view
# Property, city, stay, lowest nightly rate, availability and the full offer table. curl -H "Authorization: Bearer $APIFY_TOKEN" \ "https://api.apify.com/v2/datasets/DATASET_ID/items?view=rates"
Changelog
2026-08-09
0.0.16 Listing rebuilt around the room-rate table, with eight saved example runs covering rates, free cancellation, parity checks, reviews and two Chinese-language use cases.
2026-08-08
0.0.13 Every dataset field coerced to its declared schema type, so a row with an unexpected upstream shape lands instead of failing the push. Published to the Store the same day.
2026-08-06
0.0.1 First build. Search, property and reviews modes, dates validated before anything leaves the Actor, and an uncapped run budget handled rather than crashed.
What people use it for
- Hotel rate monitoring for a specific stay
- Hotel rate parity checks against a direct channel
- Finding refundable and free-cancellation hotel rates
- Measuring how deep an advertised hotel discount really is
- Guest review sentiment analysis for a property
- Live hotel rates inside an AI agent or MCP client
Alternatives
Google Hotels API — prices aggregated across booking sites, which is the other half of a parity checkours
Tripadvisor API — a second read on a property's reviews rather than on its ratesours
Google Flights API — the airfare half of the same trip, on the same scheduled monitoring patternours
Google Maps Places Scraper - Fast & Cheap — what is actually around a property, which a listing page never tells you honestlyours
knagymate/fast-agoda-reviews-scraper on the same marketplace — reviews only and dearer at $0.005 a review, but with 400 users and roughly 146,000 runs in the last 30 days at a 99.5% success rate; for reviews alone it is far more proven than this onecompeting
datawebot/agoda-hotel-scraper on the same marketplace — $0.004 a hotel flat with no volume tiers, so it is simpler to reason about and dearer at scalecompeting
knagymate/fast-agoda-scraper on the same marketplace — an older listing at $0.0045 a row whose 30-day run success rate on the Store sits near 56%, so budget for empty runscompeting