DEV Community

Cover image for How to Scrape Otomoto Car Listings in Python with Otomoto Scraper
ReventaData
ReventaData

Posted on Originally published at apify.com

How to Scrape Otomoto Car Listings in Python with Otomoto Scraper

By the end of this tutorial you will have a Python script that pulls car listings from otomoto.pl into structured rows: asking price, make, model, year, mileage, fuel, gearbox, engine power, town, voivodeship and seller.

Otomoto is a Polish car marketplace. Otomoto Scraper (reventadata/otomoto-scraper) is the Apify Actor built for this site to extract its passenger-car listings. You send it a search, and it returns one row per listing.

This guide runs it from Python with the Apify API client. The sample rows below are real Golf rows from the 15 November 2026 run of the Actor's public example task, not invented ones.


Prerequisites

You need three things:

  1. Python 3 with pip.
  2. A free Apify account, which you can create with an email address or a GitHub login.
  3. Your API token, from Settings > API & Integrations in Apify Console.

Install the client and export the token:

pip install -U "apify-client~=3.0"
export APIFY_API_TOKEN="your_apify_token_here"
Enter fullscreen mode Exit fullscreen mode

No scraping code is needed on your side. The Actor does the fetching.


What you'll achieve

You will search otomoto.pl for the newest listings under the term golf, read ten rows in Python and print one line per car. Then you will count how many of those rows actually mention the term, to see that a search term here selects a make or model.

The same script works for a dealer watching competitors' stock, a buyer comparing mileage and price for one model, or an analyst building a table of listings by voivodeship. Only the input changes, and the rest of the script stays as it is. Along the way you will see which fields can be empty, which matters as soon as you compare cars.


Step 1: Open Otomoto Scraper

Open the Otomoto Scraper page on Apify Store. The Try for free button there opens the Actor in Apify Console. This tutorial uses the API from Python, so you only need the Actor name, reventadata/otomoto-scraper.

The Actor covers passenger cars only. Other Otomoto categories are not part of it.


Step 2: Choose the search

The search is described by these input fields:

  • searchQueries: a list of search terms, one search per term, each matched to Otomoto's makes and models: golf, seria 3, bmw, volkswagen passat. A term that names no make or model is skipped; if none does, the run fails. Leave it empty and the Actor browses all passenger cars with the other filters.
  • make and model: slugs as they appear in Otomoto URLs, for example bmw and seria-3. A model needs a make. Without a search term they narrow the search itself; with one, they keep only that make or model among the term's matches.
  • minPrice and maxPrice: in PLN.
  • minYear and maxYear: production year.
  • fuelType: any or electric. Any other value is refused before any page is read.
  • sellerType: any, private or professional (dealers). Any other value is refused before any page is read.
  • sortBy: newest (the default), priceAsc, priceDesc or relevance.
  • requireKeywordMatch: true drops rows whose title, make, model and version do not contain every word of the search term. Off by default, because a term already selects a make or model.
  • maxItems and maxPages: caps on rows (default 1000) and on pages per search (default 50). A page holds 32 listings plus one pinned listing.

Search terms are makes and models (golf, seria 3, bmw). A term that names neither, such as vw or gti, is skipped, and the run fails if no term matches.

Private sellers are filtered by Otomoto itself. Dealers are filtered by the Actor after a page is read, so a dealers-only search walks the same result pages as an any-seller search and may need more of them to fill maxItems.

To list a model from dealers only, with no search term, the input would look like this:

{
    "make": "bmw",
    "model": "seria-3",
    "minYear": 2018,
    "maxPrice": 120000,
    "sellerType": "professional",
    "maxItems": 50,
    "maxPages": 2
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Run it from Python

This script starts the Actor, waits for it, reads the dataset and prints one line per listing, then counts the rows that mention the search term:

import os
from apify_client import ApifyClient

# 1. Initialise the client with your token from the environment
client = ApifyClient(os.environ["APIFY_API_TOKEN"])

# 2. Describe the search: the newest listings for the term "golf", ten rows, one page
run_input = {
    "searchQueries": ["golf"],
    "sortBy": "newest",
    "maxItems": 10,
    "maxPages": 1,
}

# 3. Start the Actor and wait for it to finish
run = client.actor("reventadata/otomoto-scraper").call(run_input=run_input)
if run is None or run.status != "SUCCEEDED":
    status = run.status if run else "unknown"
    raise SystemExit(f"Run did not succeed (status: {status}).")

# 4. Read the rows from the run's default dataset
items = client.dataset(run.default_dataset_id).list_items().items
print(f"Retrieved {len(items)} listings.\n")

# 5. One line per listing
for item in items:
    print(
        f"{item.get('title')} | {item.get('year')} | {item.get('mileageKm')} km | "
        f"{item.get('price')} {item.get('currency')} | {item.get('city')} | {item.get('sellerType')}"
    )

# 6. How many rows mention the search term in title, make, model or version?
term = run_input["searchQueries"][0].lower()
mentions = sum(
    term in " ".join(str(item.get(key) or "") for key in ("title", "make", "model", "version")).lower()
    for item in items
)
print(f"\n{mentions} of {len(items)} rows mention '{term}' in title, make, model or version.")
Enter fullscreen mode Exit fullscreen mode

Keep maxPages small while you test.


Step 4: Read the output

The script prints this on the ten rows of the 15 November 2026 example-task run (each with matchedKeyword set to golf):

Retrieved 10 listings.

Volkswagen Golf 1.4 TSI BMT Comfortline Perfectline | 2015 | 256000 km | 17177 PLN | Zduny | private
Volkswagen Golf 1.4 TSI Highline | 2011 | 89596 km | 25900 PLN | Brzeziny | private
Volkswagen Golf 2.0 TSI GTI | 2010 | 227000 km | 19500 PLN | Wrocław | private
Volkswagen Golf 1.4 FSI Comfortline | 2005 | 162461 km | 12000 PLN | Tarnobrzeg | private
Volkswagen Golf 2.0 TDI BMT Highline | 2014 | 168000 km | 33500 PLN | Międzyrzecz | private
Volkswagen Golf 1.2 TSI BMT Comfortline | 2016 | 107488 km | 39900 PLN | Tanowo | private
Volkswagen Golf 1.4 TSI BMT Highline DSG | 2015 | 138955 km | 48000 PLN | Jutrosin | private
Volkswagen Golf 1.6 Comfortline | 2006 | 239000 km | 13900 PLN | Gubin | professional
Volkswagen Golf 1.6 TDI Comfortline | 2013 | 274000 km | 13000 PLN | Dębno | private
Volkswagen Golf | 2017 | 50634 km | 48900 PLN | Zduny | private

10 of 10 rows mention 'golf' in title, make, model or version.
Enter fullscreen mode Exit fullscreen mode

Two things stand out. Nine rows are private and one is professional. And all ten are Volkswagen Golf listings, because the Actor matched golf to the Golf model. The field matchedKeyword records which search term the row was fetched under.

A term that names no make or model is skipped, so vw or gti return nothing. requireKeywordMatch is still there: set it to true and the Actor keeps a row only when every word of the term is found in the title, make, model or version, ignoring case and accents.


Step 5: See one full row

One row from the same run, the dealer listing, with one field (the engine capacity in cm3) left out of this copy:

{
    "id": "6151708601",
    "url": "https://www.otomoto.pl/osobowe/oferta/volkswagen-golf-ID6IjVqt.html",
    "title": "Volkswagen Golf 1.6 Comfortline",
    "description": "Goal, Klimatronic, Tempomat !!",
    "price": 13900,
    "currency": "PLN",
    "priceIsGross": true,
    "priceBadges": null,
    "priceEvaluation": "NONE",
    "priceDropPercent": null,
    "priceDropLowestPrice": null,
    "make": "Volkswagen",
    "model": "Golf",
    "version": "1.6 Comfortline",
    "year": 2006,
    "mileageKm": 239000,
    "fuelType": "Benzyna",
    "gearbox": "Manualna",
    "enginePowerHp": 105,
    "countryOrigin": "Niemcy",
    "city": "Gubin",
    "region": "Lubuskie",
    "postedAt": "2026-11-15T18:51:12Z",
    "imageUrl": "https://ireland.apollo.olxcdn.com/v1/files/3psrhbwbpptj1-OTOMOTOPL/image;s=640x480",
    "sellerType": "professional",
    "dealerName": "Visio-Fotografia",
    "dealerUrl": null,
    "badges": null,
    "originAdId": null,
    "cepikVerified": false,
    "isPromotedTopAd": false,
    "categoryId": "29",
    "matchedKeyword": "golf"
}
Enter fullscreen mode Exit fullscreen mode

On this row dealerUrl is null, as on all ten rows, and dealerName is null on the nine private rows. A field the listing does not give is null, never zero.


Understanding your results

Every row carries 34 fields. The ones you will use most:

  • price, currency, priceIsGross, priceBadges: the asking price, PLN in all ten rows, and whether Otomoto marks it as gross. No row in this run carries a price badge.
  • priceEvaluation: Otomoto's own price indicator. In this run it is NONE on five rows and IN on five. priceDropPercent and priceDropLowestPrice are empty on all ten rows.
  • make, model, version, year, mileageKm, fuelType, gearbox, enginePowerHp and the engine capacity in cm3: the car as numbers and labels. Fuel and gearbox keep Otomoto's Polish labels, for example Benzyna (petrol), Diesel, Manualna and Automatyczna.
  • city, region, countryOrigin: the town, the voivodeship and the country of origin when the seller gave one.
  • sellerType, dealerName, dealerUrl: professional rows carry the dealer's public name, and the website when the dealer published one. private and crosslisted rows carry no dealer name.
  • postedAt, imageUrl, badges, cepikVerified, isPromotedTopAd, originAdId: posting time (UTC), first photo, Otomoto's listing badges, the CEPIK-verified flag, the pinned-listing flag and the id of the original ad on crosslisted rows.

There are no phone number or e-mail fields in the rows.


Pro tips

  1. Search by make and model. Use terms such as golf, seria 3 or bmw; a nickname or a trim (vw, gti) matches nothing. The count in the script confirms each row is the car you asked for.
  2. Use newest for monitoring. It is the default, and it suits a schedule that checks for fresh listings. postedAt tells you when each was posted.
  3. Prefer filters Otomoto applies itself. Price, year, private sellers and electric fuel go to Otomoto with the search; professional and a model filter used together with a search term are applied to rows after the page is read.
  4. Other fuels are yours to filter. fuelType offers electric only, but every row carries its own fuelType.
  5. Dedupe across runs on id. Within one run the Actor returns a listing once, including the pinned one; across runs that is up to you.
  6. Start small. A first run with maxItems 10 and maxPages 1 shows you the shape of the data before you read a long search.

Troubleshooting

  • Zero rows, status SUCCEEDED. Otomoto answered but nothing matched your filters. The status message says so. Loosen the filters.
  • Zero rows, status FAILED. Either the input is invalid, for example a model without a make, or none of your search terms names a make or model (use golf, bmw or seria 3, not vw or gti); the status message says what to fix. Or pages could not be read.
  • Fewer rows than maxItems. The search may have fewer results, or the run stopped at its time limit. Rows pushed before the stop are kept and the log carries a warning.

Otomoto's terms apply to how you use the data, so check that your use complies with them and with data-protection law.


Next steps

For current pricing and details, see the Store page.

To repeat a search, save it as an Apify task and attach a schedule, which runs an Actor or task on a cron expression. Datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL (dataset docs), and a webhook can notify your application when a run finishes (webhook docs).

What are you planning to scrape on Otomoto? Drop a comment! 💬

About the author: reventadata builds and publishes data extraction Actors on the Apify Store.

Top comments (0)