# OPTIMA Hotel Rate Scraper (`cobocus/optima-hotel-rate-scraper`) Actor

Captures direct-booking prices from any hotel or ryokan running the OPTIMA booking engine (\*.reservation.jp): selling price, pre-discount price, discount rate, member price and remaining rooms for every room x plan x future check-in date, including sold-out dates.

- **URL**: https://apify.com/cobocus/optima-hotel-rate-scraper.md
- **Developed by:** [COBOCUS](https://apify.com/cobocus) (community)
- **Categories:** Travel, E-commerce, Automation
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 date scanneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

Track the price a guest actually sees on a hotel's **own** booking site — every room, every plan, every future date.

### What does OPTIMA Hotel Rate Scraper do?

This Actor extracts **direct-booking rates from any hotel or ryokan running the OPTIMA booking engine** (booking sites on `*.reservation.jp`, by [SYS.inc](https://sys.ne.jp/)) — around 2,500 properties across Japan. Give it a property's booking-site URL and a date range, and it returns one row per **room × plan × check-in date**, with the selling price, the member price, the pre-discount price, the discount, the remaining-room count, the meal arrangement and the cancellation policy.

It reads only the **public booking pages** every visitor sees — no login, no account, no API key, and no access to the property's admin console. It does not book, hold, or cancel anything.

### Why use OPTIMA Hotel Rate Scraper?

A property's own booking engine is the one channel where it controls the price outright, and it is the channel that no OTA scraper and no channel-manager API reports back. Without it you can measure your OTA prices but not the number you are measuring them *against*.

- **Rate-parity monitoring.** Compare your direct price against the same room and date on Booking.com, Expedia, or a Japanese OTA, and see the days where parity actually broke.
- **Competitive rate shopping.** Track a competitor's published direct rate daily, room type by room type, instead of guessing from a single headline "from ¥X" figure.
- **Demand and pickup analysis.** Run it on a schedule and the dataset becomes a time series of `check-in date × capture date` — the shape you need to see how far out a date fills and how price moved as it did.
- **Sell-out tracking.** Rooms the property cannot sell on a date are recorded explicitly, not dropped, so a gap in your data always means "not collected", never "sold out".
- **Member-rate visibility.** Where a property publishes a members-only price next to the public one, both are captured separately, so a member discount is never mistaken for a public one.

On the Apify platform you also get scheduling, the API, webhooks, proxy rotation, monitoring, and export to JSON, CSV, Excel, XML or HTML — plus integrations with Google Sheets, Slack, Zapier, Make and others.

### What data can OPTIMA Hotel Rate Scraper extract?

One row per room × plan × check-in date. The fields a rate analyst actually reaches for:

| Field | Type | Description |
|---|---|---|
| `checkInDate` / `checkOutDate` | string | Stay dates, `YYYY-MM-DD`. Check-out is derived from the number of nights. |
| `capturedAt` | string | ISO 8601 timestamp of the scrape — the second axis of the time series. |
| `leadTimeDays` | number | Days between capture and check-in. The core input for pickup and lead-time curves. |
| `propertyName` / `propertyId` | string | Facility name and OPTIMA facility id. |
| `roomName` / `roomId` | string | Room type. |
| `roomAttributes` | string\[] | Smoking policy, size, capacity, bed configuration, Wi-Fi — as the page lists them. |
| `planName` / `planId` | string | Rate plan. |
| `planTags` | string\[] | Badges shown on the plan, e.g. `朝食付` (breakfast included). |
| `availability` | boolean | null | `true` bookable, `false` confirmed not bookable, **`null` not collected**. Never conflated. |
| `soldOutReason` | string | The page's own wording when nothing is bookable. |
| `remainingRoomCount` | number | null | Rooms left, when the property publishes the count. |
| `sellingPrice` | number | **What a guest booking now is charged for the whole stay**, tax and fees included. |
| `originalPrice` | number | Price before the property's discount. |
| `discountAmount` | number | `originalPrice − sellingPrice`. |
| `discountType` / `discountValue` | string / number | The discount OPTIMA applied, and its configured magnitude (e.g. `10` for 10%). |
| `memberPrice` / `memberPriceLabel` | number / string | Members-only price, where the property publishes one next to the public total. |
| `memberOnly` | boolean | The plan itself is bookable by members only. |
| `mealPlan` | string | Meal arrangement, e.g. `朝食`, `朝食・夕食`. |
| `paymentMethods` | string | e.g. `現地払い・Web決済` (pay at hotel / pay online). |
| `checkInTimeFrom` / `checkOutTimeUntil` | string | Plan's check-in window and check-out time. |
| `pointAmount` | number | Loyalty points the booking grants. |
| `minStayNights` | number | Minimum nights the plan can be booked for. |
| `cancelPolicyRulesJson` | string | The plan's cancellation fee ladder and no-show rule, as JSON. |
| `taxNote` | string | Tax/fee wording shown beside the price. |
| `validationFlags` | string\[] | Data-quality flags. Empty on a clean row. |

`currency`, `adults`, `children`, `childBands`, `rooms`, `nights`, `dayUse`, `propertyHost`, `hotelCode` and `sourceUrl` are also on every row.

#### Pricing data definitions

OPTIMA reports two prices per plan: the price before whatever discount the property configured, and the price after it. What that discount *is* differs by property, so this Actor separates the two cases rather than reporting both as the same thing:

- **No member price published.** `sellingPrice` is the discounted price, `originalPrice` is the price before the discount, and `discountAmount` is the difference — a discount every visitor gets.
- **A member price published next to the public total.** The discount is conditional on signing in. `sellingPrice` stays the **public** total (what someone booking right now, not signed in, pays), `memberPrice` carries the members-only figure, and `discountAmount` is therefore `0`. `discountType` / `discountValue` still describe the member discount.

`sellingPrice` always answers the same question — *what is a guest charged if they book this now?* — regardless of which case a property falls into.

### How to scrape OPTIMA direct-booking rates

1. Open the property's official website and click through to its booking engine. You are in the right place when the address is a `*.reservation.jp` host, for example `https://go-botanicalpoolclub.reservation.jp/ja/hotels/bpc/searchInput`.
2. Copy that URL — **any** page of the booking site works, including the search form. Paste it into **Property URLs**. Add more URLs to track several properties in one run.
3. Set **Months ahead** (default 6) to choose how far forward to scan, or set **Start date** and **End date** for an explicit range.
4. Set **Adults**, **Rooms** and **Nights** to the occupancy you want priced. This matters: a property that cannot host that many adults in any room returns nothing for every date.
5. Click **Start**. Watch the log — it prints the property name and facility id as soon as each URL resolves.
6. When the run finishes, open the **Dataset** tab to browse or export the rows, and the **Storage → Key-value store → OUTPUT** record for the run's own health report.
7. To build a time series, open the **Schedules** tab and run it daily. Set **Run summary dataset name** first (see Tips).

### How much will it cost?

This Actor uses pay per event, with two charges, because the work has two parts.

| Event | Price | Charged |
|---|---|---|
| Date scanned | **$0.003** | Once per property × check-in date — one request, however heavy the page |
| Room × plan row | **$0.0002** | Each row written: one room × plan on one date |
| Property processed | $0.01 | Once per property URL |
| Actor start | $0.006 per GB | Once per run **per GB of memory** — **$0.012** at this Actor's 2 GB default |

**The floor is arithmetic you can do before you start**: `properties × days × $0.003`, so about **$0.55**
for a single property scanned six months ahead (181 dates). Above that you pay for the rows you
actually receive, and that varies a great deal between properties — measured on two real six-month
runs, one returned 24.6 room × plan rows per date and the other 86.7:

| Property | Rows/date | Total for a six-month scan |
|---|---|---|
| Small, few plans per day | 24.6 | **$1.45** |
| Larger, many plan variants | 86.7 | **$3.70** |

Under the flat $0.015 per date this Actor was first priced at, both paid $2.74 — the small property
was subsidising the large one. Now neither does.

Two things are **not** charged: a date that could not be collected at all, and a date whose page came
back with the wrong `checkin_date` (OPTIMA answers a past date with today's inventory; this Actor
detects that and marks the date `availability: null` rather than inventing a night). Neither the date
nor its rows are billed in those cases.

### Input

Only **Property URLs** is required; every other field has a sensible default. See the Input tab for the full list. The fields worth knowing about:

- **Months ahead** — how far forward to scan from today (Japan time). Ignored when both Start date and End date are given.
- **Adults / Rooms / Nights** — the occupancy priced. Prices are for the **whole stay**, not per night.
- **Children – bands 1-5** — OPTIMA splits children into five age/meal bands whose labels each property configures itself (typically from elementary-school age down to infants needing neither meal nor bedding). Band 1 is the oldest.
- **Include sold-out dates** — on by default. Leave it on unless you only want bookable rows.
- **Day-use plans** — search day-use instead of overnight stays. Most properties offer none.
- **Proxy configuration** — Apify Proxy on the datacenter pool by default; see Tips.

### Output

Every row is one room × plan × check-in date. Export from the Dataset tab as JSON, CSV, Excel, XML or HTML, or fetch it from the API.

A bookable plan at a property with no member pricing:

```json
{
  "source": "optima",
  "propertyId": "10002664",
  "propertyName": "BOTANICAL POOL CLUB",
  "checkInDate": "2026-12-07",
  "checkOutDate": "2026-12-08",
  "nights": 1,
  "leadTimeDays": 74,
  "adults": 2,
  "rooms": 1,
  "availability": true,
  "remainingRoomCount": 2,
  "roomId": "10030723",
  "roomName": "POOL CLUB ROOM",
  "planId": "10179534",
  "planName": "【秋冬限定】朝食STANDARD PLAN｜温水プールとオールインクルーシブで過ごす、冬のリトリート",
  "mealPlan": "朝食",
  "paymentMethods": "現地払い・Web決済",
  "checkInTimeFrom": "15:00",
  "checkOutTimeUntil": "11:00",
  "currency": "JPY",
  "sellingPrice": 56160,
  "originalPrice": 56160,
  "discountAmount": 0,
  "memberPrice": null,
  "minStayNights": 1,
  "capturedAt": "2026-09-24T08:25:02.183Z",
  "validationFlags": []
}
```

A plan at a property that publishes a members-only price:

```json
{
  "propertyName": "オーセントホテル小樽",
  "checkInDate": "2026-12-05",
  "roomName": "スタンダードツイン(2名様利用)",
  "availability": true,
  "sellingPrice": 66000,
  "originalPrice": 66000,
  "discountAmount": 0,
  "discountType": "1",
  "discountValue": 10,
  "memberPrice": 59400,
  "memberPriceLabel": "会員価格",
  "taxNote": "税・サービス料込",
  "currency": "JPY"
}
```

A room the property cannot sell on that date:

```json
{
  "propertyName": "BOTANICAL POOL CLUB",
  "checkInDate": "2026-12-05",
  "roomId": "10030724",
  "roomName": "POOL CLUB ROOM（TWIN）",
  "availability": false,
  "soldOutReason": "空室なし",
  "sellingPrice": null
}
```

#### The run summary

Every run also writes one small JSON object to the key-value store as `OUTPUT`: dates requested versus collected, anything left uncollected, the date range actually present in the data, HTTP errors by status, and a single `complete` boolean. On a schedule, read that instead of re-reading thousands of rate rows to find out whether last night's run worked.

### Tips and advanced options

- **Set a run summary dataset name** before scheduling. Each run then appends its summary as one row to that named dataset, so consecutive days form a single table you can scan for a `complete: false`. Named datasets are not subject to storage retention, so the history survives even after the underlying runs expire.
- **`availability: null` is not sold out.** It means the date was not collected — the request failed, or the site served a different date than the one asked for. Filter it out of price analysis, but treat a run that produces many of them as a run to re-check.
- **Past dates cannot be scraped.** OPTIMA answers a request for a past check-in date with *today's* availability. The Actor detects the substitution and writes `availability: null` with a `DATE_MISMATCH` flag rather than recording a night that was never observed.
- **Match the occupancy to the property.** Asking for 4 adults at a property whose rooms hold 2 returns "no rooms" on every date. If a whole scan comes back sold out, check Adults and Rooms first.
- **Leave the proxy off unless you need it.** It is off by default, which is deliberate. OPTIMA is a property's own booking engine with no anti-bot measures, and it serves 0.7–2.5 MB of *uncompressed* HTML per check-in date. Measured on the Apify platform, same property and same seven dates: **211 seconds through the shared datacenter proxy against 10.2 seconds without it**, identical data and zero errors either way. Turn a proxy on if a property ever starts blocking you — nothing else about the run changes.
- **Raising Max concurrency rarely helps.** The run is bounded by the size of the pages, not by the number of requests. The default of 5 completes a six-month single-property scan in about a minute.
- **A `ROOM_LIST_TRUNCATED` flag** means the property paginates its room list and this run saw only the first page. It has not been seen on any property to date; report it via the Issues tab if you hit it.

### FAQ

**Does this need a login or an account with the property?**
No. It reads only the public booking pages, exactly as an anonymous visitor sees them.

**Why is `discountAmount` zero when the page clearly shows a discount?**
Because that discount requires signing in as a member. Look at `memberPrice` — see "Pricing data definitions" above.

**Can it scrape several properties at once?**
Yes. Add as many booking-site URLs as you like; they are scanned in the same run and each row carries its own property id.

**Does it work on a property's group site?**
Yes. Both OPTIMA site shapes are supported: group sites with a `/hotels/<code>/` segment, and single-property sites without one. Paste the URL you land on when you click "book" on the property's own website.

**Which languages does it return?**
Whatever the URL you paste asks for. A `/ja/` URL returns Japanese room and plan names; `/en/` returns the property's English copy where it has any.

**Why did a date come back with no rooms?**
Either the property is genuinely sold out, the date is past the end of its booking window, or it does not offer that occupancy. OPTIMA uses one message for all three, and it is preserved verbatim in `soldOutReason`.

### Ethical scraping, and your responsibilities

This Actor collects **only publicly available information** — the room, plan and price data any visitor sees on a property's public booking pages. It collects no personal data, no guest information, and nothing behind a login, and it makes no booking, modification or cancellation.

It is still your responsibility to use the data lawfully. Review the target property's terms of use, keep your request volume reasonable (the defaults are deliberately conservative), and make sure your use has a legitimate basis under the law that applies to you — including the GDPR if you are in the EU or handling EU data. If you plan to republish or commercially redistribute what you collect, take legal advice first. Apify's [ethical web scraping](https://blog.apify.com/what-is-ethical-web-scraping-and-how-do-you-do-it/) guide is a good starting point.

### Support

Found a bug, a property this Actor cannot read, or a field you need? Open an issue on the **Issues** tab of this Actor and include the property URL and the dates you ran. Booking sites change; issues get fixed.

# Changelog

This Actor's version history is a separate document: https://apify.com/cobocus/optima-hotel-rate-scraper/changelog.md

# Actor input Schema

## `propertyUrls` (type: `array`):

One or more OPTIMA booking-engine URLs. Paste any page of the property's own booking site - the search form, the room list, anything: https://go-botanicalpoolclub.reservation.jp/ja/hotels/bpc/searchInput. Both site shapes are supported: group sites (/ja/hotels/<code>/...) and single-property sites (/ja/...). Any number of properties.

## `monthsAhead` (type: `integer`):

Scan every day from today through this many months ahead. Ignored if both Start date and End date are set. Run this Actor daily to build a rolling 'today + N months' time series.

## `startDate` (type: `string`):

Advanced: explicit range start, YYYY-MM-DD. Leave empty to start from today in Japan (recommended for daily monitoring). Dates before today are rejected by the site - it silently serves today's availability instead - so such rows are reported as not collected rather than guessed at.

## `endDate` (type: `string`):

Advanced: explicit range end, YYYY-MM-DD, inclusive. Leave empty to use Start date + Months ahead.

## `adults` (type: `integer`):

Number of adult guests per room. A property that cannot host this many adults in any room returns no rooms at all for every date, so match it to the property you are tracking.

## `nights` (type: `integer`):

Length of stay. The check-out date is derived from each check-in date plus this many nights. Prices are for the whole stay, not per night.

## `rooms` (type: `integer`):

Number of rooms to search for per booking.

## `child1` (type: `integer`):

OPTIMA splits children into five configurable age/meal bands whose exact labels are set per property (typically elementary-school age down to infants without meal or bedding). Band 1 is the oldest band. Leave all at 0 for an adults-only search.

## `child2` (type: `integer`):

Children in band 2. See band 1 for how OPTIMA splits child ages.

## `child3` (type: `integer`):

Children in band 3. See band 1 for how OPTIMA splits child ages.

## `child4` (type: `integer`):

Children in band 4. See band 1 for how OPTIMA splits child ages.

## `child5` (type: `integer`):

Children in band 5, the youngest. See band 1 for how OPTIMA splits child ages.

## `dayUse` (type: `boolean`):

Search day-use (no overnight stay) plans instead of overnight stays. Most properties offer none, in which case every date comes back with no rooms.

## `includeSoldOut` (type: `boolean`):

Keep rows for dates and rooms with nothing bookable. Recommended ON: knowing when a property sells out is itself rate data, and OPTIMA lists its unbookable rooms explicitly.

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

Maximum number of requests in flight at once, across all properties. Capped at 10. One request per check-in date, and OPTIMA serves 0.7-2.5 MB of uncompressed HTML per request, so a live production site should not be hit harder than this.

## `runSummaryDatasetName` (type: `string`):

Optional. Name of a dataset to append this run's summary to, as a single row per run. Useful on a schedule: consecutive daily runs build one small table you can read at a glance to confirm each run collected every date, instead of aggregating tens of thousands of rate rows. Named datasets are retained indefinitely. The summary is always written to this run's key-value store as OUTPUT regardless of this setting.

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

Apify Proxy, OFF by default. This is deliberate and measured, not an oversight: OPTIMA is a property's own booking engine with no anti-bot measures, and no IP blocking or rate limiting was seen across hundreds of requests. It also serves 0.7-2.5 MB of UNCOMPRESSED HTML per check-in date, and the shared datacenter proxy pool moves that at roughly 23 KB/s - an A/B on the Apify platform, same property and same 7 dates, took 211 s through the proxy against 10.2 s without it, for identical data and zero errors either way. Residential proxy is billed per GB and would cost more than a run earns. Turn a proxy on if a property ever starts blocking you; nothing else about the run changes.

## Actor input object example

```json
{
  "propertyUrls": [
    "https://go-botanicalpoolclub.reservation.jp/ja/hotels/bpc/searchInput"
  ],
  "monthsAhead": 6,
  "adults": 2,
  "nights": 1,
  "rooms": 1,
  "child1": 0,
  "child2": 0,
  "child3": 0,
  "child4": 0,
  "child5": 0,
  "dayUse": false,
  "includeSoldOut": true,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Normalized OPTIMA direct-booking rate records: selling price, member price, pre-discount price, discount, remaining rooms, meal plan and cancellation policy for every bookable room x plan, plus explicit rows for rooms and dates that are not bookable.

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

One JSON object reporting whether this run collected everything it was asked for: dates requested vs collected, dates left as availability=null (COLLECTION\_FAILED or DATE\_MISMATCH), the check-in date range actually present in the data, HTTP errors by status, and validation-flag counts.

# 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 = {
    "propertyUrls": [
        "https://go-botanicalpoolclub.reservation.jp/ja/hotels/bpc/searchInput"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("cobocus/optima-hotel-rate-scraper").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 = {
    "propertyUrls": ["https://go-botanicalpoolclub.reservation.jp/ja/hotels/bpc/searchInput"],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("cobocus/optima-hotel-rate-scraper").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "propertyUrls": [
    "https://go-botanicalpoolclub.reservation.jp/ja/hotels/bpc/searchInput"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call cobocus/optima-hotel-rate-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,cobocus/optima-hotel-rate-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/7BLnSR8j5ImI8JZB6/builds/2yNR50gUy31QOpA4y/openapi.json
