# Otomoto.pl Scraper — Clean History & VAT Invoice (`scrapio/otomoto-pl-scraper`) Actor

Otomoto.pl Scraper extracts car listings, prices, mileage, fuel type, gearbox, specifications, seller details, locations, and other vehicle data from Otomoto.pl. Automate automotive market research, price tracking, competitor analysis, and lead generation.

- **URL**: https://apify.com/scrapio/otomoto-pl-scraper.md
- **Developed by:** [Scrapio](https://apify.com/scrapio) (community)
- **Categories:** Automation, E-commerce, Developer tools
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.99 / 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.

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

### Otomoto.pl Scraper — Adverts, Sellers and Clean-History Flags

Otomoto.pl Scraper — Clean History & VAT Invoice extracts vehicle adverts from Otomoto.pl, Poland's largest used-vehicle marketplace, pre-screened on Otomoto's own provenance filters — accident-free, undamaged, documented service history, VAT invoice, first owner, registered in Poland, country of origin — and returns each one as structured JSON carrying the seller block, pricing, equipment and every provenance flag as a flat, typed column. Unlike a scraper that hands back raw HTML, every advert, seller detail and paperwork flag arrives ready for a spreadsheet, database or LLM context window with no parsing step. This guide documents every input, every output field, and how procurement, research and AI teams actually run it.

***

### 🧭 What Does Otomoto.pl Scraper — Clean History & VAT Invoice Do?

This actor is a specialised **Otomoto.pl scraper** that searches or reads individual vehicle adverts on `otomoto.pl` and narrows the result to cars whose declared history and paperwork pass a screen built from Otomoto's own search filters. It returns **vehicle adverts** (full listing data), the **seller** block attached to each advert (dealer or private-seller details as Otomoto publishes them), and a set of flat **provenance and paperwork flags** — accident-free, damage status, VAT invoice, VAT margin invoice, first owner, service history, registered-in-Poland, country of origin and VIN-published. No Otomoto.pl account or login is required — the actor reads the same `__NEXT_DATA__` payload embedded in the public listing and advert pages that a logged-out visitor's browser receives.

- 🚗 Nine vehicle categories: cars, vans, trucks, motorcycles & quads, trailers, campers, construction machinery, agricultural machinery, and parts
- 🛡️ Nine provenance/paperwork filters applied by Otomoto itself, before a detail page is ever fetched
- 🚫 A local, Polish-diacritic-insensitive word screen over the ad text for anything Otomoto has no checkbox for
- 👤 The seller/dealer block and dealer packages on every advert
- 🧾 VAT invoice and VAT margin invoice flags for business buyers who need to reclaim tax
- 🔗 A Start URLs mode that reads specific listing or advert URLs directly, bypassing the search filters
- ⚙️ Automatic proxy escalation (none → datacenter → residential) only if Otomoto actually blocks a request

***

### ⚡ Features & Capabilities

Everything below groups into what the actor scrapes, how it is structured, and how it protects you from a silently wrong result.

#### Core features

- **Full advert record per row** — `id`, `title`, `price`, `priceList`, `images`, `mainFeatures`, `description`, `equipment`, `details`, `parametersDict`, `category`, `seller`, `packages`, `createdAt`, `updatedAt` and more, copied from Otomoto's own detail-page payload.
- **Search-side provenance screening** — `damageStatus`, `accidentFreeOnly`, `serviceHistoryOnly`, `vatInvoiceOnly`, `vatMarginOnly`, `firstOwnerOnly`, `registeredInPolandOnly`, `hasVinOnly` and `countryOfOrigin` are all turned into Otomoto's own `search[filter_enum_*]` query parameters, so a non-matching advert is filtered out by Otomoto before this actor spends a request fetching it.
- **Per-row screen verification** — Otomoto's `appliedFilters` echo is not trusted on its own (a made-up filter name is echoed back too). Every returned advert is checked row by row against the filters you asked for; a violation is never charged and fails the run loudly instead of silently shipping a bad row.
- **Flat, null-honest provenance columns** — `isAccidentFree`, `isDamaged`, `hasVatInvoice`, `hasVatMarginInvoice`, `isFirstOwner`, `hasServiceHistory`, `isRegisteredInPoland`, `isImported`, `countryOfOrigin`, `countryOfOriginLabel`, `hasVin`. A missing flag is `null` ("the seller did not answer"), never `false` ("the seller said no").
- **Local word screen** — `excludeKeywords` drops adverts whose title, description or equipment list contains a banned substring, after diacritics are folded on both sides. Dropped adverts are never hidden: they are pushed as a free, uncharged `type="excluded"` row naming the exact keyword that matched.
- **Accounting rows for dead adverts** — a sold or removed advert produces a free, uncharged `type="unavailable"` row with the HTTP status and reason instead of silently vanishing from the result.

***

### Why do developers and data teams scrape Otomoto.pl?

#### 🏢 Automotive dealers, remarketing and procurement teams

A dealer building a sourcing shortlist runs a search with `accidentFreeOnly`, `serviceHistoryOnly` and `vatInvoiceOnly` set, gets back `price`, `hasVatInvoice`, `hasServiceHistory` and `seller` on every matching car, and loads the result straight into a buying spreadsheet or a fleet-procurement tool — without opening a single advert by hand to check whether the seller actually ticked the accident-free box.

#### 📊 AI training data and RAG indexing

`description`, `equipment`, `mainFeatures` and `parametersDict` are the high-information text and structured-attribute fields for this platform — `description` for free-text RAG chunks about a specific car, `parametersDict` for the same structured spec set (make, model, year, engine, mileage) across every row for training data. Two uses: (1) RAG enrichment — a car-buying assistant can answer "does this listing have a documented service history" straight from `hasServiceHistory` without another fetch; (2) training data — the provenance flags and `parametersDict` values are typed primitives with a consistent shape across every advert in a category.

#### 📱 Competitive and market intelligence

Track `price` and `priceDrop` across repeated runs on the same `query`/`category` to see how a brand or model is priced and how fast prices move; track `createdAt` and `updatedAt` to see how quickly a dealer's stock turns over. `provenanceFiltersApplied.advertsBeforeScreen` vs `advertsAfterScreen` in every run's rows also shows how large the accident-free, documented-history share of a given market segment actually is.

#### 🔬 Research and academic use

Otomoto's own filters give a direct, reproducible way to measure things like the accident-free or VAT-invoice share of the Polish used-car market by brand, price band or region — all from publicly accessible listings, with no login and no private data involved in the vehicle records themselves.

#### 🎥 Product and SaaS development

The `hasVin` flag (without the VIN itself — see Limitations) is a natural trigger for a "run a vehicle history check" upsell inside a car-shopping app; the flat provenance columns are ready-made filters for a search UI or a price-alert tool built on top of scheduled runs of this actor.

***

### 🍚 Input Parameters

None of the 22 parameters are required — an empty input runs a default search for the first 10 cars category adverts that are accident-free and have documented service history.

| Parameter | Required | Type | Description | Example Value |
| --- | --- | --- | --- | --- |
| `startUrls` | No | array | Full Otomoto.pl URLs — listing pages or individual advert pages. Non-empty, this OVERRIDES every search filter below, including the provenance screen (provenance columns are still filled in from the advert itself; the word screen still runs). A sold/removed advert produces a free, uncharged row instead of vanishing. Default `[]`. | `["https://www.otomoto.pl/osobowe/volvo"]` |
| `category` | No | string | Otomoto section to search. Enum: `cars`, `vans`, `trucks`, `motorcycles`, `trailers`, `campers`, `construction`, `agricultural`, `parts`. The provenance filters are car-market filters and return nothing on `parts`. Default `"cars"`. | `"cars"` |
| `query` | No | string | Brand, or brand plus model separated by a space, using Otomoto's own Polish model name. An unrecognised brand or model fails the run rather than silently returning the whole catalogue. Default `""`. | `"bmw seria 3"` |
| `location` | No | string | A Polish town or voivodeship, Polish characters handled. An unresolvable name fails the run rather than silently returning national results. Default `""`. | `"Warszawa"` |
| `sort` | No | string | Enum: `relevance_web`, `created_at:desc`, `created_at:asc`, `price:asc`, `price:desc`, `year:desc`, `year:asc`, `filter_float_price:asc`, `filter_float_price:desc`, `filter_float_mileage:asc`, `created_at_first:desc`. Several values are remapped or applied client-side over the rows this run collected — see Limitations. Default `"relevance_web"`. | `"filter_float_price:asc"` |
| `minPrice` | No | integer | Minimum advert price in PLN. Minimum `0`. No default (omitted from the search when unset). | `20000` |
| `maxPrice` | No | integer | Maximum advert price in PLN. Minimum `0`. No default. | `80000` |
| `minProductionYear` | No | integer | Earliest production year. Minimum `1900`, maximum `2030`. No default. | `2015` |
| `maxProductionYear` | No | integer | Latest production year. Minimum `1900`, maximum `2030`. No default. | `2022` |
| `maxItems` | No | integer | Hard cap on how many adverts are scraped and charged. Free rows (removed adverts, word-screen exclusions) never count against it. Minimum `1`, maximum `100000`. Default `10`. | `200` |
| `damageStatus` | No | string | Otomoto's own damage checkbox. Enum: `exclude` (default), `any`, `only`. Measured on `/osobowe/bmw`, 2026-08-03, baseline 19,594 adverts: `exclude` → 15,934, `only` → 1,008, `any` → 19,594. | `"exclude"` |
| `accidentFreeOnly` | No | boolean | Keep only adverts the seller declared accident-free. Measured: 10,420 of 19,594. Default `true`. | `true` |
| `serviceHistoryOnly` | No | boolean | Keep only adverts with a documented service record. Measured: 8,405 of 19,594. Default `true`. | `true` |
| `vatInvoiceOnly` | No | boolean | Keep only cars sold with a VAT invoice. Measured: 4,451 of 19,594. Default `false`. | `true` |
| `vatMarginOnly` | No | boolean | Keep only cars sold on a margin-scheme invoice (VAT generally cannot be reclaimed on these). Measured: 1,607 of 19,594. Default `false`. | `false` |
| `firstOwnerOnly` | No | boolean | Keep only cars still owned by their first owner. Measured: 4,746 of 19,594. Otomoto publishes no owner-COUNT filter or field, so this checkbox is the closest available proxy. Default `false`. | `false` |
| `registeredInPolandOnly` | No | boolean | Keep only cars already registered in Poland, dropping fresh unregistered imports. Measured: 10,251 of 19,594. Default `false`. | `true` |
| `hasVinOnly` | No | boolean | Keep only adverts where the seller published a VIN. Measured: 17,737 of 19,594. Does not return the VIN itself — see Limitations. Default `false`. | `false` |
| `countryOfOrigin` | No | string | Keep only cars Otomoto records as originating from this country. 31 codes plus `""` for any country, each verified live against a non-zero advert count (e.g. `d` → Germany, measured 3,440 of 19,594 on `/osobowe/bmw`). Default `""`. | `"d"` |
| `excludeKeywords` | No | array | Case- and Polish-diacritic-insensitive substring match over the advert title, description and equipment list. Matched adverts are pushed as a free, uncharged `type="excluded"` row. Default `[]`. | `["powypadkowy", "sprowadzony"]` |
| `concurrency` | No | integer | How many Otomoto pages are fetched in parallel. Minimum `1`, maximum `20` (values above 20 are silently clamped). Default `10`. | `10` |
| `requestDelay` | No | integer | Extra pause before each request, in seconds, plus a small random jitter. Minimum `0`, maximum `30`. Default `0`. | `0` |
| `proxyConfiguration` | No | object | Apify Proxy settings. Default `{"useApifyProxy": false}` — Otomoto answers Apify's datacenter exit directly, so no proxy is used unless you request one or the actor is genuinely blocked (see Limitations). | `{"useApifyProxy": false}` |

#### JSON input example

```json
{
  "startUrls": [],
  "category": "cars",
  "query": "bmw seria 3",
  "location": "Warszawa",
  "sort": "filter_float_price:asc",
  "minPrice": 20000,
  "maxPrice": 80000,
  "minProductionYear": 2015,
  "maxProductionYear": 2022,
  "maxItems": 200,
  "damageStatus": "exclude",
  "accidentFreeOnly": true,
  "serviceHistoryOnly": true,
  "vatInvoiceOnly": true,
  "vatMarginOnly": false,
  "firstOwnerOnly": false,
  "registeredInPolandOnly": true,
  "hasVinOnly": false,
  "countryOfOrigin": "",
  "excludeKeywords": ["powypadkowy", "sprowadzony"],
  "concurrency": 10,
  "requestDelay": 0,
  "proxyConfiguration": { "useApifyProxy": false }
}
```

#### Supported URL types and input formats

- **Listing page** — `https://www.otomoto.pl/osobowe/volvo`. Accepted as a `startUrls` entry; the actor paginates it exactly as it would a search built from `category`/`query`.
- **Model-narrowed listing page** — `https://www.otomoto.pl/osobowe/bmw/seria-3`, equivalent to `category: "cars"`, `query: "bmw seria 3"`.
- **Individual advert page** — `https://www.otomoto.pl/osobowe/oferta/volvo-xc-40-ID6IbdLC.html`, matched by the pattern `/oferta/<slug>-ID<id>.html`. Used directly as a `startUrls` entry to re-check one specific car; the provenance columns are still filled in, but the provenance screen inputs (`accidentFreeOnly` etc.) do not apply because Start URLs bypass every search filter.

***

### 📦 Output Format

Every run writes one row per advert to the Apify dataset as typed JSON. Three `type` values exist: `"advert"` (a fully scraped, screen-passing car — the only type charged via the `row_result` pay-per-event), `"excluded"` (dropped by the `excludeKeywords` word screen, free), and `"unavailable"` (removed or sold before it could be read, free). To pull only the billed rows from the dataset, filter on `type == "advert"`; the free rows stay in the dataset as an audit trail rather than disappearing.

Every run's dataset can also be exported as JSON, CSV, Excel, XML or RSS directly from the Apify Console or the Apify API — a feature of every Apify dataset, not something specific to this actor.

#### Output for a vehicle advert (`type="advert"`, charged)

```json
{
  "id": 6109876543,
  "type": "advert",
  "status": "active",
  "title": "Volvo XC40 T5 Momentum AWD",
  "url": "https://www.otomoto.pl/osobowe/oferta/volvo-xc40-t5-momentum-awd-ID6IbdLC.html",
  "sourceUrl": "https://www.otomoto.pl/osobowe/oferta/volvo-xc40-t5-momentum-awd-ID6IbdLC.html",
  "price": 129900,
  "priceList": {
    "value": "129900",
    "currency": "PLN",
    "labels": ["Faktura VAT"],
    "isUnderBudget": false
  },
  "primaryImageUrl": "https://ireland.apollo.olxcdn.com/v1/files/example-primary.jpg",
  "images": [
    "https://ireland.apollo.olxcdn.com/v1/files/example-primary.jpg",
    "https://ireland.apollo.olxcdn.com/v1/files/example-2.jpg"
  ],
  "createdAt": "2026-07-28T10:15:00Z",
  "originalCreatedAt": "2026-07-20T09:00:00Z",
  "updatedAt": "2026-08-02T14:30:00Z",
  "mainFeatures": ["2020", "45 000 km", "Diesel", "Automatyczna"],
  "badges": ["LOW_MILEAGE"],
  "description": "Sprzedaje Volvo XC40 w idealnym stanie, serwisowane w ASO.",
  "isParts": false,
  "isUsedCar": true,
  "verifiedCar": true,
  "seller": { "...": "Otomoto's own seller object for this advert, unchanged except the internal `benefits` dealer-entitlement list, which is stripped" },
  "packages": { "...": "dealer package/campaign data exactly as Otomoto publishes it, if any" },
  "equipment": { "...": "the equipment groups exactly as Otomoto publishes them" },
  "details": [
    { "key": "make", "value": "Volvo" },
    { "key": "model", "value": "XC40" },
    { "key": "year", "value": "2020" }
  ],
  "detailsGroups": { "...": "Otomoto's own grouping of the details array" },
  "parametersDict": {
    "no_accident": { "label": "Bezwypadkowy", "values": [{ "value": "1", "label": "Tak" }] },
    "vat": { "label": "VAT", "values": [{ "value": "1", "label": "Tak" }] }
  },
  "category": { "code": "PersonalVehicle", "id": "29", "label": "Osobowe", "uriPath": "osobowe" },
  "adFeatures": { "...": "Otomoto's own ad-feature list, if any" },
  "valueAddedServices": { "...": "Otomoto's own value-added-service list, if any" },
  "priceDrop": null,
  "scrapedAt": "2026-08-04T09:12:03Z",
  "isAccidentFree": true,
  "isDamaged": false,
  "hasVatInvoice": true,
  "hasVatMarginInvoice": null,
  "isFirstOwner": null,
  "hasServiceHistory": true,
  "isRegisteredInPoland": true,
  "isImported": null,
  "countryOfOrigin": "d",
  "countryOfOriginLabel": "Niemcy",
  "hasVin": true,
  "excludedByKeyword": null,
  "provenanceFiltersApplied": {
    "mode": "search",
    "screenApplied": true,
    "otomotoFilters": {
      "search[filter_enum_damaged]": "0",
      "search[filter_enum_no_accident]": "1",
      "search[filter_enum_service_record]": "1"
    },
    "otomotoAppliedFilters": ["filter_enum_damaged", "filter_enum_no_accident", "filter_enum_service_record"],
    "advertsBeforeScreen": 19594,
    "advertsAfterScreen": 3105,
    "excludeKeywords": []
  }
}
```

Fields shown as `{ "...": "…" }` above are opaque pass-through blocks: this actor forwards them exactly as Otomoto currently publishes them on the advert page and does not restructure their internal shape, because that shape is Otomoto's own and not something this actor's source enumerates.

#### Output for provenance & paperwork flags

These eleven columns are what this variant adds on top of every base advert row, lifted flat out of `parametersDict` so a shortlist can be sorted and audited without unpacking it per row.

| Field | Type | Meaning |
| --- | --- | --- |
| `isAccidentFree` | boolean or null | `true`/`false` from Otomoto's `no_accident` flag; `null` if the seller did not answer |
| `isDamaged` | boolean or null | From Otomoto's `damaged` flag |
| `hasVatInvoice` | boolean or null | From `parametersDict.vat`, corroborated by a `"Faktura VAT"` label in `priceList.labels` |
| `hasVatMarginInvoice` | boolean or null | From Otomoto's `vat_discount` flag |
| `isFirstOwner` | boolean or null | From Otomoto's `original_owner` flag |
| `hasServiceHistory` | boolean or null | From Otomoto's `service_record` flag |
| `isRegisteredInPoland` | boolean or null | From Otomoto's `registered` flag |
| `isImported` | boolean or null | From Otomoto's `is_imported_car` flag |
| `countryOfOrigin` | string or null | Otomoto's own country code, e.g. `"d"` for Germany |
| `countryOfOriginLabel` | string or null | The Polish label Otomoto shows for that code, e.g. `"Niemcy"` |
| `hasVin` | boolean or null | Whether a VIN exists on the advert — not the VIN value itself (see Limitations) |

A `null` on any of these means the seller did not answer that question on the advert form. It is never coerced to `false` — a procurement shortlist has to be able to tell "unknown" apart from "no."

#### Schema stability and export options

Field names are stable across runs of this actor and do not change when Otomoto's front end changes its rendering, because every field is read from the same `__NEXT_DATA__` JSON payload Otomoto's own React app consumes, not from rendered HTML. If Otomoto ever removes a key this actor currently reads, that field is emitted as `null` rather than silently dropped from the row, keeping the dataset rectangular across a run. Delivery is the standard Apify dataset: browse it in the Console's table view, pull it with the API or `apify_client`, or export it as JSON, CSV, Excel, XML or RSS.

***

### 💡 Otomoto.pl Scraper — Clean History & VAT Invoice Strategy Guide

#### 🎯 Strategy 1: Real-time enrichment pipeline

Feed a list of specific advert URLs you already track (e.g. from an internal shortlist or a dealer's own stock list) into `startUrls`. Each run re-fetches the current `price`, `status`, `isAccidentFree`, `hasVatInvoice` and `hasServiceHistory` for those exact cars and writes them back to your CRM or spreadsheet, so a listing that sold, changed price, or had a provenance flag change is caught the moment you run it — without re-running a full category search.

#### 🎯 Strategy 2: Scheduled monitoring and alerting

Save a search (`category`, `query`, `location`, plus the provenance toggles you care about) and run it on an Apify Schedule. Diff each run's rows against the previous run on `id`, `price` and `updatedAt`: a new `id` appearing is a new matching car, a lower `price` on a known `id` is a price drop, and a car dropping out of the result entirely is worth checking against the `unavailable` rows from that run. Otomoto publishes no push feed, so a scheduled poll is the actual mechanism here.

#### 🎯 Strategy 3: Bulk dataset build

Run the actor once per brand or model you need (each within `concurrency` ≤ 20 and `maxItems` ≤ 100,000, the actor's own documented ceilings), then aggregate every run's dataset export to a single CSV or database table for market research or model training. Because every row shares the same field set regardless of category, appending multiple runs' exports produces one consistent table without post-processing.

#### Strategy comparison at a glance

| Strategy | Best for | Run pattern | Output format |
| --- | --- | --- | --- |
| Real-time enrichment | Refreshing a known shortlist of adverts | On-demand run over `startUrls` | JSON row per advert, written back to your system |
| Scheduled monitoring | Catching new listings or price/status changes | Apify Schedule, diffed run-over-run | JSON dataset per run, compared on `id`/`price`/`updatedAt` |
| Bulk dataset build | Market research or training-data collection | One run per query/category, aggregated afterward | CSV/JSON export per run, concatenated |

***

### How to integrate Otomoto.pl Scraper — Clean History & VAT Invoice with your stack

Otomoto.pl Scraper — Clean History & VAT Invoice works with any language or tool that can make an HTTP request, through the Apify API and the `apify_client` SDKs.

#### Python

```python
from apify_client import ApifyClient
import csv

client = ApifyClient("<YOUR_APIFY_API_TOKEN>")
## Replace with this actor's full ID from the Apify Console's Actor Information tab.
actor_id = "otomoto-pl-scraper-clean-history-and-vat-invoice"

run_input = {
    "category": "cars",
    "query": "volvo xc40",
    "accidentFreeOnly": True,
    "serviceHistoryOnly": True,
    "maxItems": 100,
}

run = client.actor(actor_id).call(run_input=run_input)

rows = []
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item.get("type") == "advert":  # skip free excluded/unavailable rows
        rows.append(item)

with open("otomoto_adverts.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["id", "title", "price", "isAccidentFree", "hasVatInvoice", "url"])
    writer.writeheader()
    for row in rows:
        writer.writerow({k: row.get(k) for k in writer.fieldnames})

print(f"Wrote {len(rows)} charged adverts to otomoto_adverts.csv")
```

#### Node.js

```javascript
import { ApifyClient } from "apify-client";

const client = new ApifyClient({ token: "<YOUR_APIFY_API_TOKEN>" });
const actorId = "otomoto-pl-scraper-clean-history-and-vat-invoice";

const run = await client.actor(actorId).call({
  category: "cars",
  query: "volvo xc40",
  accidentFreeOnly: true,
  serviceHistoryOnly: true,
  maxItems: 100,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
const paidAdverts = items.filter((row) => row.type === "advert");

console.log(`Fetched ${paidAdverts.length} charged adverts`);
console.log(paidAdverts.slice(0, 3));
```

#### Async and scheduled pipelines

For large sweeps, start the run via the API and poll `client.run(runId).get()` for `status: "SUCCEEDED"` rather than blocking on `.call()`, or use an Apify Schedule to trigger the actor on a cron interval and an Apify webhook on `ACTOR.RUN.SUCCEEDED` to notify your own system when a run's dataset is ready to pull.

***

### 🎯 Who Needs Otomoto.pl Scraper — Clean History & VAT Invoice? (Use Cases & Industries)

#### 🏢 Automotive dealers, remarketing and procurement teams

A used-car dealer or fleet buyer sets `damageStatus: "exclude"`, `accidentFreeOnly: true` and `vatInvoiceOnly: true`, gets back `price`, `hasVatInvoice`, `seller` and `mainFeatures` on every matching advert, and imports the result straight into a sourcing spreadsheet — narrowing a market of tens of thousands of listings to a shortlist that already passes the paperwork checks a buyer would otherwise verify by hand.

#### 📊 AI training data and RAG indexing teams

Teams building a car-shopping assistant index `description`, `parametersDict` and the flat provenance columns per advert so the assistant can answer "is this car accident-free" or "does it have service history" directly from typed fields instead of asking the model to re-read raw text on every query.

#### 📱 Market intelligence analysts

Analysts running the same search weekly track `price`, `priceDrop` and `provenanceFiltersApplied.advertsAfterScreen` to see how a segment's pricing and the accident-free/VAT-invoice share of the market move over time, without manually re-checking each seller's disclosure.

#### 🔬 Researchers

Academic and market researchers use the provenance screen to measure disclosure rates (accident-free, service history, VAT status) across brands, price bands or regions from publicly accessible listings, with no login and no scope beyond the advert data itself.

#### 🎥 Product and SaaS developers

Developers building a car-search app or a price-alert tool use the flat provenance columns as ready-made filter facets, and the `hasVin` flag as a trigger to offer a paid vehicle-history-check integration for cars that publish a VIN.

***

### Is it legal to scrape Otomoto.pl?

Scraping publicly accessible vehicle adverts from Otomoto.pl is generally lawful; the practical risks are contractual and, narrowly, data-protection related — not criminal.

- **Scraping public data.** Otomoto's listing and advert pages are served to any logged-out visitor without authentication, and this actor reads exactly that public payload. It does not bypass a login, paywall or access control.
- **Terms of Service.** Like most online marketplaces, Otomoto's own Terms of Service may restrict automated access. Breaching a site's Terms of Service is a contractual matter between the scraper and the site — a civil risk, not a criminal one — and is separate from whether the underlying data is public.
- **Data protection.** The vehicle listing data itself (price, specification, provenance flags) describes a car, not a person, and carries no personal-data obligation on its own. The `seller` object on each advert can be different: for a private-seller advert it may carry that individual's own name, contact details or location, which is personal data under EU/Polish data-protection law (GDPR). Anyone storing or reusing the `seller` field for a private seller should treat it accordingly.

Otomoto.pl Scraper — Clean History & VAT Invoice returns only publicly accessible data. What you do with that data is your responsibility — consult legal counsel for commercial applications involving personal data.

***

### ❓ Frequently asked questions

#### Does Otomoto.pl Scraper — Clean History & VAT Invoice work without an Otomoto.pl account?

Yes. The actor reads the same public `__NEXT_DATA__` payload a logged-out browser receives on Otomoto's listing and advert pages; no Otomoto account, login or cookie is used or required.

#### How does it handle Otomoto's anti-scraping measures?

It classifies every response by whether the page's own expected data container actually parses out of it, rather than by loose keyword matching, and starts every run with no proxy at all — Otomoto answers Apify's datacenter exit directly. If a request is genuinely blocked, it retries up to three times per URL, then escalates the proxy tier from none to datacenter to residential; a removed advert (HTTP 410/404) is recognised as "gone," not a block, so it never burns a retry or triggers an escalation.

#### Can I run it at scale without getting blocked?

The actor supports up to 20 parallel requests (`concurrency`) and up to 100,000 items per run (`maxItems`), both hard-clamped in code. No uptime or success-rate figure is published for scraping at that scale — the proxy escalation described above is the actor's real defence, not a guaranteed outcome.

#### How fresh is the data Otomoto.pl Scraper returns?

Every run fetches Otomoto's listing and advert pages live at run time; nothing is cached or served from a previous run. Each row carries its own `scrapedAt` timestamp.

#### Which Otomoto.pl fields work best for AI training and RAG indexing?

For RAG, `description` is the free-text field with the most information per advert. For training data, `parametersDict` and the eleven flat provenance columns (`isAccidentFree`, `hasVatInvoice`, `hasServiceHistory`, etc.) have the most consistent structure across every advert in a category. All of them return as typed primitives — strings, booleans or `null` — requiring no normalization before use.

#### Does `hasVinOnly` return the actual VIN?

No. Otomoto serves the VIN itself as an encrypted token behind a reCAPTCHA, so this actor only reports whether a VIN exists (`hasVin: true`/`false`/`null`), not its value. Use it to identify which adverts make an external vehicle-history check possible.

#### What happens if Otomoto doesn't recognise my brand, model or location?

The run fails loudly and names the exact word it rejected, rather than silently returning Otomoto's entire unfiltered catalogue. Otomoto answers an unknown brand, model or town with HTTP 200 and the whole category's ads, so this actor checks the response against what you actually asked for and stops rather than shipping a mislabelled result.

#### Why is a provenance flag sometimes `null` instead of `false`?

Because a missing answer and a "no" answer are different facts. Otomoto's own advert form leaves several of these questions optional — measured on 40 live BMW adverts, `vat` was absent on 25 of 40 — so this actor returns `null` for "the seller did not answer" and reserves `false` for an explicit "no" from Otomoto.

#### Does Otomoto.pl Scraper — Clean History & VAT Invoice work with Claude, ChatGPT and other AI agent tools?

It is callable as an HTTP endpoint through the Apify API by any agent framework that can make a request and read JSON — every response is typed JSON with no HTML to parse before it enters an LLM's context window. No dedicated MCP server integration is documented for this actor.

#### Can I combine the provenance screen with Start URLs?

No — Start URLs override every search filter, including the provenance screen, exactly as the actor documents. The provenance columns are still filled in on each returned row from the advert's own data, and `excludeKeywords` still runs, but `damageStatus`, `accidentFreeOnly` and the other search-side toggles have no effect on a Start URL run.

***

### ℹ️ Disclaimer

Otomoto.pl Scraper — Clean History & VAT Invoice extracts only publicly available vehicle-advert data from Otomoto.pl. This tool is intended for lawful use cases only. Users are responsible for complying with Otomoto.pl's terms of service and applicable data protection laws in their jurisdiction, particularly when storing or reusing the `seller` field from adverts posted by private individuals.

# Actor input Schema

## `startUrls` (type: `array`):

Full Otomoto.pl URLs. Accepts BOTH listing pages (https://www.otomoto.pl/osobowe/volvo) and individual advert pages (https://www.otomoto.pl/osobowe/oferta/volvo-xc-40-ID6IbdLC.html). When this list is non-empty it OVERRIDES every search filter below, INCLUDING the provenance screen - the provenance columns are still filled in from the advert itself, and the word screen still runs. An advert that has been sold or removed produces a free, uncharged row with the reason instead of silently disappearing.

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

Otomoto section to search. English aliases are translated to the current Otomoto slug. Otomoto has moved several sections: motorcycles is now /motocykle-i-quady, trailers is /przyczepy, construction is /maszyny-budowlane, agricultural is /maszyny-rolnicze, and the standalone campers section was retired so campers are searched as the camper body type inside /dostawcze. The provenance filters are car-market filters; on the parts category they will return nothing.

## `query` (type: `string`):

Brand, or brand plus model separated by a space. Examples: "volvo" → /osobowe/volvo · "bmw seria 3" → /osobowe/bmw/seria-3 · "mercedes-benz c-klasa". Use Otomoto's own Polish model name. If Otomoto does not recognise the brand or the model it silently returns the whole catalogue, so the run FAILS instead and names the word it rejected.

## `location` (type: `string`):

A Polish town ("Warszawa", "Kraków", "Łódź") or a voivodeship ("Małopolskie", "Mazowieckie"). Polish characters are handled. Otomoto applies its own default catchment radius around a town, so nearby localities are included; a voivodeship is an exact administrative match. If Otomoto cannot resolve the name it falls back to the whole country, so the run FAILS instead of returning national results labelled as a local search.

## `sort` (type: `string`):

Otomoto silently ignores unrecognised sort keys, so several of these values are handled for you: "Price: low → high" and "Price: high → low" are remapped to Otomoto's working price keys, and the two Year orders plus "Oldest first" are applied CLIENT-SIDE over the rows this run collected because Otomoto publishes no server-side year sort. The run log always states which one was used.

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

Minimum advert price in Polish złoty. Applied by Otomoto as search\[filter\_float\_price:from].

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

Maximum advert price in Polish złoty. Applied by Otomoto as search\[filter\_float\_price:to].

## `minProductionYear` (type: `integer`):

Earliest production year, e.g. 2019. Applied by Otomoto as search\[filter\_float\_year:from].

## `maxProductionYear` (type: `integer`):

Latest production year, e.g. 2025. Applied by Otomoto as search\[filter\_float\_year:to].

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

Hard cap on how many adverts are scraped and charged. Free rows (removed adverts, and adverts dropped by the word screen) are never charged and can only reduce the charged count, never push it above this number. Default is 10.

## `damageStatus` (type: `string`):

Otomoto's own damage checkbox (search\[filter\_enum\_damaged]). Measured: exclude → 15,934 adverts, only → 1,008, any → 19,594. Defaults to EXCLUDE, because a procurement or family-car shortlist should not contain salvage. Choose 'only' when you are deliberately sourcing repairables.

## `accidentFreeOnly` (type: `boolean`):

Keep only adverts where the seller declared the car accident-free (search\[filter\_enum\_no\_accident]=1). Measured: 10,420 of 19,594. ON by default - it is the first question a Polish buyer asks. Turn it off to include cars with a declared accident history AND cars whose seller left the question blank.

## `serviceHistoryOnly` (type: `boolean`):

Keep only adverts advertised with a documented service record (search\[filter\_enum\_service\_record]=1). Measured: 8,405 of 19,594. ON by default - for a fleet or company purchase the maintenance record is the paperwork that matters second only to the invoice.

## `vatInvoiceOnly` (type: `boolean`):

Keep only cars sold with a VAT invoice (search\[filter\_enum\_vat]=1), so a business buyer can reclaim the tax. Measured: 4,451 of 19,594. OFF by default because private buyers do not need it. Read hasVatMarginInvoice before assuming a car is reclaimable - a margin-scheme invoice generally is not.

## `vatMarginOnly` (type: `boolean`):

Keep only cars sold on a margin-scheme invoice (search\[filter\_enum\_vat\_discount]=1). Measured: 1,607 of 19,594. This is the opposite need to the one above: VAT generally CANNOT be reclaimed on a margin-scheme car. Leave off unless you specifically want them.

## `firstOwnerOnly` (type: `boolean`):

Keep only cars still owned by their first owner (search\[filter\_enum\_original\_owner]=1). Measured: 4,746 of 19,594. Otomoto publishes NO owner-count filter and no owner-count field, so this first-owner checkbox is the deliverable half of a 'max previous owners' filter - a numeric owner limit does not exist on the site.

## `registeredInPolandOnly` (type: `boolean`):

Keep only cars already registered in Poland (search\[filter\_enum\_registered]=1), dropping fresh imports that still need customs paperwork, excise duty, an inspection and a re-registration. Measured: 10,251 of 19,594.

## `hasVinOnly` (type: `boolean`):

Keep only adverts where the seller published a VIN (search\[filter\_enum\_has\_vin]=1). Measured: 17,737 of 19,594. This actor does NOT return the VIN itself - Otomoto serves it as an encrypted token behind a reCAPTCHA - only whether one exists, so you know a history check is possible elsewhere.

## `countryOfOrigin` (type: `string`):

Keep only cars Otomoto records as originating from this country (search\[filter\_enum\_country\_origin]). The code is Otomoto's own value and is shown in each option. Measured on /osobowe/bmw: Germany (d) → 3,440 of 19,594. Every code listed here was requested live and returned a non-zero advert count. Leave empty for any country.

## `excludeKeywords` (type: `array`):

Case-insensitive, Polish-diacritic-insensitive SUBSTRING match over the advert title, description and equipment list. Typical entries: powypadkowy, sprowadzony, do poprawek, anglik. Substring matching produces false positives on purpose-written prose - "auto bezwypadkowe, nie powypadkowe" contains "powypadkow" - which is exactly why the dropped rows are kept and labelled. Leave empty to skip the word screen.

## `concurrency` (type: `integer`):

How many Otomoto pages are fetched in parallel. Default is 10, maximum 20.

## `requestDelay` (type: `integer`):

Extra pause before each request, in seconds, plus a small random jitter. Default is 0 (no extra delay).

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

Leave disabled to run without a proxy. The actor still escalates to datacenter and then residential automatically if it is genuinely blocked.

## Actor input object example

```json
{
  "startUrls": [],
  "category": "cars",
  "query": "volvo",
  "location": "Warszawa",
  "sort": "relevance_web",
  "maxItems": 10,
  "damageStatus": "exclude",
  "accidentFreeOnly": true,
  "serviceHistoryOnly": true,
  "vatInvoiceOnly": false,
  "vatMarginOnly": false,
  "firstOwnerOnly": false,
  "registeredInPolandOnly": false,
  "hasVinOnly": false,
  "countryOfOrigin": "",
  "excludeKeywords": [],
  "concurrency": 10,
  "requestDelay": 0,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (type: `string`):

All scraped items in the Actor's default dataset, with the core identity, price and top-level provenance columns.

## `provenance` (type: `string`):

Full provenance/paperwork columns per advert - accident-free, damage, VAT invoice, VAT margin invoice, first owner, service history, registration, import, country of origin, VIN published - plus the screen audit object.

## `excluded` (type: `string`):

Free, uncharged rows for adverts dropped by the local excludeKeywords word screen, naming the keyword that matched.

## `pricing` (type: `string`):

Price, full price details object, price-drop object and VAT/VAT-margin invoice flags per advert.

## `seller` (type: `string`):

Seller details and dealer package entitlements per advert.

## `equipment` (type: `string`):

Equipment list, details, detail groups and the raw parameters dictionary per advert.

## `images` (type: `string`):

Primary image URL and the full photo list per advert.

## `unavailable` (type: `string`):

Free, uncharged accounting rows for Start URLs that were removed from Otomoto before they could be read.

# 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 = {
    "query": "volvo",
    "location": "Warszawa",
    "maxItems": 10,
    "damageStatus": "exclude",
    "accidentFreeOnly": true,
    "serviceHistoryOnly": true,
    "vatInvoiceOnly": false,
    "vatMarginOnly": false,
    "firstOwnerOnly": false,
    "registeredInPolandOnly": false,
    "hasVinOnly": false,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapio/otomoto-pl-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 = {
    "query": "volvo",
    "location": "Warszawa",
    "maxItems": 10,
    "damageStatus": "exclude",
    "accidentFreeOnly": True,
    "serviceHistoryOnly": True,
    "vatInvoiceOnly": False,
    "vatMarginOnly": False,
    "firstOwnerOnly": False,
    "registeredInPolandOnly": False,
    "hasVinOnly": False,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("scrapio/otomoto-pl-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 '{
  "query": "volvo",
  "location": "Warszawa",
  "maxItems": 10,
  "damageStatus": "exclude",
  "accidentFreeOnly": true,
  "serviceHistoryOnly": true,
  "vatInvoiceOnly": false,
  "vatMarginOnly": false,
  "firstOwnerOnly": false,
  "registeredInPolandOnly": false,
  "hasVinOnly": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call scrapio/otomoto-pl-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapio/otomoto-pl-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/nO7FPGrDAKbIFnUVg/builds/lfxea5SNjcPYH4RBV/openapi.json
