DEV Community

Roberto Kerber
Roberto Kerber

Posted on Edited on

How to Scrape OLX Europe: Classified Ads & Prices Across Markets (2026)

If you are trying to scrape OLX across several European markets - Portugal, Poland, Romania, Bulgaria, Ukraine, Kazakhstan - to monitor prices, generate leads, or compare a category across countries, you already know the problem multiplies with every market you add: it is not one scraper you have to maintain, it is potentially six. This post walks through what a DIY multi-country OLX scraper actually costs you, and how to get clean structured data across all six markets from a single input without maintaining six scrapers. Whether you searched "olx scraper europe" or "olx api" to get here, the tradeoffs below apply either way.

Why scraping OLX across multiple countries is harder than it looks

Each OLX country storefront runs on the same underlying platform (the CDN paths give it away - thumbnails come from apollo.olxcdn.com across markets), which means a scraper built for one country transfers conceptually to the others. In practice that "transfer" is exactly where the work hides.

The listing grid renders client-side, so a plain requests.get() returns an app shell with no ad data in it, in every country - you need a real browser or a way to call the same internal API the page itself calls. Anti-bot protection is applied per market: datacenter IPs get rate-limited or blocked, and you need a residential IP that can actually reach the country storefront you're targeting, not just any residential IP.

Then the parts that look small until you multiply them by six:

  • Currency and formatting - price shows as "450 €" in Portugal, differently formatted in Polish złoty or Romanian leu; you parse each format or write one parser robust enough for all of them
  • Category IDs - OLX category taxonomy is not identical across countries, so a categoryId that means "phones" in one market does not necessarily mean the same thing in another
  • Language - titles, descriptions and category names come back in the local language, which affects any keyword-based filtering you do downstream
  • Layout drift, per country - a redesign can ship to one storefront before the others, so "it broke" can mean one-sixth of your pipeline silently going empty while the rest keeps working

None of this is hard for a single country. Maintaining it correctly across six, with someone noticing the moment one of them silently breaks, is the actual cost.

Approach 1: DIY with Python

Attempt 1: plain requests (does not work)

import requests

r = requests.get("https://www.olx.pt/ads/q-iphone/")
print(r.status_code, len(r.text))
# 200, but the ad grid is empty in the raw HTML - same story
# on every OLX country storefront, not just Portugal.
Enter fullscreen mode Exit fullscreen mode

Attempt 2: one Playwright scraper, looped over countries

from playwright.sync_api import sync_playwright

OLX_DOMAINS = {
    "pt": "olx.pt", "pl": "olx.pl", "ro": "olx.ro",
    "bg": "olx.bg", "ua": "olx.ua", "kz": "olx.kz",
}

def scrape_olx_country(query, country="pt", max_ads=50):
    domain = OLX_DOMAINS[country]
    listings = []
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        page.goto(f"https://www.{domain}/ads/q-{query}/")
        page.wait_for_selector("[data-cy='l-card']", timeout=15000)

        cards = page.query_selector_all("[data-cy='l-card']")
        for card in cards[:max_ads]:
            title = card.query_selector("h6")
            price_raw = card.query_selector("[data-testid='ad-price']")
            listings.append({
                "title": title.inner_text() if title else None,
                "price_raw": price_raw.inner_text() if price_raw else None,
                "country": country,
            })
        browser.close()
    return listings

def parse_price(price_raw: str) -> float:
    # "450 €" -> 450.0 - written for one currency format,
    # needs testing against every market's actual formatting
    digits = "".join(c for c in price_raw if c.isdigit() or c == ",")
    return float(digits.replace(",", ".")) if digits else 0.0
Enter fullscreen mode Exit fullscreen mode

Selector names above are illustrative. This works for one market at a time, on your own IP, for a small run. Scale it to six countries on a schedule and you now own six IP-blocking surfaces, six selector-drift risks, and a price parser that has to be correct for six currency formats - not one problem, six copies of it.

The real cost of DIY isn't one scraper, it's six of them staying in sync

Pulling a handful of listings from one country once is genuinely fine to hand-roll. The cost shows up when you need consistent, structured coverage across markets, on a schedule.

DIY (Playwright + your own infra, x6 countries) Managed scraper (API)
Upfront cost Free (your time, x6 markets) Pay per ad returned
JS rendering You maintain a headless browser per market Handled for you
IP blocking Residential proxies routed per country Already routed through a residential IP
Currency / price parsing You write and test six formats Comes back as one typed price field
Selector breakage You detect and fix it per market Maintained on the provider's side
Cross-country comparison You normalize schemas yourself Same schema everywhere, country is one field

Neither column is objectively "right." If you need a one-off pull from a single country, the Playwright script above is enough. If you need consistent coverage across several of the six markets, on a schedule, the multiplication is the real cost, not the code.

Approach 2: a ready-made OLX Europe scraper (API)

This is the part where I show you the shortcut. OLX Europe Scraper is an Apify actor that covers all six markets - Portugal, Poland, Romania, Bulgaria, Ukraine, Kazakhstan - from a single input, over a residential IP, with prices already parsed and a consistent schema across countries.

Input

Field Type Description Example
query string Search keyword to look up on OLX iphone
country string pt, pl, ro, bg, ua, or kz (default pt) pt
categoryId string Optional OLX category ID to narrow the search 1953
maxAds number Maximum number of ads to scrape (default 100) 100
{
  "query": "iphone",
  "country": "pt",
  "maxAds": 100
}
Enter fullscreen mode Exit fullscreen mode

Change only country to run the identical query against a different market - no separate config, no separate parser.

Output

{
  "id": 1093847562,
  "title": "iPhone 13 128GB Azul - Como Novo",
  "price": 450,
  "priceLabel": "450 €",
  "description": "iPhone 13 em excelente estado, 128GB, com caixa e carregador. Bateria a 92%.",
  "city": "Lisboa",
  "region": "Lisboa",
  "created": "2026-06-19T10:14:00+01:00",
  "refreshed": "2026-06-23T08:02:00+01:00",
  "business": false,
  "category": "Telemóveis",
  "imageCount": 7,
  "thumbnail": "https://apollo.olxcdn.com/v1/files/...",
  "url": "https://www.olx.pt/d/anuncio/..."
}
Enter fullscreen mode Exit fullscreen mode

price is a parsed number in every country's response, priceLabel keeps the original string for display, and business tells professional sellers apart from private ones - the same three fields, same meaning, regardless of which of the six markets the ad came from.

Calling it from JavaScript

import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });

const run = await client.actor('plum_spear/aztec-olx-eu').call({
  query: 'iphone',
  country: 'pt',
  maxAds: 100,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.length, 'ads');
console.log(items[0]);
Enter fullscreen mode Exit fullscreen mode

Calling it from Python

from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("plum_spear/aztec-olx-eu").call(run_input={
    "query": "iphone",
    "country": "pt",
    "maxAds": 100,
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["title"], item["price"], item["city"])
Enter fullscreen mode Exit fullscreen mode

Calling it from the CLI or plain REST

apify call plum_spear/aztec-olx-eu --input '{"query": "iphone", "country": "pt", "maxAds": 100}'
Enter fullscreen mode Exit fullscreen mode
curl "https://api.apify.com/v2/acts/plum_spear~aztec-olx-eu/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"query": "iphone", "country": "pt", "maxAds": 100}'
Enter fullscreen mode Exit fullscreen mode

What people actually build with this

  • Price monitoring across markets. Track average, minimum and median price for the same term in several countries and get alerted when a listing drops below your target.
  • Cross-country market research. Run the same keyword in Portugal, Poland and Romania and compare price spread, ad volume and business-vs-private mix side by side.
  • Lead generation. Use the business flag plus location fields to build prospecting lists of professional sellers by country.
  • Cross-border resale and arbitrage. Compare prices for the same item across markets and flip the gap - the numeric price field makes this a direct comparison, not manual guesswork.
  • Real estate professionals. Scrape apartment, house and land listings by city and region in Poland, Romania, Portugal and beyond.
  • Car dealers and auto traders. Monitor used-car listings across countries and separate dealer ads from private sellers with the business flag.

Pricing

Pay-per-event: $0.15 per 1,000 ads returned, plus a minimal actor-start event. No subscription, no monthly minimum, and the same price whether you run it against one country or all six. Apify's free monthly platform credits let you test it against a real query before deciding whether it's worth it.

For context: if the DIY route above means standing up and maintaining six country-specific scrapers with six currency parsers, $0.15 per 1,000 ads with one consistent schema is the kind of number that stops being a debate fast, especially once you need more than one market at once.

Using it from an AI agent (MCP)

If you're wiring this into an agent instead of a script, actors published on Apify, including this one, are reachable through Apify's MCP server, which exposes them as callable tools for MCP-compatible clients. Same query / country / maxAds input, no separate integration to write.

Wrap-up

Scraping one OLX country yourself is doable - the Playwright snippet above will get you there for a single market. What gets expensive is doing that six times, keeping six selector sets and six currency parsers alive, and normalizing the results yourself before you can compare countries. That gap is what OLX Europe Scraper on Apify closes: give it a query and a country, get back the same clean schema across all six markets, priced at $0.15 per 1,000 ads with no monthly commitment.


💡 Precisa monitorizar a sua marca nos assistentes de IA?
O GEO Tracker analisa a presença da sua empresa no ChatGPT, Gemini, Perplexity, Claude e mais — relatório PDF em 24h.

Top comments (0)