# Otomoto Cars Scraper (Poland) (`scrapyx/otomoto-cars-scraper`) Actor

Scrapes used and new car listings from Otomoto, Poland's largest car marketplace, by brand and region. Every row carries price, mileage, fuel type, gearbox, year, location, seller info and images.

- **URL**: https://apify.com/scrapyx/otomoto-cars-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** E-commerce, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.56 / 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

## Otomoto Cars Scraper (Poland)

Scrapes used and new car listings from **[Otomoto](https://www.otomoto.pl)**
— Poland's largest car marketplace.

Public data only. No login, no cookies, no browser. No bot challenge on any
of 8 TLS profiles tested across search and detail pages.

### The one thing you need to know before using this

**A brand or region Otomoto doesn't recognise is silently dropped — you get
the whole country's listings instead, with a healthy 200.**

`/osobowe/notarealbrand` and `/osobowe/audi/notarealregion` both answer 200
with the **national** total, not an error and not a smaller parent-scope
total. This actor reads Otomoto's own `appliedFilters`/`appliedLocation`
fields — which genuinely say what was applied — and if your brand or region
was silently dropped, it stops before paging and reports
`brandApplied`/`regionApplied: false` with **zero** rows, rather than handing
you the wrong scope labelled as the one you asked for.

Region is a closed list of Poland's 16 voivodeships, each individually
verified, so a typo there is refused before the run starts. Brand has no
practical fixed list (Otomoto covers dozens of makes), so it stays free text
and is checked the way described above instead.

### What you get

Three record types share one dataset, told apart by `recordType`.

#### `CAR` — one row per listing

Upstream's object, passed through verbatim: title, price, mileage, fuel
type, gearbox, year, location (city + voivodeship), seller info, images,
and a `parameters` list. With **Fetch full car details** on, each row also
gets a `detail` object: full equipment list, a richer `parametersDict`,
AI-generated summary where present, and verified-car badges.

#### `SEARCH_SUMMARY` — one row per (brand, region) query

Otomoto's own match total, requests spent, pages fetched, the filters
actually sent, and `brandApplied`/`regionApplied`.

#### `ERROR` — one row per input that could not be processed

Every input maps to at least one row, so nothing disappears silently.

### Input

| Field | What it does |
| --- | --- |
| **Search queries** | `[{"brand": ..., "region": ...}, ...]`. Both may be empty for "everything". |
| **Fuel type** | Petrol, diesel, electric, hybrid, LPG, plug-in hybrid. |
| **Minimum / maximum price (PLN)** | Verified real. |
| **Minimum / maximum mileage (km)** | Verified real. |
| **Fetch full car details** | One extra request per listing. Off by default. |
| **Max cars per query** | `0` for everything Otomoto will serve. |
| **Max concurrent requests / Minimum seconds between requests** | Throughput controls. |

### Known limits

- **Cars only.** Otomoto also lists motorcycles, trucks and parts on
  presumably similar infrastructure — not verified or covered here.
- **Brand is not validated against a fixed list** (there isn't a practical
  one) — a typo is caught at runtime via `brandApplied: false` rather than
  refused up front like region is.
- **A dead or malformed listing URL doesn't always 404** — Otomoto answers a
  malformed id with HTTP 520 and a well-formed-but-missing id with a clean
  404, both serving the site's generic homepage. This actor treats both the
  same way (a failed detail fetch, not a crash).

# Actor input Schema

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

One {brand, region} pair per entry, each with its own SEARCH\_SUMMARY row. Both may be empty strings for "all brands, all of Poland".

`brand` is free text (e.g. `bmw`, `audi`, `volkswagen`) — Otomoto lists too many makes/models for a fixed list, so it is validated at runtime instead: if the brand is not recognised, Otomoto silently serves the NATIONAL baseline rather than an error, and this actor detects that and reports `brandApplied: false` with zero rows rather than emitting cars for the wrong scope.

`region` must be one of Poland's 16 voivodeships or empty for the whole country.

Example:

```json
[{"brand": "bmw", "region": "mazowieckie"}, {"brand": "audi", "region": ""}]
```

## `fuelType` (type: `string`):

Verified real. An unrecognised value is answered with an honest zero-result total (a rare exception to this portfolio's usual silent-widen pattern), so this is offered as a courtesy rather than a strict requirement.

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

Verified real. Leave at 0 to skip.

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

Verified real: `50000` narrowed the Audi baseline from 19,270 to 11,555. Leave at 0 to skip.

## `mileageFrom` (type: `integer`):

Verified real. Leave at 0 to skip.

## `mileageTo` (type: `integer`):

Verified real: `50000` narrowed the Audi baseline from 19,270 to 4,344. Leave at 0 to skip.

## `includeCarDetails` (type: `boolean`):

Adds one request per listing and attaches a `detail` object with the full equipment list, a richer `parametersDict`, AI-generated summary/insight where present, and verified-car badges.

Off by default — search rows already carry price, mileage, fuel, gearbox, location, seller and images.

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

Stop after this many cars per (brand, region) query. Set to 0 for everything Otomoto will serve.

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

Upper bound on requests in flight at once, across all queries and any detail fetches.

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

Paces how often requests START, without tying up a concurrency slot. Leave at 0 to use the built-in default of 0.3s -- 0 does not mean 'no pacing'.

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

Residential pinned to Poland. No WAF or bot challenge was observed on any of 8 TLS profiles across search and detail surfaces, but Otomoto is a Poland-only portal and container egress is a different posture than a home connection.

## Actor input object example

```json
{
  "queries": [
    {
      "brand": "bmw",
      "region": "mazowieckie"
    }
  ],
  "fuelType": "",
  "priceFrom": 0,
  "priceTo": 0,
  "mileageFrom": 0,
  "mileageTo": 0,
  "includeCarDetails": false,
  "maxItems": 100,
  "maxConcurrency": 4,
  "minRequestInterval": 0,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "PL"
  }
}
```

# 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 = {
    "queries": [
        {
            "brand": "bmw",
            "region": "mazowieckie"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/otomoto-cars-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 = { "queries": [{
            "brand": "bmw",
            "region": "mazowieckie",
        }] }

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/otomoto-cars-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 '{
  "queries": [
    {
      "brand": "bmw",
      "region": "mazowieckie"
    }
  ]
}' |
apify call scrapyx/otomoto-cars-scraper --silent --output-dataset

```

## MCP server setup

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