# OLX Listings Scraper (`scrapyx/olx-listings-scraper`) Actor

Search live OLX Group marketplace listings across 7 countries (Poland, Ukraine, Kazakhstan, Uzbekistan, Romania, Bulgaria, Portugal) — price, condition, seller, photos and location, routed to whichever surface each country's robots.txt actually allows.

- **URL**: https://apify.com/scrapyx/olx-listings-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** E-commerce, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.05 / 1,000 results

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

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#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

## OLX Listings Scraper — PL/UA/KZ/UZ/RO/BG/PT

Search live OLX Group classifieds across **7 countries** — Poland, Ukraine,
Kazakhstan, Uzbekistan, Romania, Bulgaria, Portugal — with one unified
schema. No login, no cookies, no browser. HTTP only.

### Why one actor across 7 countries

Every OLX Group country domain runs the identical platform and answers the
identical ad-object shape — verified by cross-checking fields between the
JSON API and the site's own server-rendered state. The only real variation
between countries is **transport**, and that variation is forced by each
country's own `robots.txt`, not by choice:

| Countries | `robots.txt` says | Strategy used |
| --- | --- | --- |
| Poland, Ukraine, Kazakhstan, Uzbekistan | `/api/` allowed | `S1-api` — the JSON search API directly |
| Romania, Bulgaria, Portugal | `/api/` **disallowed**, search+detail HTML allowed | `S2-ssr` — the site's own server-rendered `__PRERENDERED_STATE__`, same ad fields |

A user picks `country`; the actor picks the strategy that host's own policy
permits. This is a routing table, not a `mode` switch — there's exactly one
behaviour (search → listings) and the registry just says how to reach it.

### What you get

One record type per search result, one `SEARCH_SUMMARY` per query.

#### `LISTING`

| Field | Notes |
| --- | --- |
| `adId`, `title`, `description`, `url` | |
| `priceAmount` | **`None` for genuinely priceless listings** — swap-only ("Zamienię") and giveaway ("Za darmo") ads carry no real number. See `priceType`. |
| `priceType` | `price` (fixed) / `arranged` (negotiable, still a real number) / `free` / `exchange` — the honest signal for "is there a price at all" |
| `priceCurrency`, `priceNegotiable`, `priceLabel` | |
| `city`, `region` | |
| `category`, `condition`, `params` | `params` is every category-specific attribute (model, mileage, size, …) as raw passthrough — no fixed schema is possible across OLX's whole category tree |
| `photos`, `photoCount` | |
| `isBusiness`, `isPromoted`, `sellerName` | |
| `createdAt`, `validToAt` | ISO 8601 UTC |

#### `SEARCH_SUMMARY` — one per query

`reachableCount` vs `upstreamTotalCount` are **different numbers, both
real**: OLX's own API caps `total_elements` at 1000 regardless of how many
results actually exist (`visible_total_count` carries the true figure — a
popular query can show 148,000+ there while `reachableCount` stays 1000).
Also reports `pagesFetched`, `duplicatesSkipped` (see below), and
`queryEchoed` — the literal query string upstream received, useful because
OLX's own search falls back to fuzzy token matching on a query with no real
hits rather than returning empty.

#### `ERROR` — one per query that failed

Every query maps to at least one row.

### Known limits — read before relying on volume figures

- **Offset ceiling is exactly 1000**, enforced identically on every query
  regardless of popularity. This is a platform limit, not something this
  actor could paginate past.
- **Promoted ads recycle independently of pagination.** OLX interleaves
  paid-placement listings into every page at fixed slots, and once a
  query's promoted pool runs dry it starts repeating earlier promoted ids.
  Deduplicated automatically — `duplicatesSkipped` on `SEARCH_SUMMARY`
  reports how many were dropped, typically ~5% of a deep (1000-item) walk.
- **Romania/Bulgaria/Portugal return one page only.** Their robots.txt
  allows the search page but not the paginated API, and the page itself
  doesn't expose a working offset parameter for deeper pages without a
  browser — out of scope for an HTTP-only actor. `notes` on the summary
  row says so explicitly when it happens.
- **A nonsense search query does not come back empty.** This is upstream's
  own site behaviour (their search box does the same fuzzy fallback), not a
  bug — `queryEchoed` lets you see exactly what was searched.

### Countries probed and found out of scope this session

`olx.ba`, `olx.com.br` — robots.txt itself answers behind a Cloudflare
challenge (policy unreadable). `olx.in`, `olx.co.za`, `olx.com.pk` — a
different, non-OLX-Group platform (404s / no JSON, no `__PRERENDERED_STATE__`).

### Proxy

Residential is the default. No WAF was observed anywhere in this family
during recon (6-profile TLS ladder × 7 hosts × both surfaces, all 200 OK),
but a shared datacenter IP is still the fastest way onto a rate limit on a
high-traffic marketplace.

# Actor input Schema

## `country` (type: `string`):

Which OLX Group marketplace to search. Each country routes to whichever surface its own robots.txt actually allows for this actor (a JSON API on 4 of them, the site's own server-rendered search page on the other 3) — both return the identical listing shape either way.

## `queries` (type: `array`):

One or more search terms to run, e.g. "iphone 15", "mieszkanie warszawa". Each query is fetched independently and all land in the same dataset. Note: OLX's own search falls back to fuzzy/token matching on a query with no real hits rather than returning zero — every SEARCH\_SUMMARY row echoes the exact query upstream received (queryEchoed) so you can see what was actually searched.

## `maxItemsPerQuery` (type: `integer`):

Upper bound on listings returned per query. OLX's own API enforces a hard offset ceiling of 1000 regardless of how popular the search term is — see the SEARCH\_SUMMARY row's reachableCount vs upstreamTotalCount for the gap between what's reachable and what upstream claims exists. Countries routed through the server-rendered surface (Romania, Bulgaria, Portugal) return only one page's worth regardless of this setting — their own robots.txt disallows the paginated API.

## `minPrice` (type: `integer`):

Keep only listings priced at or above this amount, in the country's local currency (PLN/UAH/KZT/UZS/RON/BGN/EUR). Verified server-side — this genuinely filters upstream, it isn't applied after the fact.

## `maxPrice` (type: `integer`):

Keep only listings priced at or below this amount, in the country's local currency.

## `cityId` (type: `integer`):

Restrict to one OLX city id (find it by browsing the site and noting the id in a listing's URL/location, or leave empty for nationwide). Only applies to API-routed countries (pl/ua/kz/uz).

## `regionId` (type: `integer`):

Restrict to one OLX region/voivodeship id. Only applies to API-routed countries (pl/ua/kz/uz).

## `sortBy` (type: `string`):

How upstream orders results. Leave empty for OLX's own default relevance order. Only applies to API-routed countries — an unrecognised value is rejected by upstream itself, and this actor validates it up front for the same reason.

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

How many queries run in parallel. Each query paginates sequentially on its own, so this bounds cross-query parallelism.

## `minRequestInterval` (type: `integer`):

Politeness pacing applied to request STARTS across all workers. 0 disables pacing.

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

Residential proxy is the default. No WAF was observed anywhere in this family during recon, but a shared datacenter IP is still the fastest way to get rate-limited on a high-traffic marketplace.

## Actor input object example

```json
{
  "country": "pl",
  "queries": [
    "iphone"
  ],
  "maxItemsPerQuery": 200,
  "maxConcurrency": 4,
  "minRequestInterval": 0,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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 = {
    "queries": [
        "iphone"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/olx-listings-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 = { "queries": ["iphone"] }

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/olx-listings-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 '{
  "queries": [
    "iphone"
  ]
}' |
apify call scrapyx/olx-listings-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/olx-listings-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/stSoB9qtwVbBYwVH5/builds/kGKOjI0WysOj8OM1k/openapi.json
