DEV Community

Cover image for Agoda hotel prices and a per-date price calendar for any city (Python + n8n)
Tedj MEABIOU
Tedj MEABIOU

Posted on

Agoda hotel prices and a per-date price calendar for any city (Python + n8n)

Agoda's results page does not arrive priced: it fires a search, then re-polls until each property has a rate — which is why a browser-driven agoda scraper scrolls, waits and still misses the last poll. The same call takes a flag that makes it answer complete. This is how to read agoda prices for a whole destination on exact dates, with a per-date price calendar for the properties you follow.

1. One request, one row per property

The Agoda Prices Scraper on Apify takes destinations, property URLs, ids or names, plus a stay (checkIn, nights, adults, children, childAges, rooms, currency), and returns one hotel row per property.

curl -X POST "https://api.apify.com/v2/acts/kestrel~agoda-prices-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN&timeout=300" \
  -H "Content-Type: application/json" \
  -d '{"locationQueries": ["Lisbon"], "checkIn": "30 days", "nights": 2, "adults": 2, "currency": "EUR", "sortBy": "price_low", "maxHotelsPerQuery": 100}'
Enter fullscreen mode Exit fullscreen mode

A hotel row:

{ "type": "hotel", "mode": "destination", "hotel_id": "13505436", "hotel_name": "Help Yourself Hostels - Restelo",
  "url": "https://www.agoda.com/help-yourself-hostels-restelo/hotel/all/lisbon-pt.html", "property_type": "Hotel",
  "stars": 2.0, "review_score": 6.8, "review_count": 120, "agoda_score": 6.5,
  "city": "Lisbon", "area": "Belem", "country_code": "PT",
  "check_in": "2026-09-28", "check_out": "2026-09-29", "nights": 1, "adults": 2, "rooms": 1, "currency": "USD",
  "price_nightly": 44.71, "price_nightly_with_taxes": 60.67, "price_total": 44.71, "price_total_with_taxes": 60.67,
  "crossed_out_price": null, "available_rooms": 1, "cancellation": "FreeCancellation", "free_cancellation": true,
  "breakfast_included": false, "sold_out": false, "rank": 46, "sort": "price_low" }
Enter fullscreen mode Exit fullscreen mode

price_nightly is the cheapest rate per room per night before taxes — Agoda's display price — and price_total_with_taxes is what the booking would cost: the pair a hotel price comparison needs. crossed_out_price is the struck-through rate when a discount is shown, cancellation and breakfast_included say whether two rates are comparable at all, and available_rooms is what is left.

2. Why one call is enough

The results page calls a citySearch operation on Agoda's /graphql/search. The actor sends the same request with synchronous: true — the flag the site's own client sets once it stops polling — so one answer carries 45 priced properties, already structured, and paging is the next 45. Named properties use Agoda's "extra property" slot: a small answer each, no page read. Destinations, URLs and names resolve through Agoda's autocomplete; a property reached twice is delivered once.

Two behaviours matter. A property Agoda lists but cannot price for your stay is not a zero: it arrives with sold_out: true and empty prices — free, and only if you ask with includeSoldOut. And a throttled session is never an empty destination — the proxy session rotates and retries, and only if every attempt is refused does the input become an error.

3. Filters that run before billing

maxPrice, minStars, minReviewScore and freeCancellationOnly run on the actor before the charge call, so a property a filter removes is never billed.

{ "locationQueries": ["Lisbon", "Porto"], "checkIn": "6 weeks", "nights": 3, "adults": 2, "children": 1, "childAges": ["7"],
  "rooms": 1, "currency": "EUR", "sortBy": "price_low", "maxHotelsPerQuery": 300, "maxPrice": 90,
  "minReviewScore": 8, "freeCancellationOnly": true, "includeSoldOut": true }
Enter fullscreen mode Exit fullscreen mode

That is a standing sweep for well-reviewed, free-cancellation stays under €90 in two cities. It reads the pages either way but bills only what it hands you, so a weekend when nothing qualifies costs nothing — the free status row still reports hotels, filtered, sold_out_hotels and pages.

4. Python: a city's ladder, then the price calendar

import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
def rows(inp, kind="hotel"): return [r for r in client.dataset(client.actor("kestrel/agoda-prices-scraper").call(run_input=inp)["defaultDatasetId"]).iterate_items() if r["type"] == kind]

city = rows({"locationQueries": ["Lisbon"], "checkIn": "6 weeks", "nights": 2, "adults": 2, "currency": "EUR", "sortBy": "price_low", "maxHotelsPerQuery": 200})
for r in city[:10]: print(f'{r["price_nightly"]:7.2f} {r["price_total_with_taxes"]:8.2f}  {r["review_score"]}  {r["hotel_name"]}')

cal = rows({"hotelIds": ["6988894", "63820"], "checkIn": "2026-10-10", "nights": 3, "adults": 2, "currency": "EUR", "calendarDays": 60}, kind="calendar")
for r in sorted(cal, key=lambda r: r["price_nightly"])[:5]: print(r["hotel_name"], r["check_in"], r["price_nightly"], r["trend"], r["cheapest_in_window"])
Enter fullscreen mode Exit fullscreen mode

The first run is a hotel rate scraper reading a destination's price ladder for one stay, cheapest first, taxed total beside display price. The second asks two named properties how the same three-night stay moves by check-in date: one calendar row per date, with Agoda's trend flag (Low, Normal, High), rank_in_window and cheapest_in_window. That calendar is sparse by design — only dates Agoda has a cached price for come back, so a quiet property returns a handful and a busy one most of the window. It answers "when is this cheap", not "is this bookable". For the guest side, the Agoda Reviews Scraper takes the same hotel_id.

5. Hotel price monitoring on a schedule (n8n)

checkIn takes relative dates — "30 days", "6 weeks", "tomorrow" — so a scheduled run prices the same lead time every day and rows line up on hotel_id and check_in. Copy the Google Flights template in the n8n/ folder of kestrel-actors-examples: a 07:00 Schedule trigger, an HTTP Request to the run-sync endpoint with the section 3 body and a 300 s timeout, a Code node holding yesterday's price_nightly per hotel_id, an IF on the drop, Telegram or Slack.

6. Cost and limits

  • $0.004 per priced hotel row, $0.002 per calendar day. A 100-property city sweep is $0.40; twenty named hotels with a 30-day calendar, of which about 18 dates come back priced, is $0.08 + $0.72 = $0.80 a day. Sold-out properties, status rows, duplicates and anything a filter removed are free.
  • Prices are for the party and dates you ask for; Agoda's member and app discounts are not in the row.
  • The row is the cheapest rate on the property card — one room type, one supplier — not the room grid.
  • The actor reads public, logged-out price listings — the same data Agoda publishes to search engines and metasearch partners — and stores no personal data. Using it for price comparison, market research and revenue management is a normal business use; check Agoda's terms and your local law before republishing prices commercially, keep the pacing sensible, and do not use the data to interfere with Agoda's service.

That is the feed: one synchronous call for a destination's agoda hotel prices, a sparse calendar for the properties you follow. Full reference on the actor page: apify.com/kestrel/agoda-prices-scraper.

Every actor's inputs, output fields, a sample row and its honest limits are documented at mtedj.github.io/kestrel-actors-examples, including a side-by-side comparison of every review scraper — what each source really carries and where its ceiling is.

Top comments (0)