# Otomoto Scraper - Car Listings, Prices & New-Ad Alerts (`lukehunter/otomoto-scraper`) Actor

Scrape public Otomoto.pl passenger-car search results into flat rows: price, make, model, year, mileage, fuel, gearbox, location, seller type and listing date. New-ad alert mode can send new listings to Discord, Telegram, Slack or any webhook.

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

## Pricing

$1.50 / 1,000 cars

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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 Scraper - Car Listings, Prices & New-Ad Alerts

**For Polish car dealers, importers, resellers and market analysts who want Otomoto.pl passenger-car listings as data.** Give it public Otomoto `/osobowe` search URLs and it returns one flat row per car: price, make, model, year, mileage, fuel type, gearbox, location, seller type (private or professional) and listing date. Turn on new-ad alert mode and run it on a schedule to get only newly posted cars for a saved search.

**$0.0015 per listing — 1,000 listings = $1.50.** A run that delivers nothing has no per-listing charge.

### Quick start (1 minute)

1. Open the **Input** tab and paste one or more Otomoto search-result URLs (the prefilled one works as-is).
2. Leave **Maximum pages per search** at 2 and **Maximum cars** at 20 for a first test run.
3. Click **Start**. Export the dataset as JSON, CSV or Excel, or pull it through the API shown below.

```json
{
  "startUrls": [
    "https://www.otomoto.pl/osobowe/bmw"
  ],
  "maxPagesPerSearch": 2,
  "maxItems": 20,
  "onlyNewSinceLastRun": false
}
```

That prefilled example finishes in well under a minute and costs at most $0.03. `maxPagesPerSearch` is capped at 10, `maxItems` defaults to 100 and is hard-capped at 5,000.

### How it works

1. Reads each search URL you give it and checks the site's live `robots.txt` for the exact page path before every request. A disallowed page is skipped without being fetched.
2. Parses the structured listing data that the public search-result page itself embeds, into one flat row per listing.
3. Delivers (and charges for) rows only up to your `maxItems` cap.
4. Reads politely: about one request every three seconds with an honest, self-identifying User-Agent. Blocks and challenge pages are reported in the run's status message, never bypassed.

### Use cases

- **Otomoto price tracker**: compare asking prices and mileage across a make or model search, and watch how they move over time.
- **New-car-ad alerts**: watch a saved search on a schedule and receive only ads not delivered by earlier runs, for sourcing or flipping.
- **Used-car market dataset**: build a current Polish vehicle dataset by year, fuel type, gearbox and seller type for pricing models.
- **Dealer competitor monitoring**: track what comparable cars are listed for in your region before setting your own prices.

### New-ad alerts: get only new listings

Turn on `onlyNewSinceLastRun` and put the Actor on a **schedule** to turn any saved search into a new-ad alert: after the first run, each run delivers (and charges for) only listings it has not delivered before for that exact search URL.

```json
{
  "startUrls": [
    "https://www.otomoto.pl/osobowe/bmw"
  ],
  "maxPagesPerSearch": 2,
  "maxItems": 100,
  "onlyNewSinceLastRun": true
}
```

1. Set your search URL as usual and switch `onlyNewSinceLastRun` on.
2. Click **Schedule** on the run page (or create one under **Schedules** in the Apify Console), for example every 30-60 minutes for a fast-moving search.
3. **The first scheduled run is a baseline**: it delivers every listing currently shown and remembers it, and you are charged normally for it. Later runs deliver only unseen listings.
4. Add a **Webhook** under the schedule's **Integrations** for `ACTOR.RUN.SUCCEEDED` (Apify's Slack or email integration, or a Zapier/Make step) to be notified when new listings arrive.

Each search URL keeps its own seen-ID history, so several saved searches in one task never mix. `stateStoreName` only needs changing to isolate or reset that memory. A run that finds nothing new still succeeds with an empty dataset.

### Get Otomoto alerts in Discord, Telegram, Slack, Teams, Google Chat, your phone (ntfy/Pushover) or any app (webhook)

Want a **Otomoto Discord alert**, a **Otomoto Telegram bot**, or new cars pushed to Slack, Teams, Google Chat or your phone instead of checking a dataset? Add one or more destinations in the Actor's **Notifications (optional)** section and each run sends the cars it delivered. It is a **monitor that notifies you**: it only reads public Otomoto search pages and never contacts sellers, buys anything or logs in anywhere, and the messages go only to the places you set up.

| Channel | Fill in | What you get |
|---|---|---|
| Discord | `discordWebhookUrl` | One card per car, 10 per message |
| Telegram | `telegramBotToken` + `telegramChatId` | Up to 10 cars per message, one line each |
| Slack | `slackWebhookUrl` | Block Kit message, up to 10 cars per message |
| Mattermost, Rocket.Chat | `slackCompatibleWebhookUrl` | Slack-style incoming webhook: a Markdown list (these apps do not render Slack Block Kit) |
| Microsoft Teams | `teamsWebhookUrl` | Adaptive Card from a Teams Workflows webhook, up to 10 cars per card |
| Google Chat | `googleChatWebhookUrl` | Card in a space, up to 10 cars per message |
| ntfy (phone push, no account) | `ntfyTopic` (optional `ntfyServer`, `ntfyToken`) | A push per car, up to 10, then one "...and N more" |
| Pushover (phone push) | `pushoverUserKey` + `pushoverAppToken` | A push per car, up to 10, then one "...and N more" |
| Any app: Zapier, Make, n8n, Pipedream, IFTTT, Home Assistant | `webhookUrl` | One JSON POST per run with the delivered rows |

Fill in as many as you like; each channel works on its own and a failing one never stops the others.

#### Setup, channel by channel

- **Discord (1 minute):** Server Settings → Integrations → Webhooks → New Webhook → pick the channel → **Copy Webhook URL**.
- **Telegram (2 minutes):** chat with **@BotFather**, send `/newbot`, paste the token into `telegramBotToken`. Add the bot to the chat or channel (administrator for a channel; for a chat with yourself press **Start** on the bot). `telegramChatId` is a numeric id (`123456789`, or `-1001234567890` for a group or channel) or a public `@channelusername`.
- **Slack:** at api.slack.com/apps create an app (**From scratch**) → **Incoming Webhooks** → switch it on → **Add New Webhook to Workspace** → pick the channel → copy the `https://hooks.slack.com/services/...` URL.
- **Mattermost or Rocket.Chat:** create an **incoming webhook** in the app and paste its public `https://` URL into `slackCompatibleWebhookUrl`. Localhost, private IP addresses and `.local` / `.internal` names are rejected.
- **Microsoft Teams:** in the channel open **... → Workflows** → **Post to a channel when a webhook request is received** → finish the wizard and copy the URL (a host ending in `.logic.azure.com` or `.powerplatform.com`). The old "Office 365 connector" webhooks are retired and not supported.
- **Google Chat:** in the space open **Apps & integrations → Webhooks → Add webhook**, name it and copy the `https://chat.googleapis.com/v1/spaces/...` URL (needs a Google Workspace account that allows webhooks).
- **ntfy (free, no account):** install the ntfy app (or open ntfy.sh), subscribe to a topic with a long random name, and put the same name in `ntfyTopic`. Anyone who knows the topic can read it, so make it unguessable. Own server or a protected topic: set `ntfyServer` (public `https://` address) and `ntfyToken`.
- **Pushover:** copy your user key from pushover.net into `pushoverUserKey`, create an application at pushover.net/apps/build and put its API token in `pushoverAppToken`. Pushover itself is a separate paid app after its trial: check their pricing.
- **Any app (webhook):** paste the catch-hook URL from Zapier, Make, n8n, Pipedream, IFTTT or Home Assistant into `webhookUrl`. It must be a public `https://` address.

#### The recipe: a Otomoto new-car alert in five steps

1. Set your search on otomoto.pl and paste its URL into `startUrls`.
2. Turn on `onlyNewSinceLastRun`, so only cars not delivered before are sent.
3. Add your destination(s) from the table above.
4. Run it once. The first run is the baseline and sends what is currently listed (up to `maxNotifications`).
5. Create a **Schedule** (for example every few hours). From then on you only hear about cars it has not delivered before for that search.

```json
{
  "startUrls": [
    "https://www.otomoto.pl/osobowe/bmw"
  ],
  "onlyNewSinceLastRun": true,
  "maxItems": 100,
  "ntfyTopic": "your-long-random-topic-name",
  "maxNotifications": 25
}
```

#### The generic webhook (`webhookUrl`)

Each run sends **one POST per 100 delivered rows** (so usually one) with this body, followed by whatever your tool does with it:

```json
{
  "source": "apify",
  "actor": "otomoto-scraper",
  "runId": "HG7ML7M8z78YcAPEB",
  "count": 2,
  "total": 2,
  "part": 1,
  "parts": 1,
  "items": [ { "...": "the full dataset rows" } ]
}
```

`count` is the number of rows in this POST, `total` the number in the run, and `part` / `parts` number the POSTs when a run delivers more than 100 rows. `maxNotifications` does not cap the webhook. Your endpoint should answer with a 2xx status; redirects are not followed and a failure is reported in the run status, never retried in a loop.

#### What to expect

- **What a message contains:** title, price in the listing's own currency, year, mileage, fuel type, gearbox, location, seller type (professional or private) and the listed date, plus the link. The rows carry no seller names or phone numbers, so none are sent.
- **Only delivered cars are sent.** A run that finds nothing new sends nothing, so there is no "no new cars" spam. Rows the run could not deliver (for example because your spending limit was reached) are not sent.
- **`maxNotifications` (default 25, max 100)** caps how many cars go to chat and phone channels per run (the webhook is not capped). The rest are still in the dataset and the status message says so. ntfy and Pushover send at most 10 individual pushes, then one "...and N more" summary.
- **Cost: notifications are free.** You pay only for delivered cars at the usual price, exactly as without alerts. There is no extra charge for any channel.
- **Failures never fail the run.** A deleted webhook or an unreachable service leaves the run successful and the dataset complete, and the status message says what happened, for example `Notified: Discord 12, Slack 12, ntfy 10 (+summary). Teams failed (HTTP 404) - check the webhook URL.` The logs show the channel name and HTTP status only, never a URL or token.
- **Your URLs, tokens and ntfy topic are stored as secret inputs.** Anyone holding a webhook URL can post to that channel, so treat it like a password.
- **The dataset is unchanged.** The output rows are identical whether or not you set a destination; the run's status message just gains a short note such as `Notified: Discord 12, ntfy 10 (+summary).`
- **It is a monitor that notifies you.** It never contacts sellers, buys anything or logs in to Otomoto, and the messages go only to the destinations you set up.

### Input

| Field | Type | Default | Description |
|---|---|---:|---|
| `startUrls` | string\[] | *(prefilled)* | Public Otomoto passenger-car search URLs, e.g. `https://www.otomoto.pl/osobowe/bmw`. Must be `https://www.otomoto.pl/osobowe...` search pages, not ad pages. Up to 50 URLs per run. Required. |
| `maxPagesPerSearch` | integer | 2 | 1-10. How many result pages to fetch per search URL. |
| `maxItems` | integer | 100 | 1-5,000. Hard cap on rows delivered across the run: your cost cap. |
| `onlyNewSinceLastRun` | boolean | `false` | New-ad alert mode: deliver only listings not delivered by an earlier run of the same search. |
| `stateStoreName` | string | `otomoto-scraper-seen-items` | Named key-value store that remembers delivered listing IDs for alert mode. Letters, numbers and hyphens only. |
| `discordWebhookUrl` | string (secret) | *(none)* | Discord webhook URL. Must look like `https://discord.com/api/webhooks/<id>/<token>`. Empty means no Discord messages. |
| `telegramBotToken` | string (secret) | *(none)* | Telegram bot token from @BotFather. Needs `telegramChatId`. |
| `telegramChatId` | string | *(none)* | Telegram chat to post to: a numeric chat id or `@channelusername`. The bot must be added to that chat or channel. Needs `telegramBotToken`. |
| `slackWebhookUrl` | string (secret) | *(none)* | Slack incoming webhook, `https://hooks.slack.com/services/...`. |
| `slackCompatibleWebhookUrl` | string (secret) | *(none)* | Mattermost / Rocket.Chat incoming webhook (public `https://` URL; Markdown list, no Block Kit). |
| `teamsWebhookUrl` | string (secret) | *(none)* | Microsoft Teams Workflows webhook (host ending `.logic.azure.com` or `.powerplatform.com`). |
| `googleChatWebhookUrl` | string (secret) | *(none)* | Google Chat space webhook, `https://chat.googleapis.com/v1/spaces/...`. |
| `ntfyTopic` | string (secret) | *(none)* | ntfy topic name; setting it turns ntfy phone pushes on. Pick a long, unguessable name. |
| `ntfyServer` | string | `https://ntfy.sh` | ntfy server. Only change it for your own public `https://` ntfy server. |
| `ntfyToken` | string (secret) | *(none)* | Optional ntfy access token for a self-hosted server or protected topic. Needs `ntfyTopic`. |
| `pushoverUserKey` | string (secret) | *(none)* | Pushover user key. Needs `pushoverAppToken`. |
| `pushoverAppToken` | string (secret) | *(none)* | Pushover application API token. Needs `pushoverUserKey`. |
| `webhookUrl` | string (secret) | *(none)* | Generic JSON webhook (Zapier, Make, n8n, Pipedream, IFTTT, Home Assistant): the delivered rows, 100 per POST. Public `https://` only. |
| `maxNotifications` | integer | 25 | 1–100. Most cars sent to chat and phone channels per run (the webhook is not capped). Extra rows are still in the dataset. Only used when a destination is set. |

### Output fields

| Category | Fields |
|---|---|
| Identity | `id`, `url`, `title` |
| Price | `price`, `currency` |
| Vehicle | `make`, `model`, `year`, `mileageKm`, `fuelType`, `gearbox` |
| Location | `location` |
| Advertiser | `sellerType` |
| Timing | `date` |
| Search context | `searchUrl`, `page`, `scrapedAt` |

Missing values are returned as `null`. Nothing is guessed or invented.

### Example output

```json
{
  "id": "6150904338",
  "url": "https://www.otomoto.pl/osobowe/oferta/bmw-seria-5-ID6Igzcu.html",
  "title": "BMW Seria 5",
  "price": 56200,
  "currency": "PLN",
  "make": "BMW",
  "model": "Seria 5",
  "year": 2018,
  "mileageKm": 71754,
  "fuelType": "Benzyna",
  "gearbox": "Automatyczna",
  "location": "Warszawa, Mazowieckie",
  "sellerType": "private",
  "date": "2026-10-02T15:28:16Z",
  "searchUrl": "https://www.otomoto.pl/osobowe/bmw",
  "page": 1,
  "scrapedAt": "2026-10-03T00:00:00.000Z"
}
```

`sellerType` is `private` or `professional`. `fuelType` and `gearbox` are returned in Polish, as Otomoto shows them (e.g. `Benzyna`, `Automatyczna`). `scrapedAt` in this example is the fixed timestamp used for the committed test fixtures; real runs carry the actual scrape time.

### Use it as an API

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")

run = client.actor("lukehunter/otomoto-scraper").call(
    run_input={
        "startUrls": ["https://www.otomoto.pl/osobowe/bmw"],
        "maxPagesPerSearch": 2,
        "maxItems": 200,
    }
)

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["title"], item["price"], item["currency"])
```

### Pricing and cost control

Pay per delivered listing: **$0.0015 per listing — 1,000 listings = $1.50.**

| Rows delivered | Cost |
|---:|---:|
| 20 | $0.03 |
| 100 | $0.15 |
| 1,000 | $1.50 |
| 5,000 (run maximum) | $7.50 |

There is no charge for starting a run, and none for a search that is skipped, blocked or empty. Billing happens only after rows are accepted for delivery, and `maxItems` is a hard upper bound on cost. In alert mode, listings already delivered by an earlier run are skipped and not charged again.

Notifications to Discord, Telegram, Slack, Teams, Google Chat, ntfy, Pushover and webhooks are free: you pay only for delivered cars, never for messages sent.

### Limitations

- **Passenger cars only.** Only `https://www.otomoto.pl/osobowe...` search URLs are accepted. Other vehicle categories, and individual ad pages (`/oferta/`), are rejected.
- **Only robots-allowed paths are read.** Otomoto's `robots.txt` disallows areas such as `/api/`, `/ajax/`, `/catalog/*/*/`, RSS feeds and account pages; this Actor never requests them.
- **No seller personal data, by design.** `sellerType` only says whether the seller is private or professional. Names, phone numbers, contact forms and seller profile pages are never collected.
- **Search-result pages only.** Detail pages are never fetched, so a listing's full description, all photos and other detail-page-only fields are not included.
- **robots.txt is honoured on every page.** If page 2 or later is blocked, disallowed or fails, rows already delivered from earlier pages are kept and the run reports that it was truncated.
- **No login, proxies, cookies or CAPTCHA handling.** If the site shows a block or challenge page, the Actor reports it and stops that search rather than attempting to evade it. You are not charged for a blocked search.
- Prices and availability reflect the listing at scrape time; sellers can change or remove an ad at any moment.
- This is an independent tool, not affiliated with, endorsed by, or connected to Otomoto. You are responsible for using the data in accordance with the site's terms and applicable law.

### Use it from your code, Make, Zapier or n8n

Built for automated pipelines: small runs, stable field names, one row per item, and you pay only for rows actually delivered.

**One API call that runs the Actor and returns the data** (replace `$APIFY_TOKEN` with your token from Apify Console → Settings → Integrations):

```bash
curl -s -X POST "https://api.apify.com/v2/acts/lukehunter~otomoto-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"startUrls": ["https://www.otomoto.pl/osobowe/bmw"], "maxPagesPerSearch": 2, "maxItems": 20}'
```

**JavaScript** (`npm i apify-client`):

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('lukehunter~otomoto-scraper').call({
  "startUrls": [
    "https://www.otomoto.pl/osobowe/bmw"
  ],
  "maxPagesPerSearch": 2,
  "maxItems": 20
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

**Python** (`pip install apify-client`):

```python
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('lukehunter~otomoto-scraper').call(run_input={"startUrls": ["https://www.otomoto.pl/osobowe/bmw"], "maxPagesPerSearch": 2, "maxItems": 20})
items = client.dataset(run['defaultDatasetId']).list_items().items
```

**Schedules, webhooks and no-code tools:** save your input as a Task, add a Schedule (e.g. hourly or daily), and attach a webhook or one of the Integrations tab's connectors (Make, Zapier, n8n, Google Sheets, Slack) to push each run's results into your own system.

**Only new items each run:** set `"onlyNewSinceLastRun": true` on a scheduled Task and every run after the first delivers, and charges for, only items it hasn't delivered before for that same input. That makes it a ready-made change feed.

**Reliability:** an automated test runs this Actor's parser against the live site every day, and failures are fixed fast.

**Feedback:** an honest rating on the Store page, good or bad, helps others decide if this Actor fits their needs. Missing a field or found a problem? Open an issue on the Actor page.

### FAQ

#### How much does it cost?

$0.0015 per listing delivered, so 1,000 listings cost $1.50. Set `maxItems` to cap the spend per run; a run that delivers nothing is free.

#### Can I get alerts for new listings only?

Yes. Set `onlyNewSinceLastRun: true` and run the Actor on a schedule; see "New-ad alerts" above. The first run is a baseline that delivers everything currently listed, and later runs deliver only new ads. Schedule it with Apify **Schedules** (every 30-60 minutes for alerts, or daily for market tracking) and add a webhook to push new ads to Slack, email or Zapier. Each scheduled run is billed only for the rows it delivers.

#### What export formats are available?

The dataset can be exported as JSON, CSV, Excel, XML, HTML or RSS from the Console or via the Apify API, and it connects to Google Sheets, Zapier, Make and webhooks.

#### Does it collect seller names or phone numbers?

No. Only listing fields are returned, and `sellerType` just flags private versus professional sellers.

#### Can I scrape motorcycles, trucks or individual ad pages?

No. Only `/osobowe` passenger-car search-result pages are supported.

#### Can I get Otomoto alerts by email?

Not natively. There are two routes. (1) Set `webhookUrl` to a Zapier, Make or n8n catch hook and add an email or Gmail step: the webhook delivers the cars as JSON, so the email can list them. (2) Apify's own run integrations and run notifications can email you when a run finishes, but they only send a link to the run or dataset, not the cars themselves. For instant alerts without building anything, Telegram, ntfy or Pushover reach your phone directly.

#### Can I get Otomoto alerts in WhatsApp?

Not natively, and we do not use unofficial WhatsApp APIs, which risk getting your number banned. The official route is the WhatsApp Business Platform (Cloud API) with its own business account, message templates and pricing: point `webhookUrl` at a Zapier, Make or n8n flow that calls it. For free phone alerts use Telegram or ntfy instead.

### More from this publisher

- [Vinted Scraper](https://apify.com/lukehunter/vinted-scraper)
- [Vinted Cross-Country Prices](https://apify.com/lukehunter/vinted-cross-country-prices)
- [Otodom Scraper](https://apify.com/lukehunter/otodom-scraper)
- [AutoScout24 Scraper](https://apify.com/lukehunter/autoscout24-scraper)

# Actor input Schema

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

Public Otomoto search-result URLs to scrape. The prefilled URL is a captured search known to return at least 10 listings. Maximum 50 URLs per run.

## `maxPagesPerSearch` (type: `integer`):

How many result pages to fetch for each search URL, 1-10, default 2. Every page URL is checked against robots.txt before it is fetched.

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

Hard cap on delivered rows across the run, 1-5000, default 100. Charging is per delivered car only.

## `onlyNewSinceLastRun` (type: `boolean`):

When enabled, each search URL keeps a cross-run seen-ID set. Listings delivered by a previous run of that same search are skipped and not charged again.

## `stateStoreName` (type: `string`):

Named key-value store used only by new-ad alert mode to remember listing IDs across scheduled runs. Letters, numbers and hyphens only.

## `discordWebhookUrl` (type: `string`):

Post results to a Discord channel. In Discord: Server Settings -> Integrations -> Webhooks -> New Webhook -> choose the channel -> Copy Webhook URL. Looks like https://discord.com/api/webhooks/<id>/<token>. Leave empty to skip Discord.

## `telegramBotToken` (type: `string`):

Post results to Telegram. Create a bot by chatting with @BotFather on Telegram (send /newbot) and paste the token it gives you, e.g. 123456789:AA.... Needs "Telegram chat ID" below. Leave empty to skip Telegram.

## `telegramChatId` (type: `string`):

Where the bot should post: a numeric chat id (e.g. 123456789, or -1001234567890 for a group or channel) or a public @channelusername. The bot must be added to that chat or channel first (as an administrator for a channel), and for a private chat you must press Start on the bot. Needs "Telegram bot token" above.

## `slackWebhookUrl` (type: `string`):

Post results to a Slack channel (Block Kit cards, up to 10 per message). Create an incoming webhook at api.slack.com/apps -> Create New App -> Incoming Webhooks -> Add New Webhook to Workspace. Looks like https://hooks.slack.com/services/T000/B000/XXXX. Leave empty to skip Slack.

## `slackCompatibleWebhookUrl` (type: `string`):

Post results to a Mattermost or Rocket.Chat incoming webhook, which accept Slack-style webhook payloads. Must be a public https:// address (no localhost, private IP or .local/.internal hosts). These apps do not render Slack Block Kit, so they get a plain Markdown list. For real Slack use "Slack webhook URL" above. Leave empty to skip.

## `teamsWebhookUrl` (type: `string`):

Post results to a Teams channel as an Adaptive Card. In Teams: channel -> ... -> Workflows -> "Post to a channel when a webhook request is received" -> copy the URL. The host must end in .logic.azure.com or .powerplatform.com. The retired Office 365 connector webhooks are not supported. Leave empty to skip Teams.

## `googleChatWebhookUrl` (type: `string`):

Post results to a Google Chat space. In the space: Apps & integrations -> Webhooks -> Add webhook -> copy the URL. Looks like https://chat.googleapis.com/v1/spaces/<space>/messages?key=...\&token=.... Leave empty to skip Google Chat.

## `ntfyTopic` (type: `string`):

Get a phone push with no account via ntfy (ntfy.sh). Install the ntfy app, subscribe to a topic name, and enter the same name here. Anyone who knows the topic can read it, so pick a long, unguessable name (letters, numbers, - and \_). At most 10 individual pushes per run, then one "...and N more". Leave empty to skip ntfy.

## `ntfyServer` (type: `string`):

Only used with "ntfy topic". Leave as https://ntfy.sh, or enter the public https:// address of your own ntfy server.

## `ntfyToken` (type: `string`):

Only used with "ntfy topic", for a self-hosted server or a reserved topic that needs a login. An access token from ntfy (starts with tk\_). Leave empty for open topics.

## `pushoverUserKey` (type: `string`):

Get a phone push via Pushover. Your user key is shown on the Pushover dashboard (pushover.net). Needs "Pushover app token" below. At most 10 individual pushes per run, then one "...and N more". Leave empty to skip Pushover.

## `pushoverAppToken` (type: `string`):

The API token of a Pushover application you create at pushover.net/apps/build (name it after this Actor). Needs "Pushover user key" above.

## `webhookUrl` (type: `string`):

POST the delivered rows as JSON to any public https:// URL: {source, actor, runId, count, items}. At most 100 rows per POST; every delivered row is sent regardless of the max-notifications cap. Use it to route results to email, a spreadsheet or any app. Localhost, private IP and .local/.internal addresses are rejected. Leave empty to skip.

## `maxNotifications` (type: `integer`):

Caps how many results are sent to chat and phone channels per run (Discord, Telegram, Slack, Teams, Google Chat, ntfy, Pushover), 1-100, default 25. Extra results are still in the dataset. The generic webhook is not capped. Only used when a destination above is set.

## Actor input object example

```json
{
  "startUrls": [
    "https://www.otomoto.pl/osobowe/bmw"
  ],
  "maxPagesPerSearch": 2,
  "maxItems": 20,
  "onlyNewSinceLastRun": false,
  "stateStoreName": "otomoto-scraper-seen-items",
  "ntfyServer": "https://ntfy.sh",
  "maxNotifications": 25
}
```

# Actor output Schema

## `items` (type: `string`):

No description

# 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 = {
    "startUrls": [
        "https://www.otomoto.pl/osobowe/bmw"
    ],
    "maxPagesPerSearch": 2,
    "maxItems": 20,
    "onlyNewSinceLastRun": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("lukehunter/otomoto-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 = {
    "startUrls": ["https://www.otomoto.pl/osobowe/bmw"],
    "maxPagesPerSearch": 2,
    "maxItems": 20,
    "onlyNewSinceLastRun": False,
}

# Run the Actor and wait for it to finish
run = client.actor("lukehunter/otomoto-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 '{
  "startUrls": [
    "https://www.otomoto.pl/osobowe/bmw"
  ],
  "maxPagesPerSearch": 2,
  "maxItems": 20,
  "onlyNewSinceLastRun": false
}' |
apify call lukehunter/otomoto-scraper --silent --output-dataset

```

## MCP server setup

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