TRAVELECOMMERCEAI API MCP READY

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

PARAMETERTYPEREQDEFAULTDESCRIPTION
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

FIELDTYPEDESCRIPTIONNULLABLE
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

Rates for a destination and stay — the shape almost everyone starts from, and the published "no API key" example
{
  "mode": "search",
  "locations": ["Singapore"],
  "checkIn": "2027-06-15",
  "checkOut": "2027-06-18",
  "adults": 2,
  "currency": "USD",
  "country": "SG",
  "maxResultsPerInput": 3
}
Cheap first run — two properties is enough to see the whole schema and costs under a cent
{
  "mode": "search",
  "locations": ["Singapore"],
  "checkIn": "2027-06-15",
  "checkOut": "2027-06-18",
  "maxResultsPerInput": 2
}
Price tracking on a schedule — fixed stay, same input every morning, diff lowestPricePerNight between runs
{
  "mode": "search",
  "locations": ["Bangkok"],
  "checkIn": "2027-02-09",
  "checkOut": "2027-02-12",
  "adults": 2,
  "currency": "USD",
  "country": "US",
  "maxResultsPerInput": 3
}
Free cancellation only — policies come through per offer, so refundable rates are a filter and not a guess
{
  "mode": "search",
  "locations": ["Bali"],
  "checkIn": "2027-07-05",
  "checkOut": "2027-07-08",
  "adults": 2,
  "currency": "USD",
  "country": "US",
  "maxResultsPerInput": 3
}
Rate parity check — currency and point of sale pinned, so the comparison against another channel is like for like
{
  "mode": "search",
  "locations": ["New York"],
  "checkIn": "2027-03-16",
  "checkOut": "2027-03-19",
  "adults": 2,
  "currency": "USD",
  "country": "US",
  "maxResultsPerInput": 3
}
Another market, another currency — same stay logic priced in CNY from a Chinese point of sale
{
  "mode": "search",
  "locations": ["Osaka"],
  "checkIn": "2027-05-20",
  "checkOut": "2027-05-23",
  "adults": 2,
  "currency": "CNY",
  "country": "CN",
  "maxResultsPerInput": 3
}
Guest reviews for one hotel — one row per review, ready for CSV or a sentiment pass
{
  "mode": "reviews",
  "hotelUrls": [
    "https://www.agoda.com/v-hotel-lavender/hotel/singapore-sg.html"
  ],
  "maxResultsPerInput": 5
}
Property detail without rates — URLs carry no stay, so this returns the property record and no offer table
{
  "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.

Request a task →

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

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