# Otomoto.pl Scraper With Dealer Contact Leads (`simpleapi/otomoto-pl-scraper`) Actor

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

## Pricing

from $2.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/platform/actors/running/actors-in-store#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, Prices and Dealer Contact Leads

Otomoto.pl Scraper With Dealer Contact Leads pulls vehicle adverts from Poland's Otomoto.pl marketplace and attaches the seller behind each one: plaintext phone numbers, dealership name, address, storefront URL, live stock count, working hours and badges. Every row carries `id`, `title`, `price`, `mainFeatures`, `seller`, `sellerContacts`, `hasPhone` and `scrapedAt` as structured JSON. Car dealerships building call lists, lead-gen agencies, and market analysts tracking price and stock movement use it to turn a category or brand search into a ready-to-dial dataset. Point it at a category, a brand, or a list of URLs, and it returns clean, typed rows with no manual copy-paste.

### What is Otomoto.pl Scraper With Dealer Contact Leads?

It is an Apify Actor that scrapes Otomoto.pl vehicle listing pages and individual advert pages, then resolves each advert's published phone number(s) through Otomoto's own contact endpoint. No Otomoto account, login, or API key is required — the Actor reads only what the public advert page and Otomoto's own keyless phone endpoint publish.

Key features:

- 🔎 **Discovery by category, brand, location, price, and year** — or paste specific Otomoto listing/advert URLs directly, bypassing search entirely
- ☎️ **Plaintext seller phone numbers** — resolved live per advert, not looked up or guessed
- 🏢 **Server-side dealer / private split** — `sellerType` shrinks the run before a single advert page is fetched, not after
- 📇 **Full seller outreach record** — dealership name, address, storefront URL, live stock count, working hours, and platform badges, all already present on the advert page
- 🧮 **One row per advert or one row per seller** — collapse every listing from the same business into a single lead
- 📤 **Structured JSON output** — export to CSV, Excel, JSON, or push straight into a CRM via the Apify API

### What data can I extract with Otomoto.pl Scraper With Dealer Contact Leads?

Every advert row carries the vehicle listing fields plus the seller's outreach record, using the exact key names the Actor writes to the dataset.

| Field | Example value | Use case |
| --- | --- | --- |
| `id` | `6149626356` | Unique advert identifier, dedupe key |
| `type` | `"advert"` | Row kind — `"advert"` (charged) or `"unavailable"` (free) |
| `title` | `"BMW X5 3.0d xDrive M Sport"` | Listing title |
| `url` | `"https://www.otomoto.pl/osobowe/oferta/bmw-x5-ID6IbcJS.html"` | Canonical advert URL |
| `price` | `189900` | Integer price coercion |
| `priceList` | `{ "value": "189900", "currency": "PLN" }` | Otomoto's own price object (string value preserved) |
| `mainFeatures` | `["2019", "89 000 km", "Diesel"]` | Headline specs shown on the listing |
| `isUsedCar` | `true` | New vs. used flag |
| `verifiedCar` | `true` | Otomoto's verification badge |
| `createdAt` / `updatedAt` | `"2026-07-28T09:12:00Z"` | Publish and last-edit timestamps |
| `seller` | `{ "id": 15244680, "name": "Auto Studio Kraków", "type": "PROFESSIONAL", ... }` | Raw seller block from the advert page |
| `sellerContacts` | see below | Built outreach record |
| `hasPhone` | `true` | `true`/`false` only once the phone endpoint has answered; `null` if not attempted |
| `equipment` / `details` / `detailsGroups` / `parametersDict` | arrays / objects | Full spec sheet as Otomoto structures it |
| `category` | `{ "code": "PersonalVehicles", "label": "Osobowe" }` | Section the advert sits in |
| `scrapedAt` | `"2026-08-04T10:03:11Z"` | Run timestamp |

#### 📇 Seller contact leads

`sellerContacts` is the field that turns a car list into a call list. It is built entirely from data already present on the advert page plus phone numbers walked from Otomoto's own contact endpoint — no third-party lookup, no email guessing. It carries `sellerId`, `sellerType` (`PROFESSIONAL`/`PRIVATE`), `isBusiness`, `dealerName`, `phones` (array, up to 5 resolved per advert), `phoneCount`, `storefrontUrl`, `website`, `addressLine`, `city`, `postcode`, `region`, `regionSlug`, `latitude`/`longitude`, `listedVehicleCount` (the seller's live stock count), `workingHours`, `sellerBadges`, and `onOtomotoSinceLabel`. A sales team can filter on `hasPhone` and sort dealers by `listedVehicleCount` to prioritize outreach to high-volume sellers first.

#### 🎯 Targeting data

The fields you filter and segment on are `category` (section), `price`/`priceList`, the production year read from `parametersDict`, `seller.type` (dealer vs. private), and `sellerContacts.city`/`region` for geographic segmentation. Combined with the input-side `sellerType`, `minPrice`/`maxPrice`, and `minProductionYear`/`maxProductionYear` filters, these let you narrow a national category down to a specific brand, price band, region, and seller type before a single row is charged.

### How does Otomoto.pl Scraper With Dealer Contact Leads differ from the official Otomoto API?

Otomoto does publish an API, but it is not a public search/read API: its own FAQ states integration is restricted to business clients (*"Z integracji API mogą korzystać wyłącznie Klienci biznesowi Otomoto"*) and requires an API partner-account registration (checked on otomoto.pl/news/faq-api, 2026-08-04). It is built for a dealer to post, manage, and deactivate **their own** listings — not to search or read other sellers' adverts, and it does not expose a competitor's phone number or contact record. There is no self-serve endpoint for "give me every BMW listing in Kraków with seller contacts."

#### Why not build this yourself?

Building an equivalent scraper means solving problems this Actor already handles: Otomoto's category slugs move over time (five of the original section paths now 404 and redirect to different slugs), a single-word brand+model query silently returns the entire unfiltered catalogue instead of erroring, sort keys are silently ignored when unrecognized, and a removed advert answers with a valid-looking page that has to be told apart from a genuine block. Add proxy escalation logic, and a bespoke build turns into an ongoing maintenance job. This Actor absorbs that: it validates that Otomoto actually applied the filters requested and fails loudly instead of silently returning the wrong result set, and it starts with no proxy at all, escalating to datacenter and then residential only if it is genuinely blocked.

Use the official API when you manage your own dealer inventory on Otomoto. Use this Actor when you need to read and export **other sellers'** adverts and contact data at scale.

### How to use data extracted from Otomoto.pl?

#### 🏢 Car dealerships and sales teams

Search a brand and region with `sellerType: "private"` and `withPhoneOnly: true` to build a call list of individual sellers who might trade in toward a dealer purchase, or run `sellerType: "dealer"` against a competing region to see what rival lots are stocking and at what price. The `sellerContacts.phones`, `dealerName`, and `city` fields go straight into a dialer or CRM import; `hasPhone` filters out dead lines before the file ever reaches a rep.

#### 📊 Agencies and lead-generation services

Run the same search on a recurring schedule (Apify Schedules) for multiple clients, using `oneRowPerSeller: true` to collapse each dealer's full inventory into one row with `sellerAdCount` and `sellerAdIds`, so a client-facing report shows "47 dealers with X active listings" rather than raw advert counts.

#### 🔬 Market research and price intelligence

Sweep a category with `minPrice`/`maxPrice` and `minProductionYear`/`maxProductionYear` bands to build a price distribution by year and mileage (from `parametersDict`), or compare `sellerType: "dealer"` vs. `"private"` volume in a region — the actual `search[private_business]` split Otomoto applies (measured on `/osobowe/bmw`: 8,229 dealer vs. 11,372 private adverts out of 19,601 unfiltered, 2026-08-03) to model where private-party supply outweighs dealer stock.

#### 🤖 AI agents and automated pipelines

Because the Actor returns typed JSON with a stable schema, it slots into an agent tool call or RAG pipeline as a "search Otomoto for X, return sellers with phones" function — the agent supplies `category`, `query`, and `sellerType`, and gets back a rectangular dataset it can filter on `hasPhone` without further parsing.

### 🔼 Input sample

| Parameter | Required | Type | Description | Example value |
| --- | --- | --- | --- | --- |
| `extractSellerContacts` | No | boolean | Resolve phone numbers and build the seller outreach record. Default `true`. When `false`, `sellerContacts`, `hasPhone`, and `sellerContactsFetched` are all `null`. | `true` |
| `sellerType` | No | string (enum: `dealer`, `private`, `any`) | Applied server-side before any advert page is fetched. Default `"dealer"`. Ignored when `startUrls` is non-empty. | `"dealer"` |
| `withPhoneOnly` | No | boolean | Drop advert rows where no phone number could be read. Default `false`. Requires `extractSellerContacts: true`. | `false` |
| `oneRowPerSeller` | No | boolean | Collapse every advert from the same seller into one row plus `sellerAdIds`/`sellerAdCount`. Default `false`. | `false` |
| `startUrls` | No | array | Full Otomoto.pl listing or advert page URLs. Overrides every search filter below, including `sellerType`. Default `[]`. | `[]` |
| `category` | No | string (enum, 9 values) | Otomoto section: `cars`, `vans`, `trucks`, `motorcycles`, `trailers`, `campers`, `construction`, `agricultural`, `parts`. Default `"cars"`. | `"cars"` |
| `query` | No | string | Brand, or brand plus model, space-separated. Default `""`. | `"bmw seria 3"` |
| `location` | No | string | Polish town or voivodeship. Default `""` (whole country). | `"Kraków"` |
| `sort` | No | string (enum, 11 values) | Result order. Default `"relevance_web"`. | `"created_at:desc"` |
| `minPrice` | No | integer, minimum `0` | Minimum price in PLN. | `20000` |
| `maxPrice` | No | integer, minimum `0` | Maximum price in PLN. | `90000` |
| `minProductionYear` | No | integer, min `1900`, max `2030` | Earliest production year. | `2018` |
| `maxProductionYear` | No | integer, min `1900`, max `2030` | Latest production year. | `2023` |
| `maxItems` | No | integer, min `1`, max `100000` | Hard cap on advert **detail pages** fetched. Default `10`. | `200` |
| `concurrency` | No | integer, min `1`, max `20` | Parallel requests (advert pages and phone lookups). Default `10`. | `10` |
| `requestDelay` | No | integer, min `0`, max `30` | Extra pause per request, in seconds, plus jitter. Default `0`. | `0` |
| `proxyConfiguration` | No | object | Apify Proxy settings. Default `{"useApifyProxy": false}`. | `{"useApifyProxy": false}` |

No input is required — every parameter has a default, and an empty run scrapes the first 10 car adverts by relevance.

```json
{
  "extractSellerContacts": true,
  "sellerType": "dealer",
  "withPhoneOnly": false,
  "oneRowPerSeller": false,
  "startUrls": [],
  "category": "cars",
  "query": "bmw seria 3",
  "location": "Kraków",
  "sort": "created_at:desc",
  "minPrice": 20000,
  "maxPrice": 90000,
  "minProductionYear": 2018,
  "maxProductionYear": 2023,
  "maxItems": 200,
  "concurrency": 10,
  "requestDelay": 0,
  "proxyConfiguration": { "useApifyProxy": false }
}
```

⚠️ **Common pitfall:** `query` must be space-separated (`"bmw seria 3"`), not a single hyphenated word (`"bmw-seria-3"`). A multi-word query becomes two path segments on Otomoto (`/osobowe/bmw/seria-3`); a single hyphenated blob is read by Otomoto as one unrecognized token and would silently widen the search to the entire unfiltered catalogue. This Actor checks Otomoto's own applied-filters response and **fails the run** with the exact rejected word instead of returning an unfiltered result set labeled as your search.

### 🔽 Output sample

The dataset is typed, normalized JSON with a stable schema across every row — export to CSV, Excel, JSON, or XML directly from the Apify Console, or pull it via the API/`apify_client`.

```json
{
  "id": 6149626356,
  "type": "advert",
  "status": "active",
  "title": "BMW X5 3.0d xDrive M Sport",
  "url": "https://www.otomoto.pl/osobowe/oferta/bmw-x5-ID6IbcJS.html",
  "sourceUrl": "https://www.otomoto.pl/osobowe/oferta/bmw-x5-ID6IbcJS.html",
  "price": 189900,
  "priceList": {
    "value": "189900",
    "currency": "PLN",
    "labels": ["Faktura VAT"],
    "isUnderBudget": false
  },
  "primaryImageUrl": "https://ireland.apollo.olxcdn.com/v1/files/example.webp",
  "images": ["https://ireland.apollo.olxcdn.com/v1/files/example.webp"],
  "createdAt": "2026-07-28T09:12:00Z",
  "originalCreatedAt": "2026-07-20T14:03:00Z",
  "updatedAt": "2026-08-01T11:45:00Z",
  "mainFeatures": ["2019", "89 000 km", "Diesel", "Automatyczna"],
  "badges": ["LOW_MILEAGE"],
  "description": "BMW X5 3.0d xDrive M Sport, bezwypadkowy, serwisowany w ASO...",
  "isParts": false,
  "isUsedCar": true,
  "verifiedCar": true,
  "seller": {
    "id": 15244680,
    "name": "Auto Studio Kraków",
    "type": "PROFESSIONAL",
    "location": {
      "address": "ul. Wielicka 12",
      "city": "Kraków",
      "postalCode": "30-552",
      "region": "Małopolskie"
    },
    "sellerUrl": "https://www.otomoto.pl/osobowa/auto-studio-krakow",
    "numberOfActiveAds": 47
  },
  "packages": ["EXTENDED", "TOP"],
  "equipment": [{ "code": "air-conditioning", "values": ["automatic"] }],
  "details": [{ "key": "year", "value": "2019" }],
  "detailsGroups": [{ "code": "general", "items": ["year", "mileage"] }],
  "parametersDict": { "year": { "value": "2019" }, "mileage": { "value": "89000" } },
  "category": { "code": "PersonalVehicles", "id": "29", "label": "Osobowe", "uriPath": "osobowe" },
  "adFeatures": ["highlighted"],
  "valueAddedServices": [],
  "priceDrop": null,
  "scrapedAt": "2026-08-04T10:03:11Z",
  "sellerContacts": {
    "sellerId": "15244680",
    "sellerType": "PROFESSIONAL",
    "isBusiness": true,
    "dealerName": "Auto Studio Kraków",
    "phones": ["12 345 67 89", "600 123 456"],
    "phoneCount": 2,
    "storefrontUrl": "https://www.otomoto.pl/osobowa/auto-studio-krakow",
    "website": "https://autostudiokrakow.pl",
    "addressLine": "ul. Wielicka 12",
    "city": "Kraków",
    "postcode": "30-552",
    "region": "Małopolskie",
    "regionSlug": "malopolskie",
    "latitude": 50.0247,
    "longitude": 19.9645,
    "listedVehicleCount": 47,
    "workingHours": { "mon-fri": "9:00-17:00" },
    "sellerBadges": ["registration-date"],
    "onOtomotoSinceLabel": "Sprzedający na OTOMOTO od 2015"
  },
  "hasPhone": true,
  "sellerContactsFetched": true
}
```

An advert that was sold or removed before it could be read is pushed as a free, uncharged row instead: `type: "unavailable"`, `sourceUrl`, `httpStatus`, `unavailableReason: "advert_removed_or_expired"`, with every contact field `null`. In `oneRowPerSeller` mode, the surviving row additionally carries `sellerAdIds` (all advert IDs merged into it) and `sellerAdCount`.

### How do you filter and target specific adverts?

**Category vs. free-text query.** `category` picks the Otomoto section (cars, vans, trucks, motorcycles, trailers, campers, construction, agricultural, parts); `query` narrows inside it to a brand or brand-plus-model. Use `category` alone to sweep a whole section, add `query` when you know the brand.

**Scope precision.** `location` (a Polish town or voivodeship — a town uses Otomoto's own catchment radius, a voivodeship is an exact administrative match), `minPrice`/`maxPrice` (PLN), and `minProductionYear`/`maxProductionYear` narrow the result set before any advert page is fetched.

**Quality thresholds.** `sellerType` (`dealer`/`private`/`any`) is applied server-side via Otomoto's own `search[private_business]` parameter, shrinking the run before a single detail page loads; every returned advert is then re-checked row by row. `withPhoneOnly` drops rows where no phone number could be resolved.

**Volume controls.** `maxItems` (1–100,000, default 10) caps how many advert detail pages are fetched — in `oneRowPerSeller` mode the dataset can hold fewer rows than this (adverts collapse into sellers) but never more. `concurrency` (1–20) and `requestDelay` (0–30s) tune request pacing.

```json
{ "category": "cars", "query": "bmw x5", "location": "Kraków", "sellerType": "dealer", "minProductionYear": 2019, "sort": "created_at:desc", "maxItems": 200 }
```

```json
{ "category": "cars", "query": "volvo", "sellerType": "private", "withPhoneOnly": true, "oneRowPerSeller": true, "maxItems": 500 }
```

```json
{ "startUrls": ["https://www.otomoto.pl/dostawcze/kamper", "https://www.otomoto.pl/osobowe/oferta/bmw-x5-ID6IbcJS.html"], "extractSellerContacts": true }
```

### ▶️ Want to try other Otomoto scrapers?

This is currently the only Otomoto.pl Actor in the SimpleAPI catalog. If your workflow needs contact-lead extraction from a different platform, these SimpleAPI scrapers cover a similar ground:

| Scraper | What it extracts |
| --- | --- |
| Google Maps Extractor with Reservation/Order Online Links | Google Maps business listings plus the online booking platform (OpenTable, Resy, SevenRooms, and others) each venue runs on |
| Facebook Group Posts And Details & Contact Leads Scraper | Facebook group posts, group profile data, and email/phone mentions typed by members themselves |
| Pinterest Profile Scraper With Email & Website Contacts | Public Pinterest profile bios plus emails, phone numbers, and social handles from the linked website |
| TikTok Shop Data Scraper: Reviews & Sentiment Analysis | TikTok Shop products, reviews, and reviewer trust signals by keyword, category, or seller store |

### How to extract Otomoto.pl data programmatically

Every run is a standard Apify Actor call: one API call with your Apify token, a JSON input body, and structured JSON back — no separate Otomoto credentials required.

#### Python example

```python
from apify_client import ApifyClient

client = ApifyClient("<APIFY_API_TOKEN>")

run = client.actor("SimpleAPI/otomoto-pl-scraper-with-dealer-contact-leads").call(
    run_input={
        "category": "cars",
        "query": "bmw seria 3",
        "location": "Kraków",
        "sellerType": "dealer",
        "withPhoneOnly": True,
        "maxItems": 100,
    }
)

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    contacts = item.get("sellerContacts") or {}
    print(item["title"], item["price"], contacts.get("phones"))
```

#### Export to spreadsheets or CRM

From the Apify Console, use the dataset's **Export** button to download CSV or Excel directly, or select the **☎️ Adverts + seller contacts** view for a flattened table. Map `title`, `price`, and the `sellerContacts.dealerName`/`phones`/`city` keys to your CRM's lead columns — `hasPhone` gives you a ready-made filter for a "call-ready" segment before import.

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

Scraping publicly accessible Otomoto.pl advert pages is generally lawful — these listings are published specifically to be found and contacted by buyers, and this Actor reads only what a visitor to the public page (and Otomoto's own public contact endpoint) can already see. No login, cookie, or bypass of access controls is used.

The legal framing depends on which rows you collect. With `sellerType: "dealer"` (the default), output is business/dealership data — dealership name, storefront, and stock count — governed by Otomoto's Terms of Service and database-rights considerations, not personal-data law. With `sellerType: "private"` or `"any"`, `dealerName` and `phones` can identify an **individual** rather than a business; that output is personal data and falls under GDPR (and CCPA where applicable), which requires a lawful basis for storage and further use. Consult legal counsel for commercial applications involving bulk storage of personal data.

### ❓ FAQ

#### Do sold or removed adverts still show up in the results?

Yes, but as a free, uncharged row rather than being silently dropped: `type: "unavailable"`, with `httpStatus` and `unavailableReason: "advert_removed_or_expired"`. Every contact field on that row is `null` because nothing was ever fetched for it.

#### Can I get seller phone numbers along with the advert data?

Yes — it's the Actor's core feature. With `extractSellerContacts: true` (the default), each advert's published phone numbers are resolved live from Otomoto's own contact endpoint (up to 5 per advert) and attached in `sellerContacts.phones`, alongside dealership name, address, storefront URL, live stock count, working hours, and badges. `hasPhone` and `sellerContactsFetched` tell you whether a number was actually found versus whether the lookup was even attempted.

#### How accurate is the phone and price data?

The Actor returns data exactly as Otomoto publishes it at request time — the phone number is read live from Otomoto's own endpoint, not guessed or completed from another source, and `price`/`priceList` reflect the advert's current listed price. Accuracy depends on the seller keeping the ad updated; re-run periodically if you need current stock and pricing.

#### How many adverts can I get per run?

`maxItems` caps the number of advert **detail pages** fetched, from 1 up to 100,000 (default 10). In `oneRowPerSeller` mode, the pushed dataset can hold fewer rows than `maxItems` because multiple adverts collapse into one seller row — it never holds more.

#### How do I target only dealerships, or only sellers I can actually call?

Use `sellerType: "dealer"` (or `"private"`) to apply Otomoto's own business/private split server-side, before any advert page is fetched, and add `withPhoneOnly: true` to drop rows where no phone number could be resolved. Combine both with `oneRowPerSeller: true` to get one qualified lead per business instead of one row per listing.

#### Does Otomoto.pl Scraper With Dealer Contact Leads work with Claude, ChatGPT, and AI agent frameworks?

There is no dedicated MCP server for this Actor. It is callable as a standard HTTP endpoint through the Apify API by any agent framework that can make a tool call and read JSON — LangChain, LlamaIndex, or a custom agent loop can all invoke it as a scraping tool.

#### How does this compare to other Otomoto scrapers?

The distinguishing capability, verifiable from this Actor's own source and input schema, is the combination of plaintext phone-number resolution, the server-side dealer/private split, and the one-row-per-seller collapse in a single run. Check any other Otomoto listing's own documentation for its specific field coverage before comparing on price or speed.

#### Can I use this without an Otomoto account or API key?

Yes. You need only an Apify account and API token to run it — no Otomoto login, cookie, or API key of any kind is required, since the Actor reads public pages and Otomoto's own keyless contact endpoint.

#### What happens if my search matches zero adverts?

The run fails with a clear error naming the problem — Otomoto reporting zero total results, or Otomoto not actually applying a filter you asked for (an unrecognized brand, model, or location) — rather than silently returning an empty or wrongly-scoped dataset labeled as a success.

### Conclusion

Otomoto.pl Scraper With Dealer Contact Leads turns an Otomoto category or brand search into a structured, contactable dataset: vehicle listing details plus plaintext seller phone numbers, dealership name, address, and live stock count, filterable by dealer/private split and phone availability. It's built for sales teams, lead-gen agencies, and market analysts who need Otomoto data as clean JSON rather than a spreadsheet built by hand. Start a run from the Apify Console or the API with your search filters, or paste Otomoto URLs directly, and open the **☎️ Adverts + seller contacts** view when it finishes.

# Actor input Schema

## `extractSellerContacts` (type: `boolean`):

Resolve each advert's published phone numbers and attach the seller's outreach record (dealership name, address, storefront URL, live stock count, opening hours, badges). Costs one small extra request per published number, plus one to detect the end of the list. Leave this ON - it is what this actor is for. When it is OFF, sellerContacts, hasPhone and sellerContactsFetched are all null (never false, never an empty list) because nothing was asked.

## `sellerType` (type: `string`):

Applied by Otomoto BEFORE any advert page is fetched (search\[private\_business]), so it shrinks the run rather than filtering afterwards. Measured on /osobowe/bmw: 19,601 adverts unfiltered, 8,229 dealerships, 11,372 private sellers. Every returned advert is additionally checked row by row and a contradicting advert is dropped and logged. Ignored when you paste Start URLs - those override every search filter.

## `withPhoneOnly` (type: `boolean`):

Drop advert rows where no phone number could be read, so the file handed to a sales team has no dead lines in it. Free rows for removed adverts are never dropped. OFF by default so your first run shows the true hit rate before you decide. Requires the phone reader above to be on; if the filter removes every row the run FAILS loudly instead of finishing with an empty dataset.

## `oneRowPerSeller` (type: `boolean`):

Collapse every advert from the same seller id into a single row - the advert with the most resolved phone numbers and the richest contact record - and add sellerAdIds plus sellerAdCount describing the rest. The surviving row is still a complete advert record; no field is dropped. Rows are buffered and pushed at the end of the run instead of streaming. Note that Max Items still caps how many ADVERT pages are fetched, so collapsing 40 adverts into 6 sellers still fetches 40 adverts.

## `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/bmw-x5-ID6IbcJS.html). When this list is non-empty it OVERRIDES every search filter below, including the dealer/private split. 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 sweep. 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.

## `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 ADVERT pages are scraped. In one-row-per-seller mode the dataset can hold fewer rows than this because adverts are collapsed, never more. Free accounting rows for removed adverts are never charged. Default is 10.

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

How many Otomoto requests are made in parallel, advert pages and phone lookups alike. 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
{
  "extractSellerContacts": true,
  "sellerType": "dealer",
  "withPhoneOnly": false,
  "oneRowPerSeller": false,
  "startUrls": [],
  "category": "cars",
  "query": "volvo",
  "location": "Warszawa",
  "sort": "relevance_web",
  "maxItems": 10,
  "concurrency": 10,
  "requestDelay": 0,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# 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 = {
    "sellerType": "dealer",
    "query": "volvo",
    "location": "Warszawa",
    "maxItems": 10,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("simpleapi/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 = {
    "sellerType": "dealer",
    "query": "volvo",
    "location": "Warszawa",
    "maxItems": 10,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("simpleapi/otomoto-pl-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "sellerType": "dealer",
  "query": "volvo",
  "location": "Warszawa",
  "maxItems": 10,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call simpleapi/otomoto-pl-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=simpleapi/otomoto-pl-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/VOHc7roW2aSxuPgIS/builds/LIdznmOqi8IpsUYNT/openapi.json
