# Passo Scraper — Korean Used Motorcycle Listings (파쏘) (`kdatafactory/passo-scraper`) Actor

Scrape Passo (파쏘), Korea's long-running used-motorcycle marketplace (~11,000 live listings): brand, model, year, KRW price, mileage, region, primary photo and listing date as clean JSON. Full active catalogue in one pass. Public pages only, never authenticated, no private-seller contact data.

- **URL**: https://apify.com/kdatafactory/passo-scraper.md
- **Developed by:** [Seok June Park](https://apify.com/kdatafactory) (community)
- **Categories:** E-commerce, Automation
- **Features:** Limited permissions
- **Stats:** 2 total users, 0 monthly users, 90.9% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$4.00 / 1,000 catalog listing scrapeds

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

## Passo Scraper — Korean Used Motorcycles (파쏘) 🏍️

Scrape live **used-motorcycle listings** from [Passo (파쏘)](http://bike.passo.co.kr) —
Korea's long-running used-bike marketplace, **~11,000 active listings** (11,013–11,016 over
the course of 2026-10-22; the board moves during the day) — as
clean, structured JSON. Brand, model, year, KRW price, mileage, region, listing date and
the primary photo for every bike on the board.

This is **catalog mode**: the listing-row data plus the primary image, which is what you
need to render a browsable inventory. The full per-bike record (engine capacity, type,
colour, accident and tuning status, description, dealer, all photos) lives on Passo's
detail page and is a separate, on-demand mode — see
[Field availability](#field-availability-catalog-row-vs-detail-page) for exactly which
field sits where.

***

### What it does

Passo's used-bike catalogue is a 2012-era AJAX application. The page you see in a browser
is a 353 kB shell that contains **zero** listings; its own front end fetches them by
POSTing to `index.php` and parsing an XML document. This actor speaks that endpoint
directly — no headless browser, no HTML scraping, one request per 50 bikes.

- **The whole active catalogue** (~11,000 bikes), or
- **One manufacturer** (150 makes, Korean or English name), or
- **A custom slice** using Passo's own search syntax (year range, region, price band).

Sorted by newest, price, model year or mileage.

***

### Input

| Field | Type | Description |
|---|---|---|
| `maxItems` | integer | Rows to collect. Default `100`, max `20000`. Set `12000` for the whole active catalogue. |
| `maker` | string | Optional brand filter — `혼다`/`Honda`, `BMW`, `할리데이비슨`/`Harley-Davidson`, `야마하`/`Yamaha`, `SYM`, `KTM`, `존테스`/`Zontes`… or a raw Passo `maker_idx`. Blank = all 150 makes. |
| `sort` | string | `newest` (default), `price_asc`, `price_desc`, `year_desc`, `year_asc`, `mileage_asc`, `mileage_desc`. |
| `includeSoldAndReserved` | boolean | Default `false` = active listings only (~11,000). `true` returns Passo's full historical archive (~863,000 rows), which is **not** live inventory. |
| `searchExpression` | string | Advanced. Extra terms in Passo's own DSL, e.g. `@out_year:2018~2024#`, `@area:?서울#`, `@sale_price:300~800#`. |
| `requestDelaySecs` | integer | Pause between sequential requests. Default `2`, never below `1.5` whatever you set. |
| `proxyConfiguration` | object | Defaults to Apify Proxy, `RESIDENTIAL`, country **`KR`**. See [Proxy](#proxy-use-korea). |

```json
{ "maxItems": 12000, "sort": "newest" }
```

```json
{ "maker": "Harley-Davidson", "searchExpression": "@out_year:2018~2026#", "maxItems": 500 }
```

#### Run it locally

```bash
cd actors/passo-scraper
npm install
mkdir -p storage/key_value_stores/default
printf '%s' '{"maxItems":100,"sort":"newest","includeSoldAndReserved":false,"requestDelaySecs":2}' \
  > storage/key_value_stores/default/INPUT.json
node src/main.js
```

Rows land in `storage/datasets/default/`. Omit `proxyConfiguration` from the input, as
above, when you are already on a Korean network — the actor logs a warning and goes
direct. `storage/` is run state and is not committed.

***

### Output

One dataset record per listing. `price_krw` is full Korean won (Passo publishes 만원 /
10,000-won units; this actor converts). `mileage_km` is kilometres. `brand_en` gives the
official English brand name (Honda, Harley-Davidson, Daelim, Hyosung…, `null` for small
makes with no recognised English name) and `region_province_en` the province in English.

```json
{
  "source": "passo",
  "listing_id": "1141679",
  "scraped_at": "2026-10-22T10:33:54+09:00",
  "url": "http://bike.passo.co.kr/bike/index.php?part=cybershop&path=cybershop&mode=home#HIS_ID:search=&page=0&order_by=&search_mode=country&idx=1141679&num=0",
  "brand": "SYM",
  "brand_en": "SYM",
  "model": "조이맥스 Z 125 플러스",
  "year": 2022,
  "mileage_km": 40437,
  "price_krw": 1490000,
  "region": "경남/창원시 성산구",
  "primary_image_url": "http://file.passo.co.kr/dataimg/bike_images/11417/1141679/MOTOR_gid_1141679_s_1.img?0",
  "published_date": "2026-10-22",
  "engine_cc": null, "motorcycle_type": null, "color": null,
  "accident_free": null, "tuning": null, "description": null,
  "seller_name": null, "photo_urls": null,
  "title": "출퇴근,배달,가성비 모든면에서 우수한 조이맥스제트플러스125",
  "price_man": 149,
  "price_negotiable": true,
  "mileage_value": 40437,
  "mileage_unit": "Km",
  "region_province": "경남",
  "region_province_en": "Gyeongnam",
  "region_city": "창원시 성산구",
  "listing_status": "on_sale",
  "warranty": false,
  "view_count": 264,
  "badge_codes": [6, 7],
  "badges": ["A/S", "카드"],
  "product_state_code": "2",
  "sponsored": false,
  "detail_url": "http://bike.passo.co.kr/bike/index.php?part=cybershop&path=cybershop&mode=process&process=view&idx=1141679&num=1"
}
```

A real 100-listing run is in [`samples/client-sample.json`](samples/client-sample.json),
with the same rows as [`samples/client-sample.csv`](samples/client-sample.csv) (UTF-8 BOM,
opens straight in Excel).

**The eight detail-only fields are emitted as explicit `null`s on purpose**, so your table
has one shape in both modes and a later detail pass is a plain `UPDATE` rather than a
migration.

***

### Field availability: catalog row vs detail page

Measured on 2026-10-22 against the live list XML and a real detail page
(`idx=1141679`). "Catalog row" means the field arrives in the same response as everything
else, at no extra request and no extra cost.

| Field you asked for | Where it lives | Evidence |
|---|---|---|
| **Passo ID** | **Catalog row** — `listing_id` | `<idx>` in every `<item>`. Passo's own primary key; also the argument to the detail endpoint. |
| **Original URL** | **Catalog row** — `url` (derived) | Passo has **no server-rendered detail page** for used bikes. The catalogue is one AJAX page whose state lives in the location hash: `lib/dhtmlHistory.js` reads `#HIS_ID:…&idx=<id>&num=0` on load and opens that listing. Passo generates exactly these links itself. Built from `<idx>`, so it costs nothing. |
| **Brand** | **Catalog row** — `brand` | `<maker>`. 100/100 populated in the sample. |
| **Model** | **Catalog row** — `model` | `<model>`. 100/100. |
| **Year** | **Catalog row** — `year` | `<c_out_year>`. 100/100. ⚠ The sibling `<out_year>` is a constant placeholder — it reads `2000` on every row of every page, including bikes whose real year is 2025. |
| **Engine capacity (cc)** | **Detail page only** | Not present in the list XML under any name. Detail page shows `배기량 125 cc`. |
| **Mileage** | **Catalog row** — `mileage_km` | `<distance>` + `<distance_unit>`. 100/100. Unit is `Km` on 99/100 and `ml` (miles) on 1/100; the actor converts miles and also returns the raw value and unit. |
| **Price (KRW)** | **Catalog row** — `price_krw` | `<c_sale_price>`, in 만원, converted. 100/100. ⚠ The sibling `<sale_price>` is a placeholder reading `100` on every row. |
| **Region** | **Catalog row** — `region` | `<area>`, e.g. `경남/창원시 성산구`. Also split into `region_province` / `region_city`. 100/100. |
| **Motorcycle type** | **Detail page only** | Detail page shows `유형 스쿠터 > 빅스쿠터`. The list XML has no type field. (You *can* filter the catalogue by type with `searchExpression` `@_type_idx:…#`, but the chosen type is not echoed back on the row.) |
| **Colour** | **Detail page only** | Detail page `색상 흰색`. |
| **Accident status** | **Detail page only** | Detail page `사고유무 무`. Not derivable from the row: the `hotmark` badges are commerce flags, not condition flags — the three in use decode to A/S, 카드 (card accepted) and 할부 (instalments), read off the badge GIFs. |
| **Tuning status** | **Detail page only** | Detail page `튜닝체크 무`, plus an 18-item breakdown (배기/흡기/엔진/새시/브레이크/서스펜션 튜닝, 크롬·카본 파츠, 커스텀 도색 …). |
| **Description** | **Detail page only** | Detail page `매물상세설명`, free text. ⚠ The list XML has a `<comment>` element — it is **not** a description. It is the comment-count badge (`" "` or `"[1]"`). |
| **Seller / dealer** | **Detail page only — and partly off-limits** | Detail page `판매자 정보`: 이름 (e.g. a shop name such as 모터링크), **핸드폰 `010-…`**, **전화번호**, 통화가능시간, 판매지역. Shop name is business identity and is in scope for detail mode. The two phone numbers are **not emitted, ever** — a 010- mobile does not reliably distinguish a dealer from a private seller. See [Compliance](#compliance). |
| **Publication date** | **Catalog row** — `published_date` | `<update_date>`, `YY-MM-DD`, normalised to ISO. 100/100. Note this is the **listing update** date, which is what Passo sorts "newest" by — a refreshed old listing resurfaces. |
| **All photos** | **Detail page only** | The row carries exactly one image, `<thumb>` → `primary_image_url` (100/100). The detail page carries the full set, labelled 우측면 / 좌측면 / 후면 / 핸들 … |

**Summary for your schema:** 9 of the 17 wished-for fields are on the catalog row
(ID, URL, brand, model, year, mileage, price, region, publication date) **plus** the
primary image. The remaining 7 — engine cc, type, colour, accident, tuning, description,
seller — plus the full photo set are detail-page only. **Nothing you asked for is
unavailable anywhere**, though the seller field will arrive as a business name without
phone numbers.

Fields the row gives you for free and you did not ask for: `title` (the seller's own
headline), `price_negotiable` (가격절충), `listing_status`, `warranty`, `view_count`,
`badges`, `sponsored`.

***

### Removal detection (the two-strike rule)

The actor is stateless — each run knows nothing of the last one — so the absence counter
belongs on your side. What it gives you is the two things that rule needs:

- **`listing_id`** — Passo's own `idx`. Immutable: it is the row's primary key on Passo's
  side, it appears in the deep link and it is the detail endpoint's argument. It does not
  change when the seller edits the price, swaps photos or rewrites the text. Use it as
  your primary key; do not key on the URL, which embeds it anyway.
- **`scraped_at`** — ISO-8601 in KST (UTC+9), stamped per row.

Intended usage:

1. Run a **full** pass (`maxItems: 12000`, no `maker`, no `searchExpression`). Check the
   run succeeded and returned a plausible count before you trust its absences — a failed
   or truncated pass must not count as a strike.
2. `last_seen = scraped_at` for every id in the pass; `absent_passes = 0`.
3. For ids in your table but not in the pass, `absent_passes += 1`.
4. Retire at `absent_passes >= 2`. Reset to `0` the moment an id reappears — Passo
   listings do come back.

One pass is ~221 requests and takes about 8 minutes at the default delay, so daily is
comfortable and a listing is retired after roughly 48 hours of absence.

***

### Pagination: the whole catalogue is reachable in one pass

Deep-pagination caps are the usual reason a "full catalogue" promise fails (KB Chachacha,
for instance, stops at 10,000 items per query). **Passo has no such cap.** Verified
2026-10-22:

| Probe | Result |
|---|---|
| `@state:0#`, page 0 | `<count>11015</count>`, `<last>220</last>` — 221 pages of 50 |
| `@state:0#`, page 150 | 50 rows, dated 26-06 |
| `@state:0#`, page **220** (the last) | **15 rows** → 220 × 50 + 15 = **11,015 = count exactly** |
| `@state:0#`, page 221 | 0 rows — clean end, no error |
| unfiltered, page **17255** of 17,256 | 35 rows → 17255 × 50 + 35 = **862,785 = count exactly**, in 7.8 s |

The server serves its true last page even at offset 862,750. So **no facet split is
needed**: a single unfiltered pass with `includeSoldAndReserved: false` reaches all
\~11,000 active listings in 221 sequential requests.

If Passo ever does impose a cap, the split that works is **by manufacturer**
(`maker`, 150 codes, largest bucket is a few thousand) with `@out_year:a~b#` as a second
axis inside any bucket that overflows. The facet machinery is already in the input.

One caveat, not a cap: the `<plus>` block of ~10 **sponsored** listings is injected into
every page and rotates. The actor de-duplicates by `listing_id`, flags them
`sponsored: true` and emits them **after** the page's real results, so a `maxItems`
cut-off never discards a genuine listing in favour of a promoted one. 10 of the 100 rows
in the sample are sponsored — they are real active listings, just paid-for placements, and
excluding them would under-report the catalogue.

***

### Traffic and cost

All measured on 2026-10-22 with `Accept-Encoding: gzip, deflate` sent explicitly and curl
**not** decoding, so these are true on-the-wire bytes.

**The origin does not compress.** No `Content-Encoding` header is returned even when gzip
is offered — `Server: Apache/2.4.54 (Unix) PHP/5.2.17`, `Transfer-Encoding: chunked`. A
metered proxy therefore bills the full uncompressed size. There is no header, parameter or
endpoint variant that turns compression on.

| Response | Wire body | Rows | Content-Encoding |
|---|---|---|---|
| Catalog page 0 (`@state:0#`) | 59,084 B | 50 goods + 10 sponsored | none |
| Catalog page 1 | 58,088 B / 57,866 B (two fetches) | 50 goods + 10 sponsored | none |
| Catalog page 150 | 58,027 B | 50 goods + 10 sponsored | none |
| Catalog page 220 (last, partial) | 24,663 B | 15 goods | none |
| **Detail page** (`process=view&idx=1089124`) | **60,350 B** | **1 listing** | none |

Response headers are ~190 B; a request is ~525 B (headers + the 125-byte form body).

**Catalog economics.** Mean full page 58,587 B. A full active pass = 220 full pages +
page 220 + overhead = **13.07 MB for ~11,015 listings = 1,187 bytes per listing**.

| | |
|---|---|
| Per 1,000 catalog listings | **1.19 MB → $0.0095** at $8/GB |
| Full 12,000-listing pass | **14.24 MB → $0.114** |
| Revenue on that pass | $48.00 |
| **Proxy as a share of revenue** | **0.24 %** |

That is below the optimistic end of the $0.96–$14.40 range the deal was being sized
against — by roughly 8×. The catalog tier is not proxy-constrained.

**Detail economics are a different story.** 60,350 B **per listing**, versus 1,187 B for a
catalog row — **51× more traffic per record** for a tier priced at only 2× the catalog
rate.

| | |
|---|---|
| Per detail record | 61,065 B → $0.000489 |
| Per 1,000 detail records | 61.1 MB → **$0.489** |
| Revenue per 1,000 | $8.00 |
| **Proxy as a share of revenue** | **6.1 %** |

Still healthy, but it is the real cost centre and it scales with visitor traffic rather
than with a nightly job. Two notes for whoever prices detail mode: the 60 kB is mostly
page furniture (card-instalment tables, a shopping-mall sidebar, safe-trade boilerplate)
around a few hundred bytes of actual record, and there is no leaner variant — Passo's own
front end fetches this same blob.

**Is there a more compact endpoint?** Checked deliberately, and the answer is: *this
already is the compact one.*

- The XML POST endpoint is not one of several options — it is the **only** source. The
  server-rendered page at `mode=home` is 352,877 B and contains **zero** listings; they
  are injected by JavaScript from this same XML. So HTML scraping would cost 353 kB *plus*
  the XML, not instead of it. Choosing the XML endpoint is already the ~6× win per page,
  and far more for a whole pass.
- No JSON variant exists. The site's `lib/ajax.js` supports `"xml"` and `"text"` only, and
  the catalogue path (`process_cybershop.js` → `get_list`) uses `"xml"`.
- No page-size parameter is exposed anywhere in the front-end code (`list_count`,
  `page_size`, `limit`, `rows`, `per_page` — none appear). 50 rows per page is fixed.
- The one visible waste is the `<plus>` sponsored block: **9,726 B of every 58 kB page,
  16.6 %**, or 2.15 MB across a full pass. Dropping `mainck201508=home` from the body was
  tested and returns a byte-identical response, so it cannot be suppressed.

**This task's own footprint.** 34 requests to `passo.co.kr` in total (recon + evidence
archive + the sample run), 1.29 MiB of response bodies, well inside the 120-request
budget, all sequential and ≥1.5 s apart. The sample run itself was **2 requests and
117,172 B for 100 listings** — 1,172 B per listing, matching the projection.

***

### Proxy: use Korea

Passo is reported to geo-restrict the catalogue to Korean IPs and to answer a non-Korean
IP with **HTTP 200 and an empty result rather than an error** — the same failure mode that
silently zeroed `kbchachacha-scraper`'s platform runs on 2026-10-08. The default is
therefore Apify Proxy, `RESIDENTIAL`, `apifyProxyCountry: "KR"`, and the actor forces `KR`
onto any Apify Proxy config that omits a country.

**Honest caveat:** we could not reproduce the geo-gate ourselves, because our own egress is
Korean. The default is a precaution taken on that report, not on a measurement of ours. It
costs nothing if the report turns out to be wrong, and the first-page diagnostic tells you
which way it went: a zero `<count>` means the server genuinely matched nothing, while a
large `<count>` with no rows means the parser drifted — neither of which a proxy fixes.

Note also that `bike.passo.co.kr` **refuses TCP 443** (verified: connection refused). It
is plain HTTP only. `https://` URLs for this host do not degrade, they fail to connect.

***

### Compliance

Dated evidence is archived in [`compliance/`](compliance/) with a fetch log and checksums —
Passo's 이용약관 carries no 시행일 and Article 3 ② lets the company amend it at will, so
there is no other way to prove later what the terms said today. Read
[`compliance/README.md`](compliance/README.md) before changing anything below.

- **`bike.passo.co.kr` only.** It publishes `Allow: /`. `m.passo.co.kr` publishes
  `Disallow: /` and **shares an IP with it** (116.127.120.244), so the exclusion is a
  thrown error in code (`FORBIDDEN_HOSTS`, `assertAllowedHost()`), not a convention.
- **Never authenticates.** No login, no cookie, no session, no credentials. Article 11's
  duties — including ① 8, profit-making use without consent — are addressed to 회원,
  defined in Article 2 ② as a party who has concluded a use agreement. An anonymous reader
  of `Allow: /` pages is not one. Logging in would make the actor a 회원 and destroy that
  position retroactively.
- **No private-seller personal data.** The catalog row carries no seller field at all. The
  detail page carries 핸드폰 / 전화번호; when detail mode is built those must be dropped in
  the parser.
- **Deliberately slow.** Sequential only, ≥1.5 s between requests, no concurrency control.
  The origin runs PHP 5.2.17 (EOL 2011) and Article 11 ① 6 makes obstructing the company's
  operations a breach, so rate is a terms matter, not etiquette.
- **No image binaries.** `primary_image_url` is a link to Passo's CDN. Nothing is
  downloaded, re-hosted or re-served.

***

### Known limitations

- **`published_date` is the update date.** Passo exposes `<update_date>` and sorts
  "newest" by it, so a refreshed old listing resurfaces at the top. There is no separate
  original-registration date on the row.
- **`sponsored` rows appear on every page.** ~10 per page, rotating, de-duplicated by id.
  They are real active listings; they are just promoted.
- **`badges` decodes three codes.** Passo publishes its `hotmark` badges only as unlabelled
  GIFs with no alt text and no legend. Codes 6 (A/S), 7 (카드) and 12 (할부) were read off
  the images; every other code is returned as a number in `badge_codes` and not guessed at.
- **`product_state_code` is passed through uninterpreted.** The server emits it (values 1
  and 2 observed) but Passo's own front end never reads it and no legend is published.
- **`mileage_unit` can be `ml`.** Rare (1 of 100) but real; `mileage_km` is converted and
  the raw value and unit are kept alongside.
- **Truncated responses.** The origin occasionally ends a chunked response without the
  terminating zero-length chunk. The parser keeps the complete `<item>` elements it
  received and logs a warning rather than discarding a usable page.
- **No detail mode yet.** Everything in the "detail page only" column above is `null`.

***

### FAQ

**Is it legal to scrape this?** This actor reads only public, non-authenticated pages that
Passo's own `robots.txt` marks `Allow: /`, collects no personal data, and never logs in.
See [Compliance](#compliance) for the full reasoning and the archived evidence. You remain
responsible for what you do with the data.

**Why is the URL so ugly?** Because Passo has no server-rendered detail page for used
bikes — the catalogue is a single AJAX page whose state lives in the location hash. The
`#HIS_ID:` form is Passo's own deep-link format, not one we invented; the site ships
anchors in exactly that shape.

**How long does a full pass take?** 221 requests at the 2-second default ≈ 8 minutes.

**What does it cost?** $4 per 1,000 catalog rows. A full ~11,000-listing pass is about
$44. See [PRICING.md](PRICING.md).

***

### ⭐ Found it useful?

If this Actor saves you time, a short review on its Apify Store page helps other buyers find it. Missing a field or hit a bug? Open an issue on the **Issues** tab; reports are answered and fixed.

# Actor input Schema

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

How many catalog rows to collect. Passo had 11,015 active listings on 2026-10-22, reachable in a single unfiltered pass (221 pages of 50) — there is no deep-pagination cap. Set 12000 for the whole active catalogue.

## `maker` (type: `string`):

Optional brand filter. A Korean or English make name as Passo spells it — e.g. '혼다'/'Honda', '야마하'/'Yamaha', 'BMW', '할리데이비슨'/'Harley-Davidson', 'SYM', 'KTM', '존테스'/'Zontes' — or a raw Passo maker_idx (e.g. 34). Blank = all 150 makes. An unrecognised name fails the run rather than silently returning every make.

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

How Passo orders the catalogue before collection. 'newest' is Passo's own default and sorts by listing update date, descending.

## `includeSoldAndReserved` (type: `boolean`):

Off by default, which applies Passo's '판매중' filter (@state:0#) and yields the ~11,000 active listings. Switch on to include 계약중 (reserved) and 판매완료 (sold) as well — that result set is ~863,000 rows and is Passo's full historical archive, not its live inventory.

## `searchExpression` (type: `string`):

Optional raw terms in Passo's own search DSL, appended to the ones built from the fields above. Syntax: @field:value# (equality), @field:>n#, @field:a~b# (range), @field:!v# (exclude), @field:?v# (contains). Examples: '@out_year:2018~2024#' for model years 2018-2024, '@area:?서울#' for Seoul, '@sale_price:300~800#' for 300-800만원. Leave blank unless you know the field names.

## `requestDelaySecs` (type: `integer`):

Pause between sequential requests. Passo's origin is Apache 2.4.54 / PHP 5.2.17 and deep pages are slow on it (page 0 answers in 0.25 s, a very deep page took 7.8 s), so this actor never runs requests in parallel and enforces a hard 1.5 s floor in code. Lowering this is discouraged: Passo's 이용약관 Art. 11(1)6 treats obstructing the company's operations as a breach.

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

Proxy settings. Passo is reported to geo-restrict its catalogue to Korean IPs and to answer a non-Korean IP with HTTP 200 and an empty result rather than an error, so a run on a US/EU IP would collect zero listings and look like an empty market. This therefore defaults to Apify Proxy with the RESIDENTIAL group and country KR, which is what makes API-started runs work. If you set Apify Proxy without a country, the actor forces KR for you. Clear the whole object only if you are running from a Korean network yourself.

## Actor input object example

```json
{
  "maxItems": 100,
  "maker": "",
  "sort": "newest",
  "includeSoldAndReserved": false,
  "searchExpression": "",
  "requestDelaySecs": 2,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "KR"
  }
}
```

# Actor output Schema

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

Every catalog row collected in this run.

# 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("kdatafactory/passo-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("kdatafactory/passo-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 '{}' |
apify call kdatafactory/passo-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kdatafactory/passo-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/l0OTozjeErDtc1fc4/builds/Vz6F3Ro4vKp9StyOh/openapi.json
