# Rakuten Travel Purpose-Based Hotel Recommender (`woolen_snake/rakuten-travel-purpose-recommender`) Actor

Search Rakuten Travel hotels by area, budget, and purpose — get onsen, business, family, and solo-traveler picks with a score.

- **URL**: https://apify.com/woolen\_snake/rakuten-travel-purpose-recommender.md
- **Developed by:** [coco zizi](https://apify.com/woolen_snake) (community)
- **Categories:** Travel, Agents
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.00005 / actor start

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

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — 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

## Rakuten Travel Purpose-Based Hotel Recommender

Searches hotels via the **official Rakuten Travel API** (楽天ウェブサービス / Rakuten Web Service) by
**area × budget band × purpose** (onsen / business / family / solo-traveler) and returns each hotel with
a transparent **recommendation score** — built for AI agents and travel apps to consume directly.

### Features

- Search hotels by area, budget, and travel purpose
- Find onsen hotels via Rakuten's official filter
- Detect business, family, and solo-traveler hotels
- Score and rank hotels by review quality and price fit
- Get hotel images, review data, and booking links

### ⚠️ Disclaimer (please read)

- This Actor uses the **official Rakuten Web Service API** (commercial use allowed, free, instant
  application ID issuance, no review) — not scraping. You need your own `applicationId` **and**
  `accessKey`, issued together at https://webservice.rakuten.co.jp/app/create, to run it. Rakuten migrated
  its API infrastructure in 2026: both credentials (UUID-format `applicationId`, `pk_`-prefixed
  `accessKey`) are required — a pre-migration, `applicationId`-only key will not work.
- `recommendationScore` and `purposeTags` (business/family/solo) are generated by this Actor's own logic
  on top of Rakuten's data. They are a convenience estimate, not an official Rakuten classification.
  Always verify details on the official hotel page before booking.
- Rakuten enforces **1 request/second per `applicationId`**; exceeding it returns HTTP 429. This Actor
  enforces a minimum 1100ms delay between paginated calls for safety (see "Rate limit" below).
- **Area codes**: `middleClassCode`, `smallClassCode`, and `detailClassCode` are all required — Rakuten's
  own API docs suggest `middleClassCode` alone should be enough, but in practice the search rejects
  anything short of the full 4-level chain. Look codes up via Rakuten's **Area Class API**
  (`GetAreaClass`); well-known values like `tokyo`/`tokyo`/`A` (central Tokyo) work out of the box.
- **Security note**: this Actor's Rakuten app has its IP allowlist set to `0.0.0.0/0` (open to any IP —
  needed since Apify's cloud runs from dynamic, shared outbound IPs), so `applicationId`/`accessKey` are
  the only access control left on that Rakuten app. Both are marked `isSecret: true` in the input schema
  and never logged or included in output.

### Purpose classification: official vs. estimated

This is the core design point of this Actor's output:

| Purpose | `source` | `confidence` | How it's determined |
|---|---|---|---|
| `onsen` (温泉) | `official` | always `1.0` | Rakuten's own `squeezeCondition=onsen` filter on the Vacant Hotel Search API — the hotel is only included in the search results because Rakuten itself tagged it as an onsen property. |
| `business` (ビジネス) | `heuristic` | `0`–`0.95` | Keyword-dictionary match (`src/purposeClassifier.js`) against the hotel's own `hotelSpecial` free-text description, name, and facility fields. |
| `family` (ファミリー) | `heuristic` | `0`–`0.95` | Same keyword-dictionary approach, different dictionary. |
| `solo` (一人旅) | `heuristic` | `0`–`0.95` | Same keyword-dictionary approach, plus a small contextual boost when the search itself specifies 1 adult (`adultNum: 1`). |

Each hotel's `purposeTags` array can contain multiple tags at once (e.g. an onsen ryokan can also match
the `family` dictionary). There is **no official Rakuten API parameter for business/family/solo** — that
gap is exactly what this Actor's keyword dictionary fills (tunable in `src/purposeClassifier.js`, same
pattern as `classify.js` in our PR TIMES Actor).

### What does this Actor do?

1. Calls Rakuten's **Vacant Hotel Search API** (`VacantHotelSearch`, 2017-04-26) — the only Rakuten
   Travel endpoint with a real price-band filter (`minCharge`/`maxCharge`) — filtered by area
   (`middleClassCode` etc.), budget band, stay dates, and occupancy. When `purpose` is `onsen`, this call
   also passes Rakuten's own `squeezeCondition=onsen`.
2. Tags each hotel with `purposeTags` as described above.
3. When `purpose` is `business`/`family`/`solo`, keeps only hotels where that tag was detected
   (`confidence > 0`).
4. Scores every remaining hotel with `recommendationScore` (0–100, `src/scoring.js`) combining review
   quality, price fit within the requested budget band, and purpose-match confidence — with a
   `scoreBreakdown` so the weighting is inspectable, not a black box.
5. Sorts by `recommendationScore` descending and outputs the top `maxItems`.

### Who is this for?

Existing similar Actors (piquno/rakuten-travel-scraper, shiokoshi356/rakuten-travel-scraper) stop at
dumping raw hotel list/detail data. This Actor's differentiation (same stack as our BOOTH, SUUMO, and PR
TIMES Actors: JavaScript ESM, Apify SDK, Pay-per-event pricing) is structuring the result as a curated,
scored recommendation instead — and clearly labeling **which part of that recommendation is official
Rakuten data vs. this Actor's own estimate** (see "Purpose classification" above).

### Input

| Parameter | Type | Default | Description |
|---|---|---|---|
| `applicationId` | string (secret) | — | **Required.** Your Rakuten Web Service application ID (UUID format). |
| `accessKey` | string (secret) | — | **Required.** Your Rakuten Web Service access key, issued alongside `applicationId`. |
| `middleClassCode` | string | — | **Required.** Prefecture-level area code (e.g. `"tokyo"`). |
| `smallClassCode` | string | — | **Required.** City/ward-level area code (e.g. `"tokyo"` for central Tokyo's 23 wards). |
| `detailClassCode` | string | — | **Required.** Station/neighborhood-level area code (e.g. `"A"`). |
| `largeClassCode` | string | `"japan"` | Top-level area code. Rarely needs to change. |
| `minCharge` / `maxCharge` | integer | none | Budget band, JPY per room per night. |
| `checkinDate` / `checkoutDate` | string | tomorrow / day after | Format `YYYY-MM-DD`. |
| `adultNum` | integer | `2` | Number of adults. Also a contextual signal for the `solo` tag. |
| `purpose` | string | `"any"` | `any` / `onsen` (official) / `business` / `family` / `solo` (estimated). |
| `maxItems` | integer | `20` | Cap on hotels output/charged (max 100). |
| `requestDelayMs` | integer | `1100` | Delay between paginated Rakuten API calls. Cannot go below 1100ms (Rakuten's 1 req/sec limit). |

Look up `middleClassCode`/`smallClassCode`/`detailClassCode` via Rakuten's **Area Class API**
(`GetAreaClass`) — see "FAQ & known limitations" for a note on making this step easier in a future version.

### Output

Actual output from a real run against a live `applicationId`/`accessKey` (2026-08-06, Tokyo/Ginza area,
`purpose: "onsen"`):

```json
{
  "hotelNo": 182801,
  "hotelName": "天然温泉　七宝の湯　ドーミーインＰＲＥＭＩＵＭ銀座",
  "hotelKanaName": "どーみーいんぷれみあむぎんざ",
  "hotelSpecial": "銀座エリア唯一の”黒湯”天然温泉・高温サウナ完備！　最上階には和風プレミアムフロアを御用意しております",
  "area": {
    "middleClassCode": "tokyo",
    "smallClassCode": "tokyo",
    "areaName": null,
    "address1": "東京都",
    "address2": "中央区銀座6-16-8",
    "nearestStation": "東銀座",
    "latitude": 35.6676788,
    "longitude": 139.7657912
  },
  "priceBand": { "minCharge": null, "maxCharge": null },
  "hotelMinCharge": 12210,
  "reviewAverage": 4.62,
  "reviewCount": 203,
  "stayCondition": { "checkinDate": "2026-08-07", "checkoutDate": "2026-08-08", "adultNum": 2 },
  "purposeTags": [
    { "type": "onsen", "label": "温泉", "source": "official", "confidence": 1, "matchedTerms": [] }
  ],
  "recommendationScore": 86.2,
  "scoreBreakdown": { "reviewScore": 46.2, "priceScore": 10, "purposeScore": 30 },
  "hotelInformationUrl": "https://img.travel.rakuten.co.jp/image/tr/api/re/pvonD/?f_no=182801",
  "hotelImageUrl": "https://img.travel.rakuten.co.jp/share/HOTEL/182801/182801.jpg",
  "reviewUrl": "https://img.travel.rakuten.co.jp/image/tr/api/re/gJNfM/?f_hotel_no=182801",
  "disclaimer": "Reference information generated from data retrieved via the official Rakuten Travel API. The onsen tag is confirmed data reflecting Rakuten's own official filter (squeezeCondition=onsen); the business/family/solo tags are estimates based on simple keyword matching against each property's own description text (see purposeTagConfidence for the confidence level) and are not guaranteed to be accurate. Always check the official listing page for the latest details before booking.",
  "retrievedAt": "2026-08-06T06:28:16.361Z"
}
```

Note: `hotelName`/`hotelSpecial`/addresses are Rakuten's own Japanese-language data — Rakuten's API has no
English fields to translate from, so only this Actor's own `disclaimer` text is in English (see "FAQ"
below). Also note that `area.areaName` returns `null` in practice despite Rakuten's own documentation
listing it as present on this endpoint — a discrepancy in Rakuten's API/docs, not a bug in this Actor.

### Pricing

| Event | Price | Trigger |
|---|---|---|
| Actor Start (`apify-actor-start`) | $0.00005 | Once per run (Apify's standard synthetic event) |
| Hotel recommended (`hotel-recommended`) | $0.035 | Per hotel output, with review data and a scored recommendation |
| Purpose match detected (`purpose-match-detected`) | $0.01 | Charged in addition to `hotel-recommended` when at least one purpose tag (official or estimated) matched |

A tagged hotel costs $0.045 total; an untagged one costs $0.035.

#### How the price was set (measured on Apify, 2026-08-06)

Two production-like runs, both Tokyo/Ginza area (`middleClassCode=tokyo`/`smallClassCode=tokyo`/
`detailClassCode=A`), `maxCharge: 30000`:

| Run | `purpose` | Items output | API pages fetched | Duration | Real infra cost (`usageTotalUsd`) | Cost per item |
|---|---|---|---|---|---|---|
| 1 | `any` (25 requested) | 25 | 3 | 8.0s | $0.0006275855 | $0.0000251034 |
| 2 | `business` (20 requested, only 1 hotel matched the dictionary) | 1 | 2 | 4.4s | $0.0003041074 | $0.0003041074 |
| Blended | — | 26 | — | — | $0.0009316928 | $0.0000358343 |

\~85–95% of the cost in both runs is `ACTOR_COMPUTE_UNITS`; dataset/key-value-store writes are a small
fixed add-on with no meaningful per-item scaling. Two things stand out:

1. **Per-item cost is dominated by fixed per-run overhead** (container boot + the Rakuten API calls
   themselves), not by item count. Run 2 fetched 2 pages (60 candidates) but only 1 survived the
   `business` heuristic filter, so that page-fetch cost landed entirely on a single output item —
   $0.0003/item, ~12x worse than Run 1's $0.000025/item. A purpose-filtered run that matches few or zero
   hotels is the worst case for per-item cost, and a zero-match run earns no `hotel-recommended` revenue
   at all (only the flat $0.00005 `apify-actor-start` charge) despite making the same Rakuten API calls —
   a structural gap worth being aware of, though the absolute amounts are small enough (a fraction of a
   cent per zero-match run) that it isn't worth added pricing complexity to close.
2. **Purpose tagging itself adds no measurable infrastructure cost** — official (`onsen`) tagging is a
   literal object construction and heuristic tagging is a regex scan over `hotelSpecial` text already
   fetched for the base search, with zero additional HTTP calls (same finding as the PR TIMES Actor's
   `salesTimingSignals`). `purpose-match-detected`'s price is therefore a value-based decision, not
   something a cost measurement can inform — see below.

Following the same methodology as the PR TIMES Actor's README (price against the *higher* of the measured
per-item costs, not the optimistic blended average): `hotel-recommended` was set at **$0.035** — a
placeholder $0.02 would only be 65.8x the worst-case $0.0003041/item figure (short of a 100x target),
while $0.035 clears it at **115x** (and ~1150x against the typical/blended figure).

#### Why official and estimated purpose tags are priced the same

A case could be made for pricing `business`/`family`/`solo` higher than `onsen`: they're exactly the
categories Rakuten's own API can't filter by at all, so they're where this Actor's differentiation
actually lives, while `onsen` is close to a pass-through of a filter Rakuten already provides for free.
(The PR TIMES Actor prices its own analogous estimated signal, `sales-timing-signal-detected`, 4x higher
than its base event on this logic.)

This Actor instead uses one flat `purpose-match-detected` price regardless of `source`, because `purpose`
is the core promise of the product itself here, not a bonus layered on top of an already-complete result
the way PR TIMES's signal is — a caller asking for `purpose: "family"` and one asking for `purpose:
"onsen"` are getting the same feature answering the same question, just via a different internal
mechanism they don't choose and mostly don't see. `purposeTagConfidence` already communicates reliability
directly, rather than folding it into a price split. A confidence-tiered price schedule is a reasonable
direction for a future version if usage data justifies the added billing complexity, but isn't
implemented here.

### Rate limit

Rakuten enforces **1 request/second per `applicationId`** (exceeding it returns HTTP 429). This Actor:

- Never sets `requestDelayMs` below 1100ms (1000ms + safety margin) between paginated calls.
- Auto-retries once, after a 3-second wait, if a 429 is received (`src/rakutenClient.js`).

### FAQ

**How accurate are the business/family/solo purpose tags?**
They're keyword-dictionary estimates only (`src/purposeClassifier.js`), not an official Rakuten
classification. Wording not covered by the dictionary won't be detected, and matches can be spurious
(e.g. a hotel merely mentioning "family restaurant nearby" could false-positive on `family`).
`PURPOSE_DEFINITIONS` can be tuned as real output is reviewed.

**Do I need to know Rakuten's area codes myself?**
Yes, for now — all 4 levels (`largeClassCode`/`middleClassCode`/`smallClassCode`/`detailClassCode`) are
required, so you need to look them up via Rakuten's Area Class API (`GetAreaClass`). A natural future
addition would be accepting a free-text area name and resolving it to the full code chain automatically,
or offering latitude/longitude + `searchRadius` as a simpler alternative input mode.

**Does the recommendation score include per-aspect review ratings (cleanliness, service, etc.)?**
Not currently. `VacantHotelSearch` only returns `reviewAverage`/`reviewCount`/`userReview` — the
per-aspect breakdown (service/location/room/equipment/bath/breakfast/dinner/cleanliness) would require an
additional call to `SimpleHotelSearch` or `HotelDetailSearch` per `hotelNo`, which this v0.1 skips to keep
the request count (and rate-limit exposure) minimal. Relatedly, `hotelClassCode`'s exact value meanings
(hotel/ryokan/pension/etc.) aren't documented by Rakuten and haven't been fully confirmed against real
data yet.

**Can I search for premium/luxury hotels specifically?**
Not as a dedicated option — `HotelRanking`'s `genre=premium` is a separate, area-less ranking endpoint
that isn't wired into this Actor, since the current purpose list (onsen/business/family/solo) didn't call
for it.

**Is the hotel description translated to English?**
No — `hotelName`/`hotelSpecial`/addresses etc. are Rakuten's raw Japanese text, since Rakuten's API has
no English fields to fall back to. Only this Actor's own boilerplate `disclaimer` text is in English.
Generating English summaries of the actual property descriptions would need an additional translation/LLM
step on top of `hotelSpecial`, not implemented here.

# Actor input Schema

## `applicationId` (type: `string`):

Your own applicationId (UUID format since the May 2026 Rakuten Web Service migration) issued at https://webservice.rakuten.co.jp/app/create. Required to call the Rakuten Travel API. Never shared or logged by this Actor.

## `accessKey` (type: `string`):

Your own accessKey, issued alongside applicationId on the same registration screen. Required since Rakuten's May 2026 API migration (the old applicationId-only auth no longer works). Never shared or logged by this Actor.

## `largeClassCode` (type: `string`):

Top-level area code from the Rakuten area-code hierarchy (GetAreaClass API). "japan" covers all of Japan and rarely needs to change.

## `middleClassCode` (type: `string`):

Prefecture-level area code (e.g. "tokyo", "osaka", "kyoto"). Look up exact codes with the GetAreaClass API (not yet wired into this v0.1 template). Prefilled with a known-working Tokyo example.

## `smallClassCode` (type: `string`):

City/ward-level area code narrowing down middleClassCode further (e.g. "tokyo" for Tokyo's 23 wards). REQUIRED as of the Rakuten API's May 2026 migration: despite Rakuten's own docs saying middleClassCode alone should suffice, VacantHotelSearch was confirmed (2026-08-06, against a live key) to reject the request unless the full large/middle/small/detail chain is given. Look up via the GetAreaClass API. Prefilled with a known-working Tokyo example.

## `detailClassCode` (type: `string`):

Station/neighborhood-level area code narrowing down smallClassCode further. REQUIRED for the same reason as smallClassCode above (e.g. "A" under Tokyo's 23-ward small class). Look up via the GetAreaClass API. Prefilled with a known-working Tokyo example.

## `minCharge` (type: `integer`):

Lower bound of the budget band. Leave blank for no lower bound.

## `maxCharge` (type: `integer`):

Upper bound of the budget band. Leave blank for no upper bound. Must be greater than minCharge if both are set.

## `checkinDate` (type: `string`):

Format YYYY-MM-DD. Defaults to tomorrow if left blank (Rakuten Travel requires a real, available date range to search vacancies).

## `checkoutDate` (type: `string`):

Format YYYY-MM-DD. Defaults to the day after checkinDate if left blank.

## `adultNum` (type: `integer`):

Number of adults staying. Also used as a light contextual signal for the "solo traveler" purpose tag (1 adult nudges solo confidence up).

## `purpose` (type: `string`):

onsen is an OFFICIAL filter (Rakuten's own squeezeCondition=onsen, confidence always 1.0). business / family / solo are ESTIMATED by keyword-dictionary matching against the hotel's own description text (see README) and are output with a purposeTagConfidence between 0 and 1 rather than treated as certain. any returns all hotels in the area/budget band, ranked by recommendationScore only.

## `maxItems` (type: `integer`):

Maximum number of hotels to output. A charge event is generated for every item, so this directly drives run cost.

## `requestDelayMs` (type: `integer`):

Delay between paginated calls to the Rakuten API. Rakuten enforces 1 request/second per applicationId (429 if exceeded), so this cannot be set below 1100ms for safety.

## Actor input object example

```json
{
  "largeClassCode": "japan",
  "middleClassCode": "tokyo",
  "smallClassCode": "tokyo",
  "detailClassCode": "A",
  "checkinDate": "",
  "checkoutDate": "",
  "adultNum": 2,
  "purpose": "any",
  "maxItems": 20,
  "requestDelayMs": 1100
}
```

# Actor output Schema

## `recommendedHotels` (type: `string`):

Hotels ranked by a transparent recommendation score, tagged with official (onsen) and estimated (business/family/solo) purpose classifications.

# 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("woolen_snake/rakuten-travel-purpose-recommender").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("woolen_snake/rakuten-travel-purpose-recommender").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 '{}' |
apify call woolen_snake/rakuten-travel-purpose-recommender --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,woolen_snake/rakuten-travel-purpose-recommender"
        }
    }
}

```

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/ayQWua7KnKF3q9hHa/builds/UOVPeeByPXtrsVgZB/openapi.json
