# HolidayCheck Reviews Scraper - German Hotel Ratings & Trends (`kamerozkan/holidaycheck-review-intelligence`) Actor

Scrape German hotel reviews from HolidayCheck: guest reviews & ratings, verified-stay flags, hotel review sentiment by aspect, monthly trends, anomaly alerts and hotel reputation analytics for DACH hotels.

- **URL**: https://apify.com/kamerozkan/holidaycheck-review-intelligence.md
- **Developed by:** [Kamer Ozkan](https://apify.com/kamerozkan) (community)
- **Categories:** Travel
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.80 / 1,000 review results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js/docs.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python/docs.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## HolidayCheck Reviews Scraper - Ratings, Aspects & Trends

Version 0.4.1 is a stateful, HTTP-first Apify Actor for collecting HolidayCheck hotel reviews and turning them into analytics-ready data, management reports, action alerts, health checks, and price-aware competitor intelligence.

This is an unofficial, independent Actor. It is not affiliated with, endorsed by, or supported by HolidayCheck.

The Actor is deployed privately on Apify, has passed controlled cloud runs, and has its pay-per-event launch pricing configured. Public Store activation remains gated by documentary confirmation of the lawful commercial basis, acceptance of the current Apify Store publishing terms by the account holder, and completion of the public listing identity assets.

### What V4 includes

- Multiple HolidayCheck hotels in one run
- `all`, `limit`, and `since` collection modes
- Page-by-page streaming through temporary Actor storage
- A persistent checkpoint after every successful page
- Optional cross-run deduplication by HolidayCheck review ID
- Native ratings normalized to a 1-10 display scale
- Eight deterministic aspect scores from HolidayCheck's native fields
- Optional full-text AI aspect sentiment with exact source evidence
- Native + AI hybrid aspect scores
- Persistent monthly aspect trends and three-month rolling averages across scheduled runs
- Observable anomaly signals, including writing patterns, near duplicates, rating runs, and historical burst z-scores
- Optional full-text English translation
- Shared AI request/token budgets and content-addressed caching
- Automatic same-city-or-region and star-compatible competitor sets
- Optional live comparable price bands using identical dates, occupancy, offer type, currency, and filters
- Strict same-price-band selection when live price intelligence is enabled
- English or German management reports in JSON, self-contained HTML, and plain text
- Explainable decline, burst, duplicate-pattern, price-position, collection, and AI alerts
- Optional signed HTTPS webhook delivery for email and messaging workflows
- Machine-readable production health checks
- Pay-per-event delivery gates for reviews, competitor sets, price intelligence, and reports
- User spending-limit handling that turns finite review capacity into resumable collection work
- Owner responses, optional raw contribution blocks, retries, proxy support, and per-hotel error isolation
- Reviewer data minimization by default

### Collection modes

`collectionMode` controls pagination:

- `all`: ignores `maxReviewsPerHotel` and follows every publicly pageable result until the last or an empty page.
- `limit`: returns at most `maxReviewsPerHotel`. The ending checkpoint remains incomplete, so a later run can continue at the next page.
- `since`: requires `cutoffDate` and `sort: "entrydate"`. It stops when the newest-first stream reaches an older review.

HolidayCheck can report a total review count larger than the number of reviews exposed through public pagination. `all` means every review that the current public interface actually makes pageable; the Actor does not claim access to private or non-indexed rows.

Each successfully processed page updates the named state store. Set:

- `resumeFromCheckpoint: true` to continue an incomplete run.
- `deduplicateAcrossRuns: true` to suppress IDs already emitted in earlier runs.
- `resetState: true` to clear state for the input hotels before the run.
- `stateStoreName` to isolate a customer, environment, or schedule.

When a limited run ends in the middle of a HolidayCheck page, the checkpoint deliberately revisits that page and skips only IDs already emitted. This prevents the remaining rows on that page from being silently lost.

### Example input

```json
{
  "startUrls": [
    {
      "url": "https://www.holidaycheck.de/hr/bewertungen-pickalbatros-dana-beach-resort-hurghada/1aa4c4ad-f9ea-3367-a163-8a3a6884d450"
    }
  ],
  "collectionMode": "all",
  "sort": "entrydate",
  "resumeFromCheckpoint": true,
  "deduplicateAcrossRuns": true,
  "resetState": false,
  "stateStoreName": "holidaycheck-review-intelligence-state",
  "includeAspectScores": true,
  "includeAiAspectScores": false,
  "includeTrendIntelligence": true,
  "includeAnomalySignals": true,
  "includeReviewerDetails": false,
  "includeOwnerResponses": true,
  "includeCompetitorSet": true,
  "competitorLimit": 5,
  "competitorStarTolerance": 1,
  "includePriceIntelligence": true,
  "priceSearch": {
    "checkIn": "2026-08-23",
    "checkOut": "2026-08-30",
    "adults": 2,
    "childrenAges": [],
    "rooms": 1,
    "currency": "EUR",
    "offerType": "package",
    "departureAirports": [],
    "boardTypes": []
  },
  "priceBandTolerancePercent": 15,
  "includeManagementReport": true,
  "reportLanguage": "en",
  "alertAspectDeclineThreshold": 0.15,
  "alertMinimumReviewCount": 3,
  "notificationMode": "alerts",
  "translateToEnglish": false,
  "aiMaxRequests": 1000,
  "aiMaxInputTokens": 5000000,
  "useAiCache": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
````

`startUrls` accepts HolidayCheck `/hr/`, `/hi/`, `/ho/`, `/hm/`, `/ha/`, and contribution URLs when a hotel UUID is present.

### Output records

The default dataset contains one normalized row per review.

The default key-value store contains:

- `OUTPUT`: run summary, counters, feature usage, stop reasons, and failures
- `ASPECT_TRENDS`: monthly hotel/aspect time series
- `COMPETITOR_SETS`: target hotels, selected competitors, price comparisons, and exclusions
- `COLLECTION_STATE`: a safe checkpoint summary without the full seen-ID arrays
- `MANAGEMENT_REPORT`: portfolio-level management report and alerts
- `MANAGEMENT_REPORT.html`: responsive, print-ready, email-ready report
- `MANAGEMENT_REPORT.txt`: compact email and messaging digest
- `ALERTS`: machine-readable action alerts
- `HEALTH`: collection, price, AI, billing, and notification health checks

The named persistent state store contains the full collection state, daily anomaly baseline, exact trend-deduplication IDs, monthly aspect aggregates, and optional AI cache records.

### Management report and alerts

Set `includeManagementReport: true` to create customer-facing artifacts without requiring the customer to read dataset JSON. `reportLanguage` supports `en` and `de`.

The report contains:

- portfolio hotel and review counts;
- per-hotel average rating, recommendation rate, and verified-reservation share;
- the latest eight aspect scores, deltas, directions, and sample sizes;
- target price versus comparable competitor median;
- observable anomaly summaries;
- prioritized, evidence-backed action alerts.

`alertAspectDeclineThreshold` controls the minimum monthly score drop on the `-1` to `1` scale. `alertMinimumReviewCount` suppresses trend alerts from very small monthly samples.

Alerts do not declare reviews fake. They preserve the signal type, threshold, sample count, and supporting values for human investigation.

A real alert from a live run on 2026-07-28. The burst detector flagged 31 reviews with
an elevated historical clustering signal (peak z-score 8.2), and the alert text itself
states the boundary: it is a review hint, not a verdict about fake reviews:

```json
{
  "generatedAt": "2026-07-28T12:55:00.315Z",
  "alerts": [
    {
      "id": "d5d6b6b58445b983",
      "severity": "high",
      "type": "review_burst",
      "hotelId": "1aa4c4ad-f9ea-3367-a163-8a3a6884d450",
      "hotelName": "Pickalbatros Dana Beach Resort - Hurghada",
      "title": "Erhöhtes Signal für Rezensionshäufung",
      "message": "31 Rezensionen weisen ein erhöhtes historisches Häufungssignal auf. Dies ist ein Prüfhinweis, kein Urteil über gefälschte Bewertungen.",
      "evidence": {
        "elevatedReviewCount": 31,
        "highestZScore": 8.2
      }
    }
  ]
}
```

### Signed notification webhook

Set an HTTPS `notificationWebhookUrl` to deliver a compact JSON digest to an owned email, Slack, Teams, Make, Zapier, or internal workflow. Set `notificationWebhookSecret` to add an HMAC-SHA256 signature over the exact request body.

`notificationMode` supports:

- `alerts`: send only when the management report contains alerts;
- `always`: send after every run that reaches reporting;
- `failures`: send only when `HEALTH.status` is `failed`.

Use Apify platform lifecycle webhooks as well. They cover failures or timeouts that happen before the Actor reaches its own reporting phase.

This Actor is also exposed to AI agents through Apify's MCP server
(mcp.apify.com): an agent can discover it by search and run it with the same
pay-per-event billing, with no separate integration.

### Pay-per-event billing

The code emits these custom value events:

- `review-result`
- `competitor-set`
- `price-intelligence-set`
- `management-report`

The event names and configured launch prices are documented in `docs/APIFY_MONETIZATION.md`. The Apify Actor uses `review-result` as its primary event and does not configure `apify-default-dataset-item`, because enabling both would bill the same delivered row twice.

`OUTPUT.billing` records the active pricing model, advertised per-event prices, delivered event counts, and spending-limit status.

#### Review row

```json
{
  "reviewId": "1ca1c304-88a6-4d19-9eba-b2fdae7eb503",
  "hotelId": "1aa4c4ad-f9ea-3367-a163-8a3a6884d450",
  "hotelName": "Pickalbatros Dana Beach Resort - Hurghada",
  "reviewUrl": "https://www.holidaycheck.de/hr/1aa4c4ad-f9ea-3367-a163-8a3a6884d450#hotel-review-1ca1c304-88a6-4d19-9eba-b2fdae7eb503",
  "title": "Gutes Hotel mit freundlichem Personal und schönem Pool",
  "text": "(full review text in German, shortened here)",
  "overallRating": 8.2,
  "recommendation": true,
  "verifiedReservation": true,
  "entryDate": "2026-07-28T09:22:28.000Z",
  "travelDate": "2026-07",
  "travelerType": "COUPLE",
  "originalLanguage": "de",
  "returnedLanguage": "de",
  "hotel": {
    "id": "1aa4c4ad-f9ea-3367-a163-8a3a6884d450",
    "name": "Pickalbatros Dana Beach Resort - Hurghada",
    "stars": 5,
    "starSource": null,
    "rating6": 5.657160703601454,
    "recommendationRate": 96.11,
    "reviewCount": 28225,
    "city": "Hurghada",
    "region": "Hurghada/Safaga",
    "country": "Ägypten",
    "longitude": 33.83149,
    "latitude": 27.096745
  },
  "reviewer": {
    "hotelReviewCount": 1
  },
  "ownerResponse": null,
  "topics": [],
  "nativeRatings": {
    "ROOM_SIZE": {
      "label": "Zimmergröße",
      "sourceScore": 0.8,
      "rating10": 8.2
    },
    "POOL_LOUNGER_RACE": {
      "label": "Verfügbarkeit der Liegen",
      "sourceScore": 1,
      "rating10": 10
    },
    "RATING": {
      "label": "Bewertung",
      "sourceScore": 0.8,
      "rating10": 8.2
    },
    "note": "10 more rated categories, shortened here"
  },
  "aspectScores": {
    "room": {
      "score": 0.733,
      "label": "positive",
      "confidence": "high",
      "signalCount": 3,
      "sourceSignals": [
        {
          "source": "rating",
          "key": "ROOM_SIZE",
          "score": 0.6
        },
        {
          "source": "rating",
          "key": "SLEEP",
          "score": 1
        },
        {
          "source": "rating",
          "key": "ROOM_CLEANLINESS",
          "score": 0.6
        }
      ],
      "evidence": []
    },
    "note": "7 more aspect dimensions, shortened here"
  },
  "aiAspectScores": null,
  "aiAspectError": null,
  "hybridAspectScores": "(hybrid aspect scores for the same dimensions, shortened here)",
  "anomalySignals": null,
  "translation": null,
  "competitorSetRef": null,
  "rawContributions": null,
  "sourceUrl": "https://www.holidaycheck.de/hr/bewertungen-pickalbatros-dana-beach-resort-hurghada/1aa4c4ad-f9ea-3367-a163-8a3a6884d450",
  "scrapedAt": "2026-07-28T10:14:10.261Z"
}
```

#### What the rows guarantee

The row above is a real verified stay from a live run on 2026-07-28; only free-text and
repeated structures are shortened. Alongside it, the same run delivered this critical
review:

```json
{
  "reviewId": "ee20b26c-d6b7-4272-aad5-ff3d3ac61940",
  "hotelId": "1aa4c4ad-f9ea-3367-a163-8a3a6884d450",
  "hotelName": "Pickalbatros Dana Beach Resort - Hurghada",
  "reviewUrl": "https://www.holidaycheck.de/hr/1aa4c4ad-f9ea-3367-a163-8a3a6884d450#hotel-review-ee20b26c-d6b7-4272-aad5-ff3d3ac61940",
  "title": "Service nach wie vor Top, ...",
  "text": "(full review text in German, shortened here)",
  "overallRating": 6.4,
  "recommendation": false,
  "verifiedReservation": true,
  "entryDate": "2026-02-18T09:25:16.000Z",
  "travelDate": "2026-02",
  "travelerType": "COUPLE",
  "originalLanguage": "de",
  "returnedLanguage": "de",
  "hotel": {
    "id": "1aa4c4ad-f9ea-3367-a163-8a3a6884d450",
    "name": "Pickalbatros Dana Beach Resort - Hurghada",
    "stars": 5,
    "starSource": null,
    "rating6": 5.657160703601454,
    "recommendationRate": 96.11,
    "reviewCount": 28225,
    "city": "Hurghada",
    "region": "Hurghada/Safaga",
    "country": "Ägypten",
    "longitude": 33.83149,
    "latitude": 27.096745
  },
  "reviewer": {
    "hotelReviewCount": 116
  },
  "ownerResponse": null,
  "topics": [
    {
      "id": "EVENING_PROGRAM",
      "name": "Abendprogramm",
      "sentiment": -1,
      "quotes": [
        "Hier kann man deutlich mehr machen.",
        "Die Shows waren gefühlt fast täglich Tanzshows.",
        "Das ist echt stinklangweilig, weil jeden Tag das Gleiche.",
        "Die Bauchtanzshow war unterstes Niveau.",
        "Die Abendshows fanden immer gegen 20.30 nach der Mini Disco stat."
      ]
    },
    {
      "id": "ENTERTAINMENT",
      "name": "Unterhaltung",
      "sentiment": -1,
      "quotes": [
        "Einmal angeschaut, es gab soweit ich mich erinnere 5 Gewinne...alles Schrottpreise.",
        "Der Höhepunkt für die Animation war es dann einmal wöchentlich Lose für eine Riesen Tombola zu verkaufen."
      ]
    },
    "(16 more topics, shortened here)"
  ],
  "nativeRatings": {
    "ROOM_SIZE": {
      "label": "Zimmergröße",
      "sourceScore": 1,
      "rating10": 10
    },
    "RATING": {
      "label": "Bewertung",
      "sourceScore": 0.6,
      "rating10": 6.4
    },
    "FOOD_VARIETY": {
      "label": "Essensauswahl",
      "sourceScore": 0.4,
      "rating10": 4.6
    },
    "note": "7 more rated categories, shortened here"
  },
  "aspectScores": {
    "room": {
      "score": 0.7,
      "label": "positive",
      "confidence": "high",
      "signalCount": 6,
      "sourceSignals": [
        {
          "source": "rating",
          "key": "ROOM_SIZE",
          "score": 1
        },
        {
          "source": "rating",
          "key": "SLEEP",
          "score": 0.2
        },
        {
          "source": "rating",
          "key": "ROOM_CLEANLINESS",
          "score": 1
        },
        {
          "source": "topic",
          "key": "ROOM",
          "score": 1
        },
        {
          "source": "topic",
          "key": "ROOM_CLEANLINESS",
          "score": 1
        },
        {
          "source": "topic",
          "key": "SLEEP",
          "score": 0
        }
      ],
      "evidence": [
        "Wir hatten auf Grund mehrerer Aufenthalte in dem Hotel ein upgrade auf ein Superiorzimmer im 2000er Block erhalten. \nGroß, sauber, riesen Dusche, Matratzen ungewohnt weich.",
        "Aber da man ja wieder dankbar ist (darum geht es mir echt nicht) haben wir ein Upgrade für ein Superior Zimmer beim nächsten Urlaub bekommen.",
        "Groß, sauber, riesen Dusche, Matratzen ungewohnt weich."
      ]
    },
    "note": "7 more aspect dimensions, shortened here"
  },
  "aiAspectScores": null,
  "aiAspectError": null,
  "hybridAspectScores": "(hybrid aspect scores for the same dimensions, shortened here)",
  "anomalySignals": null,
  "translation": null,
  "competitorSetRef": null,
  "rawContributions": null,
  "sourceUrl": "https://www.holidaycheck.de/hr/bewertungen-pickalbatros-dana-beach-resort-hurghada/1aa4c4ad-f9ea-3367-a163-8a3a6884d450",
  "scrapedAt": "2026-07-28T10:15:56.087Z"
}
```

- **No cherry-picking.** Critical reviews arrive with `recommendation: false` and their
  full rating breakdown, exactly like the positive ones.
- **Verification is reported, never implied.** `verifiedReservation` is true only when
  HolidayCheck confirmed the stay; unverified reviews say so instead of hiding it.
- **Transparent normalization.** `nativeRatings` keeps the raw `sourceScore` next to the
  derived `rating10`, so every normalized number can be recomputed from source.
- **Reviewer privacy by design.** The `reviewer` object carries activity counts only;
  no names, avatars, or profile links are collected.

### Eight aspect dimensions

Every aspect uses a `-1` to `1` sentiment scale:

1. `room`
2. `cleanliness`
3. `service`
4. `foodAndBeverage`
5. `location`
6. `amenities`
7. `value`
8. `familyAndEntertainment`

`aspectScores` are deterministic. They map HolidayCheck rating contributions, topics, and quotes to stable dimensions. Each result includes confidence, source signals, and evidence.

When `includeAiAspectScores` is enabled, the Actor also analyzes the full title and review text. AI output uses a strict schema. A non-null AI score is retained only if at least one evidence string is an exact substring of the source review; unsupported or hallucinated evidence forces that aspect back to `null`.

`hybridAspectScores` is the final view:

- native-only when only native evidence exists
- AI-only when only validated AI evidence exists
- weighted hybrid when both exist
- unavailable when neither exists

The implementation uses the OpenAI Responses API with Structured Outputs:

https://developers.openai.com/api/docs/guides/structured-outputs

### Monthly aspect trends

`ASPECT_TRENDS.points` contains one row per hotel, month, and aspect:

- `averageScore`
- `reviewCount`
- `availableScoreCount`
- `positiveShare`
- `previousMonthDelta`
- `rollingThreeMonthAverage`
- `direction`: `improving`, `stable`, `declining`, or `unavailable`

Direction changes only when the month-over-month score delta is at least `0.1` in magnitude.

Monthly aggregates persist in the named state store. An incremental weekly run therefore retains earlier months and does not recalculate “since March” trends from only the newest batch. Review IDs in the trend state prevent repeated schedules from double-counting a row.

### Observable anomaly signals

The Actor does not label a review as fake. It returns source-level and sample-level evidence so the customer can make its own decision.

`anomalySignals.signalVersion` is `observable-v2` and includes:

- displayed verified-reservation status
- displayed reviewer hotel-review count
- single-review-profile flag
- account-age value, availability status, and source
- exact duplicate count in the run
- near-duplicate count and nearest Jaccard similarity
- same-day review count
- same-rating run length
- loaded hotel sample size
- 90-day burst baseline, mean, standard deviation, z-score, and status
- writing-pattern metrics:
  - characters, words, and sentences
  - average word and sentence length
  - unique and repeated word ratios
  - uppercase and punctuation ratios
  - exclamation and emoji counts
  - lexical entropy

HolidayCheck's current public review rows expose reviewer history depth but do not consistently expose a verifiable account creation date. In that case the Actor returns:

```json
{
  "accountAgeDays": null,
  "accountAgeStatus": "unavailable",
  "accountAgeSource": null
}
```

The field is never guessed from first-review dates or private assumptions.

The burst signal uses prior daily counts from the persistent state store plus other days in the current run. It remains `insufficient_history` until at least seven non-zero historical days are available.

### Competitor and price intelligence

The automatic competitor pipeline:

1. Deduplicates HolidayCheck alternatives from nearby, city, region, and country buckets.
2. Keeps candidates in the same city or region when that context is available.
3. Applies the configured star-class tolerance.
4. Scores location, star similarity, distance, rating similarity, and review-volume similarity.
5. Retains human-readable `selectionReasons` and source buckets.

Without live prices, the top similarity matches are selected.

With `includePriceIntelligence: true`, the Actor fetches the target and a wider candidate pool with one identical search context. It stores:

- complete `targetPrice`
- normalized total, nightly, and per-person prices
- check-in/out, nights, occupancy, rooms, offer type, and currency
- board, room, cancellation, provider, and offer ID when exposed
- target price band, market median, and comparable count
- each competitor's `below`, `same_band`, `above`, or `unavailable` relation

Only `same_band` competitors remain in `competitors`; rejected or unavailable candidates remain auditable in `excludedCompetitors`. The percentage boundary is configured with `priceBandTolerancePercent`.

An unavailable or rejected offer stays unavailable. The Actor never fabricates, carries forward, or currency-converts a price.

### English translation and AI cost controls

Set `translateToEnglish: true` and supply either the secret `openaiApiKey` input or the `OPENAI_API_KEY` environment variable.

The translation pipeline:

- preserves all original fields
- translates title, full review text, and owner response
- skips rows already marked English
- batches calls and validates that every requested review ID returns exactly once
- disables OpenAI response storage with `store: false`
- sends only the required text fields, never reviewer-profile fields
- can isolate failed batches and keep the source row

AI aspects and translation share:

- `aiMaxRequests`
- `aiMaxInputTokens`
- `useAiCache`

The cache key includes feature version, model, and a SHA-256 hash of the relevant source content. `OUTPUT.ai` reports API calls, cache hits, tokens, failures, and budget-blocked batches.

The default model is `gpt-5.6-luna`, the lower-cost extraction/classification role in the GPT-5.6 family. The integration uses the Responses API, Structured Outputs, explicit `reasoning.effort: "none"`, and `store: false`. Both AI features are disabled by default, and API usage is billed separately from the Actor run.

### Failure and data semantics

- A URL can fail independently when `ignoreUrlErrors` is true.
- Page requests use bounded retries and configurable timeouts.
- Price failures are row-level availability states, not fabricated zeroes.
- AI failures can be isolated per batch.
- Duplicate suppression uses HolidayCheck review IDs.
- Temporary raw datasets are dropped after successful processing.
- Originals are always preserved when AI features are enabled.

### Verification

The V4 implementation is covered by 32 automated tests across 15 test files:

- URL and input validation
- Fluxible-state parsing
- native aspect normalization
- AI evidence validation, hybrid merging, cache, and budget limits
- English translation and source-language skipping
- anomaly writing, duplicate, near-duplicate, burst, and accumulator behavior
- checkpoint resume, `all`, `limit`, and `since` collection modes
- trend calculations
- persistent cross-run trend aggregates and trend-level review-ID deduplication
- competitor selection
- comparable price-band selection
- persistent baseline merging
- in-page checkpoint continuation without lost rows
- pay-per-event limits and event accounting
- management report generation and HTML escaping
- run-health classification
- notification mode, delivery, and HMAC signing

A separate 30,000-review stress validation exercises streaming anomaly and trend accumulation:

```bash
npm run test:stress
```

Local validation:

```bash
npm ci
npm run check
npm test
npm run test:stress
npm run test:live
npm run build
npm run preflight
```

`npm run test:openai` performs a real paid Structured Outputs smoke test only when `OPENAI_API_KEY` is present. `npm run preflight -- --strict` is the public Store release gate. Docker and a live OpenAI key are reported as optional validation capabilities; the strict gate remains closed until the explicit commercial-permission confirmation and Apify Store terms acceptance are present.

The included GitHub workflows run the deterministic CI suite and a weekly public-source contract smoke test. Operational setup is documented in `docs/OPERATIONS.md`.

Apify cloud build, controlled collection, and pay-per-event launch validation have completed successfully. Local Docker remains an optional secondary validation path.

### Compliance

HolidayCheck states that its published content and data are legally protected, prohibits various forms of reuse including commercial exploitation, and provides `info@holidaycheck.de` for permission requests:

https://www.holidaycheck.de/haftungsausschluss

Use this Actor only when you have a lawful basis and every permission required for the intended use. Respect applicable terms, copyright and database rights, privacy law, request rates, and retention rules. This project does not provide legal advice and does not bypass authentication, CAPTCHAs, paywalls, or access controls.

# Actor input Schema

## `startUrls` (type: `array`):

Direct HolidayCheck hotel, review, offer, media, or contribution URLs. Multiple hotels are supported. Billing is per delivered review row; with cross-run deduplication enabled, already-seen reviews are not re-emitted and not re-billed.

## `collectionMode` (type: `string`):

All ignores the row limit and walks every available page. Limit returns at most the configured count. Since stops at the date boundary.

## `maxReviewsPerHotel` (type: `integer`):

Used only in Limit mode. A limited run leaves a resumable checkpoint.

## `cutoffDate` (type: `string`):

Required in Since mode. Use YYYY-MM-DD and Newest sorting.

## `sort` (type: `string`):

Since mode requires Newest sorting.

## `resumeFromCheckpoint` (type: `boolean`):

Continue an interrupted or limited collection at its next saved page.

## `deduplicateAcrossRuns` (type: `boolean`):

Uses the named state store to emit only newly observed review IDs.

## `resetState` (type: `boolean`):

Clears checkpoints, seen IDs, daily anomaly history, and persistent monthly trend aggregates for the input hotels before collecting. Content-addressed AI cache records are retained.

## `stateStoreName` (type: `string`):

Named key-value store used for checkpoints, seen review IDs, anomaly baselines, and AI cache.

## `includeAspectScores` (type: `boolean`):

Create eight deterministic scores from HolidayCheck rating and topic fields.

## `includeAiAspectScores` (type: `boolean`):

Score the same eight aspects from full text with exact-substring evidence and combine them with native signals. Requires an OpenAI API key and may incur charges.

## `aiAspectModel` (type: `string`):

OpenAI model used for evidence-validated full-text aspect analysis.

## `aiAspectBatchSize` (type: `integer`):

Maximum number of review texts included in one AI aspect request.

## `aiAspectMaxConcurrency` (type: `integer`):

Maximum number of AI aspect requests processed at the same time.

## `ignoreAiAspectErrors` (type: `boolean`):

Keep rows with unavailable AI scores and an error field instead of failing the run.

## `includeTrendIntelligence` (type: `boolean`):

Persist and store monthly averages, positive share, month-over-month delta, three-month rolling average, and direction in ASPECT\_TRENDS.

## `includeAnomalySignals` (type: `boolean`):

Adds writing-pattern metrics, exact/near duplicates, rating runs, reviewer history, and historical burst z-scores. These fields are not fake-review verdicts.

## `includeReviewerDetails` (type: `boolean`):

Include displayed name, age group, profile ID, and public-profile flag. Disabled by default for data minimization.

## `includeOwnerResponses` (type: `boolean`):

Keep publicly displayed hotel owner responses in the normalized review row.

## `includeRawContributions` (type: `boolean`):

Keep source contribution blocks for audit or custom downstream processing. Disabled by default to reduce output size.

## `includeCompetitorSet` (type: `boolean`):

Rank nearby and location-based alternatives with transparent similarity and source reasons.

## `competitorLimit` (type: `integer`):

Maximum number of selected comparable competitors returned for each target hotel.

## `competitorStarTolerance` (type: `integer`):

Maximum absolute difference in star class allowed between target and competitor.

## `includePriceIntelligence` (type: `boolean`):

Fetch a best available offer for the target and every competitor using the exact same dates, occupancy, currency, offer type, and optional board filters.

## `priceSearch` (type: `object`):

Required when price intelligence is enabled.

## `priceBandTolerancePercent` (type: `integer`):

A competitor within plus or minus this percentage of the target nightly price is in the same band.

## `includeManagementReport` (type: `boolean`):

Creates JSON and email-ready HTML portfolio reports plus explainable action alerts.

## `reportLanguage` (type: `string`):

Language used for the generated HTML, JSON labels, text digest, and action alerts.

## `alertAspectDeclineThreshold` (type: `number`):

Create an alert when a monthly aspect score falls by at least this amount on the -1 to 1 scale.

## `alertMinimumReviewCount` (type: `integer`):

Suppress aspect decline alerts based on smaller monthly samples.

## `notificationWebhookUrl` (type: `string`):

Optional HTTPS endpoint for a signed JSON digest. Connect it to your own email, Slack, Teams, Make, or Zapier workflow.

## `notificationWebhookSecret` (type: `string`):

Optional HMAC-SHA256 secret. When set, the request includes X-HolidayCheck-Signature.

## `notificationMode` (type: `string`):

Choose whether the signed webhook is sent for alerts, every completed report, or failed health states.

## `ignoreNotificationErrors` (type: `boolean`):

Keep the Actor run successful and expose the delivery failure in health output instead of throwing.

## `translateToEnglish` (type: `boolean`):

Translate title, full review, and owner response while preserving originals. Requires an OpenAI API key and may incur charges.

## `openaiApiKey` (type: `string`):

Encrypted secret used for AI aspects and/or translation. OPENAI\_API\_KEY may be used instead.

## `translationModel` (type: `string`):

OpenAI model used to translate review content into English.

## `translationBatchSize` (type: `integer`):

Maximum number of reviews included in one translation request.

## `translationMaxConcurrency` (type: `integer`):

Maximum number of translation requests processed at the same time.

## `ignoreTranslationErrors` (type: `boolean`):

Keep original review rows and expose the translation error instead of failing the run.

## `aiMaxRequests` (type: `integer`):

Shared safety budget across AI aspect and translation requests.

## `aiMaxInputTokens` (type: `integer`):

Shared preflight input-token safety budget across AI features.

## `useAiCache` (type: `boolean`):

Caches by feature version, model, and source-content hash.

## `maxConcurrency` (type: `integer`):

Maximum number of target hotels collected at the same time.

## `maxRequestRetries` (type: `integer`):

Maximum retry count for a failed HolidayCheck HTTP request.

## `requestTimeoutSecs` (type: `integer`):

Maximum time to wait for one HolidayCheck HTTP request before retrying or failing it.

## `ignoreUrlErrors` (type: `boolean`):

Isolate a failed hotel URL, record its error, and continue processing the remaining hotels.

## `proxyConfiguration` (type: `object`):

Apify Proxy is recommended for larger recurring collections.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.holidaycheck.de/hr/bewertungen-pickalbatros-dana-beach-resort-hurghada/1aa4c4ad-f9ea-3367-a163-8a3a6884d450"
    }
  ],
  "collectionMode": "limit",
  "maxReviewsPerHotel": 100,
  "sort": "entrydate",
  "resumeFromCheckpoint": true,
  "deduplicateAcrossRuns": false,
  "resetState": false,
  "stateStoreName": "holidaycheck-review-intelligence-state",
  "includeAspectScores": true,
  "includeAiAspectScores": false,
  "aiAspectModel": "gpt-5.6-luna",
  "aiAspectBatchSize": 10,
  "aiAspectMaxConcurrency": 2,
  "ignoreAiAspectErrors": true,
  "includeTrendIntelligence": true,
  "includeAnomalySignals": true,
  "includeReviewerDetails": false,
  "includeOwnerResponses": true,
  "includeRawContributions": false,
  "includeCompetitorSet": true,
  "competitorLimit": 5,
  "competitorStarTolerance": 1,
  "includePriceIntelligence": false,
  "priceBandTolerancePercent": 15,
  "includeManagementReport": true,
  "reportLanguage": "en",
  "alertAspectDeclineThreshold": 0.15,
  "alertMinimumReviewCount": 3,
  "notificationMode": "alerts",
  "ignoreNotificationErrors": true,
  "translateToEnglish": false,
  "translationModel": "gpt-5.6-luna",
  "translationBatchSize": 10,
  "translationMaxConcurrency": 2,
  "ignoreTranslationErrors": true,
  "aiMaxRequests": 1000,
  "aiMaxInputTokens": 5000000,
  "useAiCache": true,
  "maxConcurrency": 3,
  "maxRequestRetries": 3,
  "requestTimeoutSecs": 30,
  "ignoreUrlErrors": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `runSummary` (type: `string`):

No description

## `competitorSets` (type: `string`):

No description

## `aspectTrends` (type: `string`):

No description

## `collectionState` (type: `string`):

No description

## `managementReport` (type: `string`):

No description

## `managementReportHtml` (type: `string`):

No description

## `alerts` (type: `string`):

No description

## `health` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("kamerozkan/holidaycheck-review-intelligence").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("kamerozkan/holidaycheck-review-intelligence").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call kamerozkan/holidaycheck-review-intelligence --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=kamerozkan/holidaycheck-review-intelligence",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "HolidayCheck Reviews Scraper - German Hotel Ratings & Trends",
        "description": "Scrape German hotel reviews from HolidayCheck: guest reviews & ratings, verified-stay flags, hotel review sentiment by aspect, monthly trends, anomaly alerts and hotel reputation analytics for DACH hotels.",
        "version": "0.4",
        "x-build-id": "gRieJtxE53b39jygx"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/kamerozkan~holidaycheck-review-intelligence/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-kamerozkan-holidaycheck-review-intelligence",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/kamerozkan~holidaycheck-review-intelligence/runs": {
            "post": {
                "operationId": "runs-sync-kamerozkan-holidaycheck-review-intelligence",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/kamerozkan~holidaycheck-review-intelligence/run-sync": {
            "post": {
                "operationId": "run-sync-kamerozkan-holidaycheck-review-intelligence",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "startUrls"
                ],
                "properties": {
                    "startUrls": {
                        "title": "HolidayCheck hotel or review URLs",
                        "type": "array",
                        "description": "Direct HolidayCheck hotel, review, offer, media, or contribution URLs. Multiple hotels are supported. Billing is per delivered review row; with cross-run deduplication enabled, already-seen reviews are not re-emitted and not re-billed.",
                        "default": [
                            {
                                "url": "https://www.holidaycheck.de/hr/bewertungen-pickalbatros-dana-beach-resort-hurghada/1aa4c4ad-f9ea-3367-a163-8a3a6884d450"
                            }
                        ],
                        "items": {
                            "type": "object",
                            "required": [
                                "url"
                            ],
                            "properties": {
                                "url": {
                                    "type": "string",
                                    "title": "URL of a web page",
                                    "format": "uri"
                                }
                            }
                        }
                    },
                    "collectionMode": {
                        "title": "Collection mode",
                        "enum": [
                            "all",
                            "limit",
                            "since"
                        ],
                        "type": "string",
                        "description": "All ignores the row limit and walks every available page. Limit returns at most the configured count. Since stops at the date boundary.",
                        "default": "limit"
                    },
                    "maxReviewsPerHotel": {
                        "title": "Maximum reviews per hotel",
                        "minimum": 1,
                        "maximum": 10000,
                        "type": "integer",
                        "description": "Used only in Limit mode. A limited run leaves a resumable checkpoint.",
                        "default": 100
                    },
                    "cutoffDate": {
                        "title": "Only reviews on or after",
                        "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                        "type": "string",
                        "description": "Required in Since mode. Use YYYY-MM-DD and Newest sorting."
                    },
                    "sort": {
                        "title": "Review sorting",
                        "enum": [
                            "entrydate",
                            "ranking",
                            "positive",
                            "negative"
                        ],
                        "type": "string",
                        "description": "Since mode requires Newest sorting.",
                        "default": "entrydate"
                    },
                    "resumeFromCheckpoint": {
                        "title": "Resume incomplete collections",
                        "type": "boolean",
                        "description": "Continue an interrupted or limited collection at its next saved page.",
                        "default": true
                    },
                    "deduplicateAcrossRuns": {
                        "title": "Skip review IDs seen in earlier runs",
                        "type": "boolean",
                        "description": "Uses the named state store to emit only newly observed review IDs.",
                        "default": false
                    },
                    "resetState": {
                        "title": "Reset saved state for these hotels",
                        "type": "boolean",
                        "description": "Clears checkpoints, seen IDs, daily anomaly history, and persistent monthly trend aggregates for the input hotels before collecting. Content-addressed AI cache records are retained.",
                        "default": false
                    },
                    "stateStoreName": {
                        "title": "Persistent state store name",
                        "type": "string",
                        "description": "Named key-value store used for checkpoints, seen review IDs, anomaly baselines, and AI cache.",
                        "default": "holidaycheck-review-intelligence-state"
                    },
                    "includeAspectScores": {
                        "title": "Include native aspect scores",
                        "type": "boolean",
                        "description": "Create eight deterministic scores from HolidayCheck rating and topic fields.",
                        "default": true
                    },
                    "includeAiAspectScores": {
                        "title": "Analyze full review text with AI",
                        "type": "boolean",
                        "description": "Score the same eight aspects from full text with exact-substring evidence and combine them with native signals. Requires an OpenAI API key and may incur charges.",
                        "default": false
                    },
                    "aiAspectModel": {
                        "title": "AI aspect model",
                        "type": "string",
                        "description": "OpenAI model used for evidence-validated full-text aspect analysis.",
                        "default": "gpt-5.6-luna"
                    },
                    "aiAspectBatchSize": {
                        "title": "Reviews per aspect request",
                        "minimum": 1,
                        "maximum": 25,
                        "type": "integer",
                        "description": "Maximum number of review texts included in one AI aspect request.",
                        "default": 10
                    },
                    "aiAspectMaxConcurrency": {
                        "title": "Aspect-request concurrency",
                        "minimum": 1,
                        "maximum": 5,
                        "type": "integer",
                        "description": "Maximum number of AI aspect requests processed at the same time.",
                        "default": 2
                    },
                    "ignoreAiAspectErrors": {
                        "title": "Continue when AI aspect analysis fails",
                        "type": "boolean",
                        "description": "Keep rows with unavailable AI scores and an error field instead of failing the run.",
                        "default": true
                    },
                    "includeTrendIntelligence": {
                        "title": "Build monthly aspect trends",
                        "type": "boolean",
                        "description": "Persist and store monthly averages, positive share, month-over-month delta, three-month rolling average, and direction in ASPECT_TRENDS.",
                        "default": true
                    },
                    "includeAnomalySignals": {
                        "title": "Include observable anomaly signals",
                        "type": "boolean",
                        "description": "Adds writing-pattern metrics, exact/near duplicates, rating runs, reviewer history, and historical burst z-scores. These fields are not fake-review verdicts.",
                        "default": true
                    },
                    "includeReviewerDetails": {
                        "title": "Include reviewer profile details",
                        "type": "boolean",
                        "description": "Include displayed name, age group, profile ID, and public-profile flag. Disabled by default for data minimization.",
                        "default": false
                    },
                    "includeOwnerResponses": {
                        "title": "Include hotel owner responses",
                        "type": "boolean",
                        "description": "Keep publicly displayed hotel owner responses in the normalized review row.",
                        "default": true
                    },
                    "includeRawContributions": {
                        "title": "Include raw contribution blocks",
                        "type": "boolean",
                        "description": "Keep source contribution blocks for audit or custom downstream processing. Disabled by default to reduce output size.",
                        "default": false
                    },
                    "includeCompetitorSet": {
                        "title": "Build an automatic competitor set",
                        "type": "boolean",
                        "description": "Rank nearby and location-based alternatives with transparent similarity and source reasons.",
                        "default": true
                    },
                    "competitorLimit": {
                        "title": "Competitors per hotel",
                        "minimum": 1,
                        "maximum": 20,
                        "type": "integer",
                        "description": "Maximum number of selected comparable competitors returned for each target hotel.",
                        "default": 5
                    },
                    "competitorStarTolerance": {
                        "title": "Star-class tolerance",
                        "minimum": 0,
                        "maximum": 2,
                        "type": "integer",
                        "description": "Maximum absolute difference in star class allowed between target and competitor.",
                        "default": 1
                    },
                    "includePriceIntelligence": {
                        "title": "Add comparable live price bands",
                        "type": "boolean",
                        "description": "Fetch a best available offer for the target and every competitor using the exact same dates, occupancy, currency, offer type, and optional board filters.",
                        "default": false
                    },
                    "priceSearch": {
                        "title": "Comparable price search",
                        "type": "object",
                        "description": "Required when price intelligence is enabled.",
                        "properties": {
                            "checkIn": {
                                "title": "Check-in or package departure",
                                "type": "string",
                                "description": "Arrival date for hotel-only searches or departure date for package searches, formatted as YYYY-MM-DD.",
                                "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
                            },
                            "checkOut": {
                                "title": "Check-out or package return",
                                "type": "string",
                                "description": "Departure date for hotel-only searches or return date for package searches, formatted as YYYY-MM-DD.",
                                "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
                            },
                            "adults": {
                                "title": "Adults",
                                "type": "integer",
                                "description": "Number of adult guests used for every offer in the comparable price search.",
                                "default": 2,
                                "minimum": 1,
                                "maximum": 12
                            },
                            "childrenAges": {
                                "title": "Children's ages",
                                "type": "array",
                                "description": "One age from 0 to 17 for each child.",
                                "editor": "stringList",
                                "items": {
                                    "type": "string"
                                },
                                "default": []
                            },
                            "rooms": {
                                "title": "Rooms",
                                "type": "integer",
                                "description": "Number of rooms used for every offer in the comparable price search.",
                                "default": 1,
                                "minimum": 1,
                                "maximum": 6
                            },
                            "currency": {
                                "title": "Currency",
                                "type": "string",
                                "description": "Three-letter currency code requested for target and competitor offers.",
                                "default": "EUR",
                                "pattern": "^[A-Za-z]{3}$"
                            },
                            "offerType": {
                                "title": "Offer type",
                                "type": "string",
                                "description": "Compare hotel-only offers or package offers; all targets and competitors use the same type.",
                                "editor": "select",
                                "default": "hotelonly",
                                "enum": [
                                    "hotelonly",
                                    "package"
                                ],
                                "enumTitles": [
                                    "Hotel only",
                                    "Package"
                                ]
                            },
                            "departureAirports": {
                                "title": "Departure airport codes",
                                "type": "array",
                                "description": "Optional IATA codes for package searches. Empty means any supported airport.",
                                "editor": "stringList",
                                "items": {
                                    "type": "string"
                                },
                                "default": []
                            },
                            "boardTypes": {
                                "title": "HolidayCheck board filter codes",
                                "type": "array",
                                "description": "Optional HolidayCheck codes such as GT06-AI. Empty means any board.",
                                "editor": "stringList",
                                "items": {
                                    "type": "string"
                                },
                                "default": []
                            }
                        }
                    },
                    "priceBandTolerancePercent": {
                        "title": "Same-price-band tolerance",
                        "minimum": 1,
                        "maximum": 100,
                        "type": "integer",
                        "description": "A competitor within plus or minus this percentage of the target nightly price is in the same band.",
                        "default": 15
                    },
                    "includeManagementReport": {
                        "title": "Create a management report",
                        "type": "boolean",
                        "description": "Creates JSON and email-ready HTML portfolio reports plus explainable action alerts.",
                        "default": true
                    },
                    "reportLanguage": {
                        "title": "Management report language",
                        "enum": [
                            "en",
                            "de"
                        ],
                        "type": "string",
                        "description": "Language used for the generated HTML, JSON labels, text digest, and action alerts.",
                        "default": "en"
                    },
                    "alertAspectDeclineThreshold": {
                        "title": "Aspect decline alert threshold",
                        "minimum": 0.01,
                        "maximum": 2,
                        "type": "number",
                        "description": "Create an alert when a monthly aspect score falls by at least this amount on the -1 to 1 scale.",
                        "default": 0.15
                    },
                    "alertMinimumReviewCount": {
                        "title": "Minimum reviews for a trend alert",
                        "minimum": 1,
                        "maximum": 10000,
                        "type": "integer",
                        "description": "Suppress aspect decline alerts based on smaller monthly samples.",
                        "default": 3
                    },
                    "notificationWebhookUrl": {
                        "title": "Notification webhook URL",
                        "type": "string",
                        "description": "Optional HTTPS endpoint for a signed JSON digest. Connect it to your own email, Slack, Teams, Make, or Zapier workflow."
                    },
                    "notificationWebhookSecret": {
                        "title": "Webhook signing secret",
                        "type": "string",
                        "description": "Optional HMAC-SHA256 secret. When set, the request includes X-HolidayCheck-Signature."
                    },
                    "notificationMode": {
                        "title": "When to send a notification",
                        "enum": [
                            "alerts",
                            "always",
                            "failures"
                        ],
                        "type": "string",
                        "description": "Choose whether the signed webhook is sent for alerts, every completed report, or failed health states.",
                        "default": "alerts"
                    },
                    "ignoreNotificationErrors": {
                        "title": "Continue when notification delivery fails",
                        "type": "boolean",
                        "description": "Keep the Actor run successful and expose the delivery failure in health output instead of throwing.",
                        "default": true
                    },
                    "translateToEnglish": {
                        "title": "Translate review text to English",
                        "type": "boolean",
                        "description": "Translate title, full review, and owner response while preserving originals. Requires an OpenAI API key and may incur charges.",
                        "default": false
                    },
                    "openaiApiKey": {
                        "title": "OpenAI API key",
                        "type": "string",
                        "description": "Encrypted secret used for AI aspects and/or translation. OPENAI_API_KEY may be used instead."
                    },
                    "translationModel": {
                        "title": "Translation model",
                        "type": "string",
                        "description": "OpenAI model used to translate review content into English.",
                        "default": "gpt-5.6-luna"
                    },
                    "translationBatchSize": {
                        "title": "Reviews per translation request",
                        "minimum": 1,
                        "maximum": 25,
                        "type": "integer",
                        "description": "Maximum number of reviews included in one translation request.",
                        "default": 10
                    },
                    "translationMaxConcurrency": {
                        "title": "Translation concurrency",
                        "minimum": 1,
                        "maximum": 5,
                        "type": "integer",
                        "description": "Maximum number of translation requests processed at the same time.",
                        "default": 2
                    },
                    "ignoreTranslationErrors": {
                        "title": "Continue when translation fails",
                        "type": "boolean",
                        "description": "Keep original review rows and expose the translation error instead of failing the run.",
                        "default": true
                    },
                    "aiMaxRequests": {
                        "title": "Maximum AI requests per run",
                        "minimum": 1,
                        "maximum": 100000,
                        "type": "integer",
                        "description": "Shared safety budget across AI aspect and translation requests.",
                        "default": 1000
                    },
                    "aiMaxInputTokens": {
                        "title": "Maximum AI input tokens per run",
                        "minimum": 1000,
                        "maximum": 100000000,
                        "type": "integer",
                        "description": "Shared preflight input-token safety budget across AI features.",
                        "default": 5000000
                    },
                    "useAiCache": {
                        "title": "Reuse AI results from the state store",
                        "type": "boolean",
                        "description": "Caches by feature version, model, and source-content hash.",
                        "default": true
                    },
                    "maxConcurrency": {
                        "title": "Maximum hotel concurrency",
                        "minimum": 1,
                        "maximum": 10,
                        "type": "integer",
                        "description": "Maximum number of target hotels collected at the same time.",
                        "default": 3
                    },
                    "maxRequestRetries": {
                        "title": "Request retries",
                        "minimum": 0,
                        "maximum": 10,
                        "type": "integer",
                        "description": "Maximum retry count for a failed HolidayCheck HTTP request.",
                        "default": 3
                    },
                    "requestTimeoutSecs": {
                        "title": "Request timeout",
                        "minimum": 5,
                        "maximum": 120,
                        "type": "integer",
                        "description": "Maximum time to wait for one HolidayCheck HTTP request before retrying or failing it.",
                        "default": 30
                    },
                    "ignoreUrlErrors": {
                        "title": "Continue when a hotel fails",
                        "type": "boolean",
                        "description": "Isolate a failed hotel URL, record its error, and continue processing the remaining hotels.",
                        "default": true
                    },
                    "proxyConfiguration": {
                        "title": "Proxy configuration",
                        "type": "object",
                        "description": "Apify Proxy is recommended for larger recurring collections.",
                        "default": {
                            "useApifyProxy": false
                        }
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
