kestrel actors docs

kestrel actors › Guest & customer reviews

HRS Reviews Scraper — Hotel Guest Reviews & Ratings

Every commented guest rating of any hotel on HRS (hrs.com), Europe's business-travel booking platform — hrs hotel reviews and hrs ratings as clean rows: the 0-10 score with twelve category votes, what the guest liked and disliked, the hotel's replies, traveller type and age group, stay dates, nights and the language of the comment — plus one free row per property with its averages by traveller type and by category. Give the HRS reviews scraper hotel URLs or hotel ids and it returns the hotel guest reviews as a dataset you can export to CSV, Excel or JSON, feed to n8n or Make, or hand to an AI agent over MCP. No login, no browser, no HRS API key.

Run HRS Reviews Scraper on Apify →

HRS reviews scraper pricing: what one row costs

EventWhat it isPriceCharged for
reviewReview row$0.003One commented guest rating: 0-10 score on both scales with twelve category votes, positives, negatives, the hotel's replies, traveller type, age group, stay dates and language. Properties with no commented ratings are free.

$0.003 per review row delivered, and nothing else: hotel rows, status rows, unknown hotels and filtered reviews are free, and an empty run costs $0. A busy city hotel has 50-150 commented ratings, so the full corpus of one property is $0.15-$0.45; a daily complaints feed over 20 properties that surfaces a handful of new low ratings costs a few cents a day. Proxy traffic is small (about 100 KB of JSON per hotel, plus the page for the hotel row) and is paid through your Apify plan. Set maxReviewsPerHotel and the filters to keep bulk work predictable — the status row tells you how much a hotel would have delivered.

HRS reviews scraper input reference

Every field of HRS Reviews, straight from the actor's input schema.

FieldTypeDefaultWhat it does
startUrlsarray[]HRS hotel pages, e.g. https://www.hrs.com/en/hotel/10520 — any language prefix works; the number is the hotel id.
hotelIdsarray[]HRS numeric hotel ids, e.g. 10520 — the number after /hotel/ in any hrs.com hotel URL. Faster than URLs for long lists; up to 20 ids go into one request.
maxReviewsPerHotelinteger2000 = every commented rating the hotel has (HRS returns the whole list in one call; a busy hotel has a few hundred). N = only the first N in the chosen order — the cost control. Range 0-20000.
reviewsSortstring (enum)"most_recent"Which reviews come first before the cap applies. "Lowest rated" is the fastest way to a complaints feed; "Most recent" suits a schedule. One of: most_recent, oldest, highest_rated, lowest_rated.
travellerTypesarray[]Leave empty for every guest. Otherwise keep only these HRS traveller types: BUSINESS, PRIVATE, YOUNG_COUPLE, MATURE_COUPLE, FAMILY_SMALL_CHILDREN, FAMILY_OLDER_CHILDREN, GROUP, CONFERENCE. Filtered reviews are never billed.
languagesarray[]Leave empty for every language. Otherwise keep only comments written in these languages, as codes (de, en, fr, it, es, nl, pl, pt, ru, zh, tr, cs — or the 3-letter deu, eng, …) or names (German, English…). Filtered reviews are never billed.
maxRatingnumber00 = keep every review. 1-10 on the HRS scale (7 is a middling stay, below 6 is a bad one) = keep only reviews at or below it. Runs before billing, so a complaints feed pays for the complaints only. Range 0-10.
requireTextbooleanfalseHRS's list already contains only ratings with a written positive or negative, so this rarely removes anything; it guarantees every billed row carries text.
languagestring (enum)"en"Language of the hrs.com page read for the hotel row (name, address, stars). Review comments keep the language their author wrote in. One of: en, de, fr, it, es, nl, pl, pt, ru, zh_cn, tr, cs.
includeHotelRowbooleantrueAlso emit one free row per hotel with name, stars, address, overall rating, rating count, recommendation rate, and the averages by traveller type and by category — and put the hotel name on every review row. Off = one request fewer per hotel, but no names.
sessionsinteger4How many proxy sessions (egress IPs) run in parallel. More is faster; each is paced separately. Range 1-16.
perIpnumber1Pace for each session. HRS answered 10 of 10 at 1/s on one residential IP in testing. Range 0.1-3.
proxyConfigurationobject{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}Apify Proxy RESIDENTIAL is required: both hrs.com and its ratings API answer 403 to datacenter IPs (Akamai and CloudFront). Residential traffic is small — one JSON call per 20 hotels plus one page per hotel for the hotel row.

Example input

{
  "hotelIds": ["10520", "162"],
  "reviewsSort": "lowest_rated",
  "maxRating": 7,
  "maxReviewsPerHotel": 50,
  "proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}
{
  "startUrls": ["https://www.hrs.com/de/hotel/162"],
  "travellerTypes": ["BUSINESS"],
  "languages": ["de"],
  "language": "de",
  "maxReviewsPerHotel": 0,
  "proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}

HRS reviews scraper output fields

One row per guest review (charged), plus a free hotel context row and a free status row per property.

Dataset views in the Apify Console: Overview, Reviews, Complaints, Hotels, Status.

FieldTypeWhat it holds
typestringreview | hotel | status
hotel_idstringHRS hotel id (the number in the hotel URL)
hotel_namestringHotel name from the hotel page (empty when includeHotelRow is off)
urlstringHotel page URL
review_idstringStable id of the rating (HRS booking id)
ratingnumberGuest score 0-10
rating_5numberSame score on the 5-star scale
recommendsbooleanGuest recommends the hotel
comfortstringHRS comfort word, e.g. NICE
positivesstringWhat the guest liked
negativesstringWhat the guest disliked
textstringPositives and negatives joined
hotel_replystringThe hotel's replies
traveller_typestringBUSINESS, PRIVATE, YOUNG_COUPLE, MATURE_COUPLE, FAMILY_SMALL_CHILDREN, FAMILY_OLDER_CHILDREN, GROUP, CONFERENCE
age_groupstringUP_TO_59 or OVER_59
languagestringISO 639-2 code of the comment (deu, eng, …)
localestringTwo-letter site locale the guest used
reviewer_namestringGuest display name as HRS shows it
arrival_datestringCheck-in date
departure_datestringCheck-out date
nightsintegerLength of stay
review_datestringWhen the rating was written (ISO 8601)
votesobjectTwelve category votes 0-10: FRIENDLINESS_OF_RECEPTION, SERVICE_OF_HOTEL_EMPLOYEES, HOTEL_AMBIANCE, ROOM_CONFIGURATION, HOTEL_TIDINESS, SANITARY_FACILITIES, ROOM_SIZE, BREAKFAST_SERVICE, RESTAURANT_SERVICE, PRICE_PERFORMANCE_RATIO, WELLNESS_RANKING, QUALITY_OF_BEDS
starsintegerHRS star category
addressstringStreet address
postal_codestringPostal code
citystringCity
rating_countintegerAll ratings HRS counts for the property, commented or not
comment_countintegerRatings with a written comment (what can be delivered)
recommendation_pctnumberShare of guests who recommend the hotel
ratings_by_traveller_typeobjectPer traveller type: count, rating, recommendation_pct
avg_by_categoryobjectAverage 0-10 per category
avg_by_super_categoryobjectAverage per super category (HOTEL_GENERAL_ASPECTS, ROOM, STAFF_AND_GASTRONOMY)
reviews_fetchedintegerCommented ratings read for this hotel
targetstringThe input this status row is for
statusstringok | no_reviews | not_found | duplicate | error
reviewsintegerReview rows delivered
filteredintegerReviews dropped by a filter (never billed)
totalintegerHRS's rating count for the hotel
commentsintegerCommented ratings available
duplicatesinteger1 when this input repeats a hotel already harvested
errorstringWhy the hotel failed
fetched_atstringUTC timestamp of the row

Example row from a real run

Taken from the actor's README — this is the shape your dataset holds.

{
  "type": "review",
  "review_id": "315653282",
  "hotel_id": "10520",
  "hotel_name": "Bilderberg Bellevue Hotel Dresden",
  "url": "https://www.hrs.com/en/hotel/10520",
  "rating": 9.36,
  "rating_5": 4.68,
  "recommends": true,
  "comfort": "NICE",
  "positives": "Tolle Lage direkt an der Elbe und fußnah zu Altstadt",
  "negatives": null,
  "text": "Tolle Lage direkt an der Elbe und fußnah zu Altstadt",
  "hotel_reply": "Das klingt nach einer schönen Zeit in Dresden! Wir freuen uns, dass Sie bei der Gelegenheit mit uns als Ihren Gastgeber geplant haben.",
  "traveller_type": "PRIVATE",
  "age_group": "OVER_59",
  "language": "deu",
  "locale": "de",
  "reviewer_name": "Müller K.",
  "arrival_date": "2025-12-08",
  "departure_date": "2025-12-11",
  "nights": 3,
  "review_date": "2025-12-11T14:28:35Z",
  "votes": { "FRIENDLINESS_OF_RECEPTION": 10, "SERVICE_OF_HOTEL_EMPLOYEES": 10, "HOTEL_AMBIANCE": 9, "ROOM_CONFIGURATION": 10, "HOTEL_TIDINESS": 10, "SANITARY_FACILITIES": 8, "ROOM_SIZE": 10, "BREAKFAST_SERVICE": 9, "RESTAURANT_SERVICE": 0, "PRICE_PERFORMANCE_RATIO": 9, "WELLNESS_RANKING": 8, "QUALITY_OF_BEDS": 10 },
  "fetched_at": "2026-08-29T10:20:11+00:00"
}

HRS reviews scraper in Python, JavaScript and curl

Python

from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("kestrel/hrs-reviews-scraper").call(run_input={
    "hotelIds": ["10520", "162"],
    "reviewsSort": "lowest_rated",
    "maxRating": 7,
    "maxReviewsPerHotel": 50,
    "proxyConfiguration": {"useApifyProxy": True, "apifyProxyGroups": ["RESIDENTIAL"]},
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if row["type"] == "review":
        print(row["hotel_name"], row["rating"], row["traveller_type"], (row["negatives"] or "")[:80])

JavaScript (Node)

import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('kestrel/hrs-reviews-scraper').call({
    startUrls: ['https://www.hrs.com/en/hotel/10520'],
    maxReviewsPerHotel: 100,
    proxyConfiguration: { useApifyProxy: true, apifyProxyGroups: ['RESIDENTIAL'] },
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const reviews = items.filter(r => r.type === 'review');
console.log(reviews.length, 'reviews;', reviews.filter(r => r.hotel_reply).length, 'answered by the hotel');

curl

curl -X POST "https://api.apify.com/v2/acts/kestrel~hrs-reviews-scraper/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>&timeout=300" \
  -H "Content-Type: application/json" \
  -d '{"hotelIds": ["10520"], "maxReviewsPerHotel": 30, "reviewsSort": "most_recent", "proxyConfiguration": {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}}'

The same three calls are runnable files in the examples repository: examples/hrs-reviews-scraper.py|.js|.sh.

Limits and honest notes

  • Only commented ratings are delivered. HRS counts every rating in rating_count (a property may show 150) but exposes the text of the ones with a written comment (comment_count, say 85). Score-only ratings are in the averages, not in the rows.
  • The whole list comes in one answer. There is no paging on HRS's side, so a very large property is one big response; maxReviewsPerHotel trims it after sorting rather than saving requests.
  • Residential proxy only. Both hrs.com and its ratings API answer 403 to datacenter IPs; the input defaults to the RESIDENTIAL group. Traffic is small.
  • Hotel names need the page. The ratings endpoint carries no name or address, so with includeHotelRow on the scraper reads one page per hotel (about 400 KB); off, hotel_name is empty and the run is one request per 20 hotels.
  • Unknown ids are free. An id HRS does not know comes back as status: not_found and costs nothing; a hotel with ratings but no comments is no_reviews.
  • Comments are in the guest's language — HRS is a German-founded platform, so most rows are german hotel reviews, then English; HRS does not translate them and neither does this scraper. language tells you which.

FAQ

Does it need an HRS API key or login?

No. The ratings endpoint the hotel page calls is public; the scraper sends the same client identifier the site's own JavaScript sends. Nothing is logged in and no session is reused.

Can I download HRS reviews as CSV or Excel?

Yes — download HRS reviews from any run, and get HRS reviews CSV files straight from the dataset. Every Apify dataset exports as CSV, Excel, JSON, XML or RSS from the Console or the API. The votes object flattens to twelve columns in the Excel export; in CSV it is a JSON string.

How do I get only HRS negative reviews?

maxRating: 7 keeps ratings at or below 7; reviewsSort: "lowest_rated" puts the worst first; maxReviewsPerHotel caps the count. Everything above 7 is filtered before billing.

What is the difference between rating_count and comment_count?

rating_count is every rating HRS has for the property, with or without a comment. comment_count is how many carry a written positive or negative — the rows this scraper can deliver. Both are on the hotel row and the status row.

Do I need the hotel id, or can I use the URL?

Either. The id is the number after /hotel/ in any hrs.com URL; the scraper reads it from the URL without loading the page. Ids are handier for long lists — twenty go into one request.

How many reviews can one hotel return?

All of its commented HRS hotel ratings — HRS returns the full list in one answer. City business hotels typically have 30-150; the status row's comments tells you the count before the cap.

Are reviews translated, and which languages come back?

They are not translated. Most comments are German or English, with French, Italian, Spanish, Dutch and others; language carries the ISO 639-2 code and languages filters on it before billing.

Does it include the hotel's replies?

Yes. HRS lets a hotel answer the positive and the negative separately; both go into hotel_reply. Counting rows with a reply is a quick measure of how attentive a property is.

Can I filter by traveller type?

Yes. travellerTypes accepts one or more of HRS's eight types. One type is asked of HRS directly (nothing else is downloaded); several are kept from the full list. The hotel row carries each type's average and recommendation rate.

Can I scrape HRS reviews without an API?

HRS reviews without API access is exactly what this does: no HRS API, no key, no browser — a scheduled Apify run and a dataset. Call it from the Apify API, n8n, Make, MCP or the Console.

What does bulk work cost?

$0.003 per review delivered. 100 hotels at 60 comments each is 6,000 rows and $18; the same 100 hotels with maxRating: 6 deliver only the bad stays and cost a fraction of that. Runs that deliver nothing cost $0.

How to use HRS Reviews Scraper

Ready-made runs you can open and start

Each one is a saved input on the Apify Store — open it, press Start, and it runs with the fields already filled in. All 163 across the suite.

Ready-made runWhat it does
HRS Negative Reviews for Any HotelHRS ratings of six or below for one Cologne conference hotel, lowest first, with the negative comment, traveller type and the hotel's reply on each row.
Download HRS Reviews to CSVA hundred and fifty newest HRS reviews of one Berlin hotel with rating, recommendation, positives, negatives, reply and traveller type, as CSV or JSON.
HRS Reviews by Hotel ID, Four Berlin HotelsFour Berlin business hotels by HRS hotel id in one run, twenty-five newest reviews each - up to twenty ids share one ratings request, no lookup step.
Business Travel Hotel Reviews: Frankfurt AirportOnly business travellers' HRS reviews of three Frankfurt airport hotels, forty per property, with each hotel's average rating by traveller type.
HRS Competitor Reviews: Cologne Comp SetFive Cologne hotels compared on HRS: rating, recommendation rate and twelve category averages per property in one table, twenty newest reviews each.
German Hotel Reviews from HRS, German Only (de)A hundred German-language HRS reviews of one Hamburg hotel, other languages dropped before billing, positives and negatives already split by the guest.
HRS Hotel Ratings, Highest Rated FirstThe highest-rated HRS reviews of one Cologne hotel, sixty of them, each with the positive comment, recommendation flag, traveller type and arrival date.
Hotel Reputation Monitoring on HRSTwenty newest HRS reviews for each of five German hotels by id, in one ratings request - small enough to schedule daily and alert on a low rating.
Corporate Travel Hotel Feedback: Conference GuestsOnly conference and group guests' HRS reviews of three German conference hotels, forty per property, with the other traveller types never billed.
HRS Reviews Without API AccessHRS has no public review API. This is the smallest replacement: one hotel URL, forty newest reviews and a free hotel row, no key and no contract needed.
Hotel Guest Reviews in Three Languages from HRSA hundred and fifty HRS reviews of one Hamburg hotel written in French, German and English, the language recorded on every row for a split by market.
Guest Feedback Data with Hotel Replies from HRSThe fifty lowest HRS ratings of one Berlin five-star hotel with the guest's complaint and the hotel's reply side by side - empty where nobody answered.

Run HRS Reviews Scraper

The actor lives on the Apify Store, where the pricing tab carries the live per-row price and the input form has every field above: apify.com/kestrel/hrs-reviews-scraper