Yanolja Scraper: Stays, Rates & Reviews avatar

Yanolja Scraper: Stays, Rates & Reviews

Pricing

from $1.50 / 1,000 listing records

Go to Apify Store
Yanolja Scraper: Stays, Rates & Reviews

Yanolja Scraper: Stays, Rates & Reviews

Scrape Yanolja (NOL), Korea's largest accommodations marketplace, by region or URL. Every row carries name, address, coordinates, phone, star class, room types with per-night rates, review score and review text, amenities, policies and photos from one stay page read.

Pricing

from $1.50 / 1,000 listing records

Rating

0.0

(0)

Developer

Abot API

Abot API

Maintained by Community

Actor stats

1

Bookmarked

1

Total users

1

Monthly active users

11 days ago

Last modified

Categories

Security

Limited permissions

Share

Yanolja Scraper turns Yanolja (NOL), Korea's largest accommodations marketplace, into structured stay data. Get full records for motels, hotels, resorts, pensions and poolvillas: name, address, coordinates, the stay's own publicly displayed booking phone, room types with per-night rates, review score and review text, facilities, policies and photos. Browse by Korean region and stay group, or paste stay and collection links directly, then export to JSON, CSV or Excel, or pull results straight into your app through the API.

Why This Scraper?

  • Two ways to scope a run. Browse the site's own ranked lists by Korean region and stay group, or paste stay and collection links directly; both return the same full record shape.
  • Full stay detail, not just a listing card. Each enriched record carries room types with per-night rates, review score and highlighted review text, address and coordinates, the stay's own booking phone, facilities, policies and photos, all from one stay page read.
  • Availability rates, not stale prices. Overnight and day-use rates come from the site's own next-night availability window, including sold-out flags, so a returned price is what the site is quoting right now.
  • Reorder the rows you collected. Sort one run's output by price or review score with a single input field, instead of post-processing it yourself.
  • Resume a big crawl without repaying for it. Paste a previous run's id and this run skips every record it already collected there.
  • Built for schedules. Incremental mode returns only new, updated and reappeared records on recurring runs, and a refused run fails loudly instead of returning an empty dataset.
  • Pipe results into the apps you use. Optional MCP export sends a condensed summary of each record to Notion, Linear or Airtable, while the full record always stays in the dataset.

Use Cases

  • Travel meta-search and price comparison: track live overnight and day-use rates plus sold-out flags across motels, hotels and pensions in the regions you serve.
  • Revenue management and market research: benchmark rate positioning, review scores and amenities against comparable stays in the same region and stay group.
  • Travel content and itinerary tools: pull address, coordinates, photos and policy text to build region guides or booking adjacent content.
  • Price drop and availability monitoring: schedule the same region with incremental mode and get only the stays whose rate or availability actually changed.
  • Review and reputation tracking: collect rating value, review count and the site's own review highlights to watch sentiment over time.

Data You Get

Sample shape: values are illustrative placeholders, not from a real stay.

FieldExample
recordId / stayId"100000001"
rowType"stay"
url"https://nol.yanolja.com/stay/domestic/100000001"
name / subtitle"Sample Stay at Sample District" / "Sample subtitle text"
stayGroup"Motel" (also "Hotel", "Resort", depending on the stay)
hotelStar"3"
badges["Sample badge one"]
address"Sample-si, Sample-gu, Sample-daero 12"
latitude / longitude37.11 / 127.02
cityName / cityDetail"Sample-si" / "Sample-gu"
sellerPhone"0501234xxxx"
ratingValue / reviewCount4.2 / 456
priceRange"Room 20,000 KRW and up"
lowestPriceKrw36000
overnightPriceKrw / dayUsePriceKrw36000 / null
overnightSoldOut / dayUseSoldOutfalse / true
checkInTime / checkOutTime"16:00" / "12:00"
facilities["Free parking", "Sample amenity"]
reviewHighlightsone row per highlighted review: reviewer initials, score, date, text
gptReviewSummary"Clean rooms, helpful staff"
cancelPolicy"Cancel 1 day prior: full refund"
mainImageUrl / photoUrlsthe stay's main photo and full property gallery
roomsone nested entry per room type: id, name, rate pair, benefits, up to three photos

Some fields depend on the stay type: hotelStar is filled only for star-rated hotels, and checkInTime / checkOutTime only for stays that publish them as a field (some pensions; the text is the site's own, such as a time range), otherwise they are empty. cancelPolicy holds the cancellation text of the stay's rate plans and is empty only when the stay returns no rate plans.

Every row also carries replyCount, ratingFromFd/ratingCountFromFd (a second, structured-data copy of the review score), descriptionText, popularityMessage, policyInformation, petsAllowed, availableLanguage and roomPhotoUrls. A bare listing stub (search mode with Fetch full stay details off) carries only recordId, stayId, rowType, url, name and listingImageUrl. With incremental mode on, rows also carry scrapedAt, changeType, changedFields, firstSeenAt and lastSeenAt.

How to Use

  1. Pick a mode: search (browse by Korean region and stay group) or url (paste stay or collection links).
  2. For search mode, list one or more region names and optionally narrow to one stay group. For url mode, paste the stay or collection URLs.
  3. Set the ordering and Max items to shape the run, and switch Fetch full stay details on or off.
  4. Click Start, then download the dataset as JSON, CSV or Excel, or read it through the API.

Search a region for motels:

{
"mode": "search",
"locations": ["μ„œμšΈ"],
"categoryGroup": "motel",
"fetchDetails": true,
"maxItems": 20
}

Search two regions, cheapest first:

{
"mode": "search",
"locations": ["제주", "λΆ€μ‚°"],
"sortResultsBy": "price_asc",
"maxItems": 50
}

URL shapes (every one is in the input's default, each on a different stay, so url mode with the field untouched reads them all; the two sitemap entries expand to as many stays as Max items allows, not to one example):

ShapeExampleResult
Collection page, area slughttps://nol.yanolja.com/programmatic/domestic-accommodation/μ²œμ•ˆμ‹œ-μ²œμ•ˆμ—­-근처-νŽœμ…˜-ν’€λΉŒλΌ-μΆ”μ²œ-베슀트-10the ranked stays of that list
Collection page, province slug (percent-encoded works too)https://nol.yanolja.com/programmatic/domestic-accommodation/%EA%B0%95%EC%9B%90%EB%8F%84-...-10the ranked stays of that list
Collection sitemap indexhttps://nol.yanolja.com/programmatic/domestic-accommodation/sitemap.xmlexpanded to about one ranked list per ten Max items
Stay sitemap indexhttps://nol.yanolja.com/stay/sitemap.xmlthe first Max items stays of the first child sitemap
Stay pagehttps://nol.yanolja.com/stay/domestic/1000102592one full stay
Stay page with dateshttps://nol.yanolja.com/stay/domestic/1000114130/?checkIn=2026-11-10&checkOut=2026-11-11one full stay priced for those dates (both dates needed; they win over the default dates)
Mobile hosthttps://m.yanolja.com/stay/domestic/10040685read as the nol.yanolja.com stay

A collection slug the site does not publish is refused by the site and logged as a warning for that URL, never a silent empty run. This is a behaviour change: with the urls field untouched, url mode now reads one URL of each shape above; the sitemap entries expand up to Max items.

Paste a stay URL:

{
"mode": "url",
"urls": ["https://nol.yanolja.com/stay/domestic/1000102592"]
}

Paste a collection URL (a region's own ranked top list):

{
"mode": "url",
"urls": ["https://nol.yanolja.com/programmatic/domestic-accommodation/μ„œμšΈ-근처-λͺ¨ν…”-μˆ™μ†Œ-μΆ”μ²œ-베슀트-10"]
}

Run it from your code

Python:

from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/yanolja-com-scraper").call(run_input={"mode": "search", "locations": ["μ„œμšΈ"], "maxItems": 20})
for stay in client.dataset(run["defaultDatasetId"]).iterate_items():
print(stay["name"], stay["overnightPriceKrw"])

JavaScript:

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/yanolja-com-scraper').call({ mode: 'search', locations: ['μ„œμšΈ'], maxItems: 20 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.

How a search run is scoped and capped

Each region word is matched against the site's own ranked collection pages (one word can match several, for example one per district and stay group), and every matched collection is a single ranked top list of about ten stays, read once. So the practical size of a search run comes from how many regions and stay groups you list, not from paging through one long list. Max items is one run wide cap, split evenly across every region, stay group and pasted URL a run reads.

Resume, incremental mode, and a caveat on tight caps

  • Resume (resumeFromRunId) continues one interrupted run: paste its run or dataset id and this run skips every record already collected there, so you don't pay twice.
  • Incremental mode (incrementalMode) is for scheduled runs over the same region, stay group and filters. Each record is classified NEW, UPDATED (with changedFields, a rate move counts as a change), UNCHANGED (suppressed and not billed unless emitUnchanged is on), REAPPEARED or EXPIRED (only after a run that scanned the whole tracked scope, and only with emitExpired). stateKey names or shares the stored state. With incremental mode off, output is exactly as before.
  • What is compared: by default every field of the stay record (rates, sold-out flags, room rates, description, facilities, photos, ratings and review counts); a change to any of them returns UPDATED with changedFields naming it. scrapedAt and detailCarriedForward are always excluded, and so is any field you list in ignoreFieldsForChanges (a dotted name such as rooms.photos ignores that key inside every room while room rates stay tracked). Set changeScope to priceAndStock to compare only the lowest, overnight and day-use rates, the sold-out flags and each room's rates. With compactUpdates on, UPDATED and EXPIRED rows are short (ids, url, name, current rates, priceChanges, variantChanges) and NEW rows stay complete; the per-record detail-enrichment event is charged exactly as for a full row. expireAfterMissedScans makes a stay EXPIRED only after it has been missing from that many full scans in a row; a stay missing from the listing is not proof that it cannot be booked, so use the sold-out fields for availability. All of these default to off, so existing output is unchanged. Stored state from earlier versions keeps comparing correctly, with no one-time UPDATED wave.

Keep Max items comfortably above what a run actually needs. Max items splits evenly across the regions, stay groups and URLs a run reads, and each collection is read once, in the site's own ranked order. When that split cuts a collection's list short: the trimmed off stays are still marked as read internally, so a DIFFERENT region or stay group that also lists one of those same stays skips it too, even though it was never actually collected; and, in incremental mode, a stay ranked below the cut is never compared against its stored state at all, so a genuine price or availability change on it goes undetected, run after run, for as long as the cap stays tight. Example: two collections both list the same popular stay, one near the top of its list and one near the bottom; if Max items caps the first collection before it reaches that stay, the second collection also skips it, and it is missing from that run's output entirely. Workaround: set Max items to 0 (unlimited) or to at least ten times the number of collections and URLs the run reads (the run log's Source 1/N line shows N; one region word can match several collections), so no collection's top ten is ever cut short. This matters most for incremental mode, where a trimmed stay's changes are not merely delayed, they stop being checked at all.

Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step after the scrape: the Apify dataset is never changed.

What gets written to the connector: a condensed, human readable summary of each record, not the full JSON. Each item becomes one entry with a title and its key fields flattened to plain text. The complete record always stays in the Apify dataset.

  1. Authorize a connector once under Apify, Settings, Integrations (Notion, Linear, Airtable, or Apify).
  2. Select it in the "Pipe results into your apps" input field. (If the picker is empty, you haven't authorized a connector yet.)
  3. For Notion, also set notionParentPageUrl to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third party credentials. Leave the field empty to skip.

Input Parameters

ParameterTypeDefaultDescription
modestringsearchsearch (browse by region and stay group) or url (paste stay or collection links).
locationsarray["μ„œμšΈ"]One or more Korean region or place names, matched against the site's own published collection pages. Search mode only.
categoryGroupstringallNarrow search mode to hotel, motel, or pension. Leave as all for every group the region publishes. Search mode only.
urlsarrayone URL of each shape (sitemaps expand up to Max items)Stay or collection links to read directly. URL mode only.
minPriceKrwinteger(none)Intended to keep only rows whose lowest rate is at least this many Korean won. Currently has no effect: the bound is checked before the stay page is read, when a row carries no rate yet, so every row passes.
maxPriceKrwinteger(none)Intended to keep only rows whose lowest rate is at most this many Korean won. Currently has no effect, for the same reason as minPriceKrw.
checkInstring(none)Stay check-in date (YYYY-MM-DD); needs checkOut too. Stay pages are read for those dates, so room rates and sold-out flags are those of that stay, and checkInDate / checkOutDate show them. A date pair pasted inside a stay URL wins. Only used when full stay details are on.
checkOutstring(none)Stay check-out date (YYYY-MM-DD), after check-in; needs checkIn too. Stay pages are read for those dates, so room rates and sold-out flags are those of that stay.
sortResultsBystringsite_orderArrange the rows this run collected, after they are read: site_order, price_asc, price_desc, rating_desc, or review_count_desc (ordered by the review count each delivered row carries; rows without a count go last).
fetchDetailsbooleantrueRead each stay's own page for the full record (rooms, rates, reviews, policies, photos). Off returns the bare listing stub only (name, image, URL).
maxItemsinteger20Stop after collecting this many records across every region, group and URL. 0 for unlimited.
maxPagesinteger0Currently unused: accepted in the input, but each collection is one single ranked list read once, so this has no effect on any run.
resumeFromRunIdstring(none)Continue one previous interrupted run: paste its run or dataset id to skip records already collected there.
incrementalModebooleanfalseTurn on for recurring runs of the same region, group and filters, returning only new, updated and reappeared records since the last run.
stateKeystring(none)Optional: name a monitoring campaign so its incremental state stays stable and separate from other scopes.
emitUnchangedbooleanfalseAlso return (and bill) records that have not changed since the last run.
emitExpiredbooleanfalseAlso return (and bill) records no longer found, after a run that fully scanned the tracked scope.
changeScopestringallIncremental mode only. priceAndStock makes only rate and sold-out changes (including each room's rates) return UPDATED. Changing it keeps the saved state, so it does not return the catalogue as NEW again.
expireAfterMissedScansinteger1Incremental mode only, with emitExpired. A stay is marked EXPIRED only after it has been missing from this many full scans in a row. Availability itself is reported by the sold-out fields.
compactUpdatesbooleanfalseIncremental mode only. UPDATED and EXPIRED rows are short: ids, url, name, current rates, priceChanges and variantChanges. NEW rows stay complete.
ignoreFieldsForChangesarray[]Incremental mode only. Extra field names to exclude from UPDATED detection; a dotted name such as rooms.photos ignores that key in every room while room rates stay tracked.
mcpConnectorsarray(none)Optional: send a summary of each record to apps you authorized under Integrations.
notionParentPageUrlstring(none)Notion connector only: page under which record pages are created.
maxNotifyListingsinteger50Cap on records written to each connector per run. Does not affect the dataset.
proxyobjectKorean residentialConnection settings. The default pins a Korean resident type connection; collection and sitemap pages also read through datacenter pools, but stay pages are refused there and on plain residential pools, so they are only served by the result-unblocking backup tier (see Plan Requirement).

Output Example

Sample shape: values are illustrative placeholders, not from a real stay.

{
"recordId": "100000001",
"stayId": "100000001",
"rowType": "stay",
"url": "https://nol.yanolja.com/stay/domestic/100000001",
"name": "Sample Stay at Sample District",
"subtitle": "Sample subtitle text",
"stayGroup": "Motel",
"hotelStar": "3",
"badges": ["Sample badge one"],
"address": "Sample-si, Sample-gu, Sample-daero 12",
"latitude": 37.11,
"longitude": 127.02,
"cityName": "Sample-si",
"sellerPhone": "0501234xxxx",
"ratingValue": 4.2,
"reviewCount": 456,
"priceRange": "Room 20,000 KRW and up",
"lowestPriceKrw": 36000,
"overnightPriceKrw": 36000,
"dayUsePriceKrw": null,
"overnightSoldOut": false,
"dayUseSoldOut": true,
"checkInTime": "16:00",
"checkOutTime": "12:00",
"facilities": ["Free parking", "Sample amenity"],
"reviewHighlights": [
{ "reviewer": "Sample user*", "score": 5, "date": "2025.01.02", "content": "Clean rooms, quiet at night, would stay again." }
],
"gptReviewSummary": "Clean rooms, helpful staff",
"cancelPolicy": "Cancel 1 day prior: full refund",
"mainImageUrl": "https://example.com/sample-main.jpg",
"photoUrls": ["https://example.com/sample-1.jpg", "https://example.com/sample-2.jpg"],
"rooms": [
{
"roomTypeId": 250000,
"roomTypeName": "Sample room double",
"subTitle": "Sample walk-in type",
"overnightPriceKrw": 36000,
"dayUsePriceKrw": null,
"benefits": ["Streaming service included"]
}
],
"scrapedAt": "2026-09-29T00:00:00Z"
}

Plan Requirement

Collection pages and sitemaps were read live through a datacenter pool and through a Korean residential pool, and both were served with real content. Stay pages are the gated part: they were refused on both of those pools (an edge refusal page saying the service is temporarily unavailable) and were served only through the result-unblocking backup tier. So a run that reads stay pages (Fetch full stay details on, or any stay URL) needs a plan that includes Apify proxy access; the backup tier is billed to your proxy usage like any other proxy traffic.

The default connection pins a Korean residential proxy and is yours to change: the choice of proxy group is your cost and your call. The actor tries your configured connection first and moves to the backup tier only for a page that the first one refused. If the connection is refused for your account, the run logs that the selected connection is unavailable, continues on a backup connection where it can, and says so when it serves a page that way. When no page could be read at all the run stops with a clear message rather than returning an empty dataset (see the FAQ). Any proxy configuration you paste yourself is used exactly as given.

FAQ

How much does it cost?

Billing has three events: the actor start (per GB of memory), one charge per record returned to the dataset, and one Stay detail enrichment charge per record, only for a record whose stay page read actually succeeded this run. Records suppressed as unchanged in incremental mode, and rows that were not delivered, are not charged. The Pricing tab shows the current rates. Use Max items to cap the cost of any run.

This actor collects only publicly available accommodation data. You are responsible for how you use it: follow Yanolja's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Listing details are generally facts, but photos and review text may be subject to third party rights.

Which output views does the dataset have?

Overview (the main stay columns), Detail (rooms, rates, policies and review highlights) and Changes (incremental mode: changeType, changedFields, priceChanges, firstSeenAt, lastSeenAt).

Can I get only new or changed stays on a schedule?

Yes. Schedule the actor from the Schedules tab and turn on Incremental mode. Each run then returns only new, updated and reappeared records, and unchanged ones are not billed unless you also turn on Emit unchanged.

Why did a stay disappear from my results, or never show a price change?

Max items splits evenly across every region, stay group and URL a run reads, and each collection is a single ranked list read once. A stay ranked below that split is skipped this run, and if a different collection also lists that same stay further up its own list, that collection skips it too, so it can be missing entirely even though it is a real, live stay. In incremental mode this also means the stay is never compared against its stored state, so a real rate or availability change on it goes undetected for as long as the cap stays tight. Set Max items to 0, or to at least ten times the number of sources shown in the run log's Source 1/N line, to avoid this.

Do the price filters narrow my results?

Not today. minPriceKrw and maxPriceKrw are accepted, but they are checked against the listing row before its stay page is read, and a listing row carries no rate yet, so every row passes. To work with a price range, collect the rows and filter on lowestPriceKrw after the run, or use Order the returned rows by to put the cheapest first.

Why did my run fail instead of returning an empty dataset?

If every collection or stay page this run tried was refused, the run stops with a clear message so "no stays matched" is never confused with "nothing could be read". Run it again in a few minutes, or check your proxy configuration. If some sources served pages while others were refused, you still get what was found.

Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.

πŸ”— Want more travel data?

Pair this actor with these related scrapers from the same team:

🏨 Booking.com Scraper
Scrape booking.com hotels, apartments, villas and hostels. Search any destination with...
🏨 Rakuten Travel Scraper
Scrape Rakuten Travel, Japan's largest domestic hotel-booking site, by prefecture...
🏨 Wego Scraper
Scrape Wego hotels by city or search URL. Get rates, providers, booking links...
🏨 Hipcamp Scraper
Scrape Hipcamp campgrounds, RV parks, glamping and unique stays by US or AU region or...
🏨 Traveloka Hotel Scraper
Scrape Traveloka hotels across Singapore, Malaysia, Indonesia, Thailand, Vietnam and the...
🏨 Rover Pet Care Scraper
Scrape Rover.com pet-care providers: sitter search by city, rates per stay type, ratings...

πŸ‘‰ Browse all abotapi scrapers

πŸ’¬ Support

  • 🐞 Found a bug or a missing field? Open a ticket on the Issues tab. We usually reply within hours.
  • πŸ› οΈ Need another site, extra fields or a private build? Email contact@abotapi.com.
  • ⭐ Enjoying it? A quick review on the actor page helps other users find it.