If you are trying to scrape OLX Brazil to monitor prices, generate leads, or feed a reselling/arbitrage workflow, you already know the frustrating part: OLX is not a static page you can pull with a single requests.get(), and even when you get the HTML, half of what you need - price, location, seller type - is buried in inconsistent formatting. This post walks through what a DIY OLX scraper actually costs you to build and keep running, and how to get clean structured data (parsed price, full location breakdown, seller type) without babysitting it. Whether you searched "olx scraper", "olx brazil api" or "scrape classified ads" to get here, the tradeoffs below apply either way.
Why scraping OLX is harder than it looks
OLX Brazil, like most modern classifieds sites, renders its listing grid client-side. A plain requests.get() gets you the app shell, not the ads - the actual titles, prices and thumbnails get injected after the page hydrates in the browser. That alone forces you into a headless browser for anything beyond the first few results.
Then there is anti-bot protection. Classifieds are scraped constantly by resellers, price trackers and lead-gen tools, so datacenter IPs get rate-limited or blocked outright after a short burst of requests. You either rotate residential proxies yourself, or your scraper quietly stops returning results a few pages in.
On top of that:
-
Price parsing - listings show
"R$ 3.900"as a formatted string, not a usable number, and some ads carry anoldPricefor markdowns that you have to detect separately - Location - city, neighbourhood, state and DDD area code often arrive as one free-text blob you have to split and normalize yourself
-
Pagination - OLX search results page past a
?page=parameter that stops behaving predictably once you combine it with region and category filters - Layout drift - selectors quietly break whenever OLX ships a redesign, and you get empty or wrong fields with no error thrown
None of these is hard in isolation. Together, they are why "just scrape OLX" turns into a maintenance job nobody signed up for.
Approach 1: DIY with Python
Attempt 1: plain requests (does not work)
import requests
r = requests.get("https://www.olx.com.br/brasil?q=iphone+15+pro")
print(r.status_code, len(r.text))
# 200, but the listing grid is empty in the raw HTML.
# Ad data is fetched and rendered client-side after the page loads.
The response comes back 200 OK with no ad data inside it. You need a real browser to see what a user sees.
Attempt 2: headless browser with Playwright
from playwright.sync_api import sync_playwright
def scrape_olx(term="iphone 15 pro", max_ads=50):
listings = []
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto(f"https://www.olx.com.br/brasil?q={term.replace(' ', '+')}")
page.wait_for_selector(".olx-ad-card", timeout=15000)
cards = page.query_selector_all(".olx-ad-card")
for card in cards[:max_ads]:
title = card.query_selector(".olx-ad-card__title")
price_raw = card.query_selector(".olx-ad-card__price")
location = card.query_selector(".olx-ad-card__location")
listings.append({
"title": title.inner_text() if title else None,
"price_raw": price_raw.inner_text() if price_raw else None,
"location_raw": location.inner_text() if location else None,
})
browser.close()
return listings
def parse_price(price_raw: str) -> int:
# "R$ 3.900" -> 3900
digits = "".join(c for c in price_raw if c.isdigit())
return int(digits) if digits else 0
Selector names above are illustrative - the real ones will differ, and that is exactly the point: the moment you scale this past a handful of pages or run it on a schedule from a server, requests start getting blocked, the selectors break on the next redesign, and location parsing ("Tubarão - Centro, SC" split three different ways depending on the listing type) becomes its own small project.
The real cost of DIY isn't writing the scraper, it's keeping it alive
A one-off script for twenty ads is genuinely fine to write yourself. The cost shows up when you need this reliably, on a schedule, at volume.
| DIY (Playwright + your own infra) | Managed scraper (API) | |
|---|---|---|
| Upfront cost | Free (your time) | Pay per ad returned |
| JS rendering | You maintain the headless browser | Handled for you |
| IP blocking | You buy and rotate residential proxies | Already routed through a residential IP |
| Selector breakage | You fix it every time OLX changes | Maintained on the provider's side |
| Price / location parsing | You write and maintain it | Comes back typed (price: 3900, split location fields) |
| Scheduling | You wire up cron + monitoring | Native scheduler on the platform |
Neither column is objectively "right." If you need thirty listings once for a one-off comparison, the Playwright script above will get you there. If you need this running daily at hundreds or thousands of ads, the maintenance tax is the real cost, not the code.
Approach 2: a ready-made OLX scraper (API)
This is the part where I show you the shortcut. OLX Brazil Scraper is an Apify actor that handles the JS rendering, residential IP routing, pagination and price/location parsing described above, and returns structured JSON per listing.
Input
| Field | Type | Description | Example |
|---|---|---|---|
term |
string | Search keyword to look up on OLX Brazil | iphone 15 pro |
url |
string | Full OLX search or category URL (advanced, overrides term) |
https://www.olx.com.br/... |
region |
string | OLX region slug to scope the search |
brasil, estado-sp
|
maxAds |
number | Maximum number of ads to scrape | 100 |
{
"term": "iphone 15 pro",
"region": "brasil",
"maxAds": 100
}
Paste any already-filtered OLX URL into url when you need pixel-perfect targeting instead of a plain keyword.
Output
{
"listId": 1455857240,
"title": "iPhone 15 Pro 256GB Titânio Natural",
"price": 3900,
"priceText": "R$ 3.900",
"oldPrice": null,
"category": "Celulares e telefonia",
"municipality": "Tubarão",
"neighbourhood": "Centro",
"state": "SC",
"ddd": "48",
"date": "2026-06-17T14:32:00",
"imageCount": 6,
"professionalAd": false,
"url": "https://www.olx.com.br/...",
"thumbnail": "https://img.olx.com.br/..."
}
price comes back as an integer, not a "R$ 3.900" string, so df["price"].median() just works. Location is already split into municipality, neighbourhood, state and ddd, so geographic analysis (by state, by area code) needs no text parsing on your side.
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').call({
term: 'iphone 15 pro',
region: 'brasil',
maxAds: 100,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.length, 'ads');
console.log(items[0]);
Calling it from Python
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("plum_spear/aztec-olx").call(run_input={
"term": "iphone 15 pro",
"region": "brasil",
"maxAds": 100,
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
print(item["title"], item["price"], item["state"])
Calling it from the CLI or plain REST
apify call plum_spear/aztec-olx --input '{"term": "iphone 15 pro", "region": "brasil", "maxAds": 100}'
curl "https://api.apify.com/v2/acts/plum_spear~aztec-olx/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"term": "iphone 15 pro", "region": "brasil", "maxAds": 100}'
What people actually build with this
- Price monitoring and repricing. Run the same term daily and track how the price distribution moves; filter for anything below your target and alert your team.
- Market research and segment analysis. Pull a broad category and compute average price, price spread, and geographic distribution by state and DDD for a report.
-
Lead generation. Filter by niche and region, then export sellers with location and
professionalAdstatus to build prospecting lists for a sales team. -
Resellers and arbitrage. Diff new listings by
listIdon a frequent schedule and act on underpriced items before anyone refreshing the page by hand does. -
Real estate professionals. Scrape
apartamento,casaorterrenolistings by city or state to benchmark asking prices per neighbourhood. -
Car dealers and auto traders. Monitor used-car listings (
civic,corolla,hb20) across regions and price stock against live market data.
Pricing
Pay-per-event: $0.15 per 1,000 ads returned, plus a minimal actor-start event. No subscription, no monthly minimum - you pay for the data you pull, not for idle time. Apify's free monthly platform credits let you run a real query against your own use case before deciding whether it's worth it.
For context: if the DIY route above costs you a residential proxy subscription plus a few hours a month keeping selectors and location parsing alive, $0.15 per 1,000 ads is the kind of number that stops being a debate past a certain volume. Below that volume, write the script above - it's genuinely fine.
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 term / region / maxAds input, no separate integration to write.
Wrap-up
Scraping OLX Brazil yourself is doable, and for a one-off pull it's probably the right call - the Playwright snippet above will get you there. What it won't do on its own is survive a redesign, dodge IP blocking at scale, or hand you typed, geographically-split data you can pipe straight into a spreadsheet or database. That gap is what OLX Brazil Scraper on Apify closes: give it a term and a maxAds, get back clean JSON with parsed prices, full location breakdown and seller type, 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)