Otodom Scraper - Property Listings, Prices & New-Ad Alerts
Pricing
$1.50 / 1,000 properties
Otodom Scraper - Property Listings, Prices & New-Ad Alerts
Scrape public Otodom.pl property search results into flat rows: price, price/m², area, rooms, floor, location, property type, transaction, agency and listing date. New-ad alert mode can send new listings to Discord, Telegram, Slack or any webhook.
Pricing
$1.50 / 1,000 properties
Rating
0.0
(0)
Developer
Luke Hunter
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
6 days ago
Last modified
Categories
Share
For Polish property investors, agents, analysts and flat-hunters who want Otodom.pl listing data in a spreadsheet. Give it public Otodom search-result URLs and it returns one flat row per property: price, price per m², area, rooms, floor, location, property type, sale or rent, the business agency (where there is one) and the listing date. Turn on new-ad alert mode and run it on a schedule to get only newly appearing ads 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)
- Open the Input tab and paste one or more Otodom search-result URLs (the prefilled one works as-is).
- Leave Maximum pages per search at 2 and Maximum properties at 20 for a first test run.
- Click Start. Export the dataset as JSON, CSV or Excel, or pull it through the API shown below.
{"startUrls": ["https://www.otodom.pl/pl/wyniki/sprzedaz/mieszkanie/mazowieckie/warszawa/warszawa/warszawa"],"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
- Reads each search URL you give it and checks the site's live
robots.txtfor the exact page path before every request. A disallowed page is skipped without being fetched. - Parses the structured listing data that the public search-result page itself embeds, into one flat row per listing.
- Delivers (and charges for) rows only up to your
maxItemscap. - 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
- Otodom price tracker: run a Warsaw (or any city, district or category) search on a schedule and compare
priceandpricePerM2over time for investment research. - New-ad alerts for flat hunting: monitor a saved neighbourhood search and receive only ads that were not there on the previous run.
- Comparable listings for market analysis: export price per m², area, rooms and floor for a district to benchmark asking prices.
- Polish property data export: pull current sale or rent inventory from Otodom.pl into CSV or Excel without copying listings by hand.
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.
{"startUrls": ["https://www.otodom.pl/pl/wyniki/sprzedaz/mieszkanie/mazowieckie/warszawa/warszawa/warszawa"],"maxPagesPerSearch": 2,"maxItems": 100,"onlyNewSinceLastRun": true}
- Set your search URL as usual and switch
onlyNewSinceLastRunon. - 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.
- 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.
- 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 Otodom alerts in Discord, Telegram, Slack, Teams, Google Chat, your phone (ntfy/Pushover) or any app (webhook)
Want a Otodom Discord alert, a Otodom Telegram bot, or new listings 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 listings it delivered. It is a monitor that notifies you: it only reads public Otodom 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 listing, 10 per message |
| Telegram | telegramBotToken + telegramChatId | Up to 10 listings per message, one line each |
| Slack | slackWebhookUrl | Block Kit message, up to 10 listings 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 listings per card |
| Google Chat | googleChatWebhookUrl | Card in a space, up to 10 listings per message |
| ntfy (phone push, no account) | ntfyTopic (optional ntfyServer, ntfyToken) | A push per listing, up to 10, then one "...and N more" |
| Pushover (phone push) | pushoverUserKey + pushoverAppToken | A push per listing, 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 intotelegramBotToken. Add the bot to the chat or channel (administrator for a channel; for a chat with yourself press Start on the bot).telegramChatIdis a numeric id (123456789, or-1001234567890for 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 intoslackCompatibleWebhookUrl. Localhost, private IP addresses and.local/.internalnames 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.comor.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: setntfyServer(publichttps://address) andntfyToken. - Pushover: copy your user key from pushover.net into
pushoverUserKey, create an application at pushover.net/apps/build and put its API token inpushoverAppToken. 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 publichttps://address.
The recipe: a Otodom new-listing alert in five steps
- Set your search on otodom.pl and paste its URL into
startUrls. - Turn on
onlyNewSinceLastRun, so only listings not delivered before are sent. - Add your destination(s) from the table above.
- Run it once. The first run is the baseline and sends what is currently listed (up to
maxNotifications). - Create a Schedule (for example every few hours). From then on you only hear about listings it has not delivered before for that search.
{"startUrls": ["https://www.otodom.pl/pl/wyniki/sprzedaz/mieszkanie/mazowieckie/warszawa/warszawa/warszawa"],"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:
{"source": "apify","actor": "otodom-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, price per m², floor area, rooms, floor, location, property type, the agency (not private owners) and the listed date, plus the link.
- Only delivered listings are sent. A run that finds nothing new sends nothing, so there is no "no new listings" 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 listings 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 listings 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 Otodom, and the messages go only to the destinations you set up.
Input
| Field | Type | Default | Description |
|---|---|---|---|
startUrls | string[] | (prefilled) | Public Otodom search-result URLs, e.g. https://www.otodom.pl/pl/wyniki/sprzedaz/mieszkanie/mazowieckie/warszawa/warszawa/warszawa. Must be https://www.otodom.pl/pl/wyniki/... 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 | otodom-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 listings 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, pricePerM2 |
| Property | areaM2, rooms, floor, propertyType, transaction |
| Location | location |
| Advertiser | agencyName |
| Timing | date |
| Search context | searchUrl, page, scrapedAt |
Missing values are returned as null. Nothing is guessed or invented.
Example output
{"id": "68303900","url": "https://www.otodom.pl/pl/oferta/nowe-mieszkanie-31-66-m2-warszawa-rembertow-ID4CAXO","title": "Nowe > Mieszkanie 31.66 m2 > Warszawa > Rembertów","price": 468000,"currency": "PLN","pricePerM2": 14625,"areaM2": 32,"rooms": 2,"floor": "SECOND","location": "Kawęczyn-Wygoda, Rembertów, Warszawa, mazowieckie","propertyType": "FLAT","transaction": "sale","agencyName": "Develop Invest","date": "2026-09-27 11:29:13","searchUrl": "https://www.otodom.pl/pl/wyniki/sprzedaz/mieszkanie/mazowieckie/warszawa/warszawa/warszawa","page": 1,"scrapedAt": "2026-10-03T00:00:00.000Z"}
transaction is sale or rent. agencyName is null when the ad comes from a private owner. 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
from apify_client import ApifyClientclient = ApifyClient("YOUR_APIFY_TOKEN")run = client.actor("lukehunter/otodom-scraper").call(run_input={"startUrls": ["https://www.otodom.pl/pl/wyniki/sprzedaz/mieszkanie/mazowieckie/warszawa/warszawa/warszawa"],"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 listings, never for messages sent.
Limitations
- Only
https://www.otodom.pl/pl/wyniki/...search-result URLs are accepted. Individual ad pages, the map view (?map=1) and the site's/api/queryendpoint are not used: Otodom'srobots.txtdisallows the map view and the query API, and this Actor does not fetch them. - No seller personal data, by design. Only listing fields are collected.
agencyNameis filled only for business agencies and isnullfor private owners; phone numbers, contact forms and seller profile pages are never fetched. - 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 Otodom. 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):
curl -s -X POST "https://api.apify.com/v2/acts/lukehunter~otodom-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H 'Content-Type: application/json' \-d '{"startUrls": ["https://www.otodom.pl/pl/wyniki/sprzedaz/mieszkanie/mazowieckie/warszawa/warszawa/warszawa"], "maxPagesPerSearch": 2, "maxItems": 20}'
JavaScript (npm i apify-client):
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('lukehunter~otodom-scraper').call({"startUrls": ["https://www.otodom.pl/pl/wyniki/sprzedaz/mieszkanie/mazowieckie/warszawa/warszawa/warszawa"],"maxPagesPerSearch": 2,"maxItems": 20});const { items } = await client.dataset(run.defaultDatasetId).listItems();
Python (pip install apify-client):
import osfrom apify_client import ApifyClientclient = ApifyClient(os.environ['APIFY_TOKEN'])run = client.actor('lukehunter~otodom-scraper').call(run_input={"startUrls": ["https://www.otodom.pl/pl/wyniki/sprzedaz/mieszkanie/mazowieckie/warszawa/warszawa/warszawa"], "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 phone numbers or private-owner details?
No. Only listing fields are returned. The business agency name is included where the ad has one; private owners are left as null.
Can I scrape Otodom ad detail pages?
No. This Actor reads search-result pages only, so it returns the fields shown on the results list, not the full ad description or gallery.
Can I get Otodom 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 listings 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 listings themselves. For instant alerts without building anything, Telegram, ntfy or Pushover reach your phone directly.
Can I get Otodom 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.