# OLX Listings Scraper (PL, UA, KZ, UZ) (`fetchfoundry-labs/olx-listings-scraper`) Actor

Classified listings from OLX Poland, Ukraine, Kazakhstan and Uzbekistan: price with currency, category branch, city and coordinates, seller, photos and the category-specific attributes. Paid placements are flagged, not blended in, and both of OLX's contradictory totals are reported.

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

## Pricing

from $1.75 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
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?

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

## OLX Listings Scraper (PL, UA, KZ, UZ)

Classified listings from **OLX** — Poland, Ukraine, Kazakhstan and Uzbekistan
— as structured rows: price with its currency, the full category branch, city
and coordinates, seller, photos, and the category-specific attributes OLX
attaches to every ad.

***

### The thing that makes this different

**About a quarter of every OLX result page is advertising, and OLX tells you
which rows — but only as positions.**

Ask for 40 listings and 52 come back. The extra twelve are paid placements,
injected on top of the page size you requested. OLX publishes them in
`metadata.promoted` as **indexes into the results array** — `[0, 1, 2, 13,
14, 15, 26, ...]` — not as ids or flags on the rows themselves.

That has two consequences most scrapers miss:

1. **Slicing `data[:limit]` drops real listings.** The overflow is not at the
   end; the ads are interleaved from position 0.
2. **Those indexes stop being true the moment anything reorders the list.**
   Dedupe, sort or filter first and the labels land on the wrong offers.

This Actor resolves the positions to offer ids on the raw page, before
anything else touches it. Every row carries `isPromoted`, and each search
reports `promotedReturned` and `promotedShare` — measured at **0.231** on a
typical page. Set `includePromoted: false` to drop the ads; the count is
still reported, so removing them does not also hide that they were there.

**And the total you are shown is not the total you can have.** OLX reports
`total_elements: 1000` on every query however large the result set — that is
the pageable cap, not a count — while `visible_total_count` says 159,397
matched. Both are emitted, named for what they are: `pageableOffers` and
`matchingOffers`. The second one is a live estimate and visibly moves between
identical calls, so it is flagged as one.

***

### Countries, and why only four

OLX runs one platform behind a dozen national domains, and their `robots.txt`
files do not agree. Poland opts the search endpoint **in by name**
(`Allow: /api/v1/offers/`); Romania, Bulgaria and Portugal carry
`Disallow: /api/` with no such exception; Brazil, Bosnia and India answer
`robots.txt` itself with a challenge page.

Only the four that permit it are offered here. Asking for any other country is
refused at input validation, with the reason. The full per-domain table is in
[CRAWLING_METHOD.md](CRAWLING_METHOD.md).

***

### What you get

One `SEARCH_SUMMARY` row per keyword, then one `OFFER` row per listing.

| Group | Fields |
| --- | --- |
| Identity | `offerId`, `title`, `url`, `description`, `status`, `offerType` |
| Price | `price`, `priceCurrency`, `priceLabel`, `priceNegotiable`, `priceArranged`, `priceIsFree`, `previousPrice` |
| Placement | `isPromoted`, `isHighlighted`, `isTopAd`, `promotionOptions`, `resultPosition` |
| Category | `categoryId`, `categoryName`, `categoryType`, `categoryPath` (full branch) |
| Location | `cityName`, `regionName`, `districtName`, `latitude`, `longitude`, `geoPrecisionRadius` |
| Seller | `sellerId`, `sellerName`, `sellerIsBusiness`, `sellerSince`, `sellerCompanyName`, `shopSubdomain` |
| Timing | `createdTime`, `lastRefreshTime`, `pushupTime`, `validToTime` |
| Attributes | `attributeCount`, `attributes` (key / name / value, category-specific) |
| Media | `photoCount`, `photos` |
| Provenance | `_input`, `_source`, `_scrapedAt`, `country`, `sourceHost` |

`categoryPath` walks OLX's own tree to the root, so a phone comes back as
`["Elektronika", "Telefony", "Smartfony i telefony komórkowe", "iPhone"]`
rather than a bare id.

`attributes` are category-specific and cannot be fixed columns — a phone has
`state`, `phonemodel`, `builtinmemory_phones`; a flat has floor area and room
count. They are kept as a counted list so that a consumer flattening to CSV
at least sees what it dropped.

***

### Input

| Field | Notes |
| --- | --- |
| `country` | `pl`, `ua`, `kz` or `uz`. Sets the host and the currency. |
| `queries` | Keywords, one search each. An empty string lists a whole category. |
| `category` | Numeric id **or** OLX's own path (`nieruchomosci`). Checked against the published tree before any request. |
| `regionId` | Numeric. Read it off a row from an unfiltered run — see the warning below. |
| `sortBy` | `relevance`, `newest`, `priceAsc`, `priceDesc`. |
| `priceFrom` / `priceTo` | In the country's own currency. |
| `includePromoted` | Keep the paid placements (default) or drop them. |
| `maxPages`, `pageSize`, `maxItems` | `pageSize` maxes at 50; OLX refuses 100. |

**A note on `regionId`.** An unknown category id is refused with HTTP 400, but
an unknown *region* id comes back as **HTTP 200 with zero rows** —
indistinguishable from a search that genuinely matched nothing. There is no
published region list to validate against, so take the id from a real row
rather than guessing at one.

**A note on `sortBy`.** OLX refuses the plausible value `relevance` with
HTTP 400 and silently ignores outright nonsense like `banana:desc`. Its own
error is therefore not a validity test, so sort values are checked here
before the request.

***

### Limits, stated plainly

- **~1,000 organic listings per search**, whatever the reported match count.
  OLX stops paging at offset 1040. To go deeper, narrow the search — by
  category, region or price — rather than raising `maxPages`.
- **No anti-bot layer was found.** The endpoint answers cold on six TLS
  profiles with no cookie or warm-up, so a proxy is optional and off by
  default.
- Every run reports why it stopped (`stoppedReason`: `no_new_ids`,
  `offset_ceiling`, `max_pages` or `max_items`), so a short run is never
  ambiguous.

Method, measurements and the full trap list: [CRAWLING_METHOD.md](CRAWLING_METHOD.md).

# Actor input Schema

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

Which OLX site to read. Only these four are offered: their own robots.txt permits the offers endpoint. OLX Romania, Bulgaria and Portugal disallow /api/ with no exception, and OLX Brazil, Bosnia and India answer robots.txt with a challenge page, so none of those are available here.

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

Keywords to search, one run each with its own summary row. Note that a keyword OLX does not recognise does NOT return zero: it falls back to fuzzy matches, so rows coming back is not proof the keyword was understood. When OLX substitutes something it is reported in the summary's searchSuggestion. Pass an empty string to list a whole category without a keyword.

## `category` (type: `string`):

Optional category, given either as a numeric id (for example 99) or as OLX's own path (for example 'nieruchomosci'). It is checked against the category tree OLX publishes on its own listing page before any request is spent, and an unknown value is refused with examples rather than guessed at.

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

Optional numeric region id. Be careful with this one: OLX answers an unknown region id with HTTP 200 and zero rows, which is indistinguishable from a search that genuinely matched nothing. Read the regionId off a row from an unfiltered run rather than guessing.

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

Result ordering. Checked against this list before the request, because OLX refuses the plausible value 'relevance' with HTTP 400 while silently ignoring outright nonsense - so its own error is not a validity test.

## `priceFrom` (type: `integer`):

Lower price bound, in the country's own currency. Verified to genuinely move the match count.

## `priceTo` (type: `integer`):

Upper price bound, in the country's own currency.

## `includePromoted` (type: `boolean`):

OLX injects paid placements on top of every page - about 12 extra rows per page of 40 - and publishes which positions they occupy. They are kept by default and flagged with isPromoted so you can tell them apart. Turn this off to keep organic results only; either way the summary reports how many there were.

## `maxPages` (type: `integer`):

Pages per keyword. OLX caps paging at offset 1040 whatever the reported match count, so about 1,000 organic listings is the hard ceiling per search even when it says 159,000 matched. To go deeper, narrow the search rather than raising this.

## `pageSize` (type: `integer`):

Organic listings requested per page. 50 is OLX's ceiling - 100 is refused. Promoted listings arrive on top of this number, so a page of 50 returns about 65 rows.

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

Optional overall cap on listing rows across every search. Summary rows are always emitted and do not count against it.

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

Parallel requests across searches.

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

Politeness delay between request starts. It paces starts only and does not hold a concurrency slot, so raising it slows the run without idling the workers.

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

Optional. Not needed for OLX in testing; use it if you are running at volume or from a blocked network.

## Actor input object example

```json
{
  "country": "pl",
  "queries": [
    "iphone"
  ],
  "sortBy": "relevance",
  "includePromoted": true,
  "maxPages": 5,
  "pageSize": 50,
  "maxConcurrency": 3,
  "minRequestInterval": 0,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("fetchfoundry-labs/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 = {}

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

```

## MCP server setup

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