# Otodom.pl Scraper — Polish Property Listings, Photos, Dates (`skubadev/otodom-scraper`) Actor

Scrape Otodom.pl property listings for sale and rent across Poland — Warsaw, Krakow, Wroclaw, Gdansk and every other city. 30 fields per listing: price, price per m2, area, rooms, geo, agency, photo URLs, publication date and days on market.

- **URL**: https://apify.com/skubadev/otodom-scraper.md
- **Developed by:** [Anatoly Skuba](https://apify.com/skubadev) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 listings

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?

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

## Otodom.pl Scraper — Polish Property Listings, Photos, Dates

Extract property listings from **Otodom.pl**, Poland's largest real estate portal —
for sale and for rent, apartments, houses and plots. No login, no cookies, no browser.

### Empty results, and why you will not get them here

A scraper built on CSS selectors fails quietly. When Otodom changes its markup,
or answers an automated request with a block page, you do not get an error — you
get an empty table, or a column of empty values, and you find out later.

This Actor is built the other way round:

- it reads the data **Otodom already renders into the page**, so a redesign of
  the visual layout does not change what it returns;
- it goes through **residential proxies** with polite delays and retries every page;
- at the end of every run it prints **how many pages failed and how many rows it
  skipped**, and warns you when a promised field came back mostly empty. You never
  have to guess whether the file is complete;
- you pay **per listing returned**. A run that finds nothing costs you the Actor
  start fee of $0.00005 and nothing else.

### Quick Start

1. Open [otodom.pl](https://www.otodom.pl), set your filters
2. Copy the address of the **results page** (not a single listing)
3. Paste it into `searchUrls` and press **Start**

The default input works out of the box: apartments for sale in Warsaw.

```json
{
  "searchUrls": ["https://www.otodom.pl/pl/wyniki/sprzedaz/mieszkanie/mazowieckie/warszawa/warszawa/warszawa"],
  "maxItems": 1000
}
```

### Why this one does not break

Most scrapers read the page with CSS selectors. When the site changes its markup,
they silently return nothing.

This Actor reads the **data the server already renders into the page** as structured
JSON. There are no CSS selectors anywhere in the code. The listing array is located
**by its shape** — the set of fields every record must have — rather than by a fixed
path, so a framework upgrade that moves the data does not break extraction.

- **No browser.** Plain HTTP requests, so runs are fast and compute is cheap
- **36 listings per page**, so large result sets finish quickly
- **Typed fields.** Numbers arrive as numbers, room counts as integers

A failed page is retried three times with backoff and never aborts the run.
A malformed listing is skipped, not the page around it. Every run writes a
`RUN_STATS` record with the real failure rate — you can audit it yourself.

### Output

| Field | Notes |
|---|---|
| `id`, `url` | Stable listing id and direct link |
| `type` | `flat`, `house`, `terrain`, … |
| `title` | Listing headline |
| `price`, `currency`, `marketingType` | `BUY` / `RENT`, PLN |
| **`pricePerSqm`** | Taken from the source, or calculated when missing |
| `priceHidden` | Some listings hide the price |
| `livingSpace`, `terrainArea` | m² |
| `rooms` | Integer — the source returns words like `THREE` |
| `street`, `houseNumber`, `city`, `province` | |
| `realtorCompany`, `realtorId` | Agency — see below |
| `isPrivateOwner` | Whether the seller is a private person |
| `isExclusiveOffer`, `isPromoted` | Listing flags |
| `developmentTitle` | For new-build projects |
| `imagesCount`, `source` | |
| `country` | Always `PL` |
| **`publishedAt`** | Real publication date and time from the portal |
| **`daysOnMarket`** | Days since publication, calculated for you |
| **`imageUrls`** | Full-size photo URLs, ready to download |
| `sourceUrl`, `scrapedAt` | |

### The three things that go wrong with property scrapers

These are the complaints you will find under other Otodom scrapers in this Store.

**"It stopped working."** This Actor reads the structured data the server already
puts inside the page, not CSS selectors, so a visual redesign does not break
extraction. A health check runs every morning, so a real break is found before a
customer runs into it.

**"Missing the posted date."** Every listing carries `publishedAt` with the portal's
own publication timestamp, plus `daysOnMarket` calculated from it. No extra requests,
no extra cost.

**"Unexpected billing."** You are charged per listing written to the dataset, not per
request. `maxItems` is a hard stop. Otodom mixes developer projects and hidden-price
listings into results — measured on 1,000 Warsaw listings: 6.9% developer projects,
9.1% with no price. Turn on `skipWithoutPrice` and those are dropped **before** billing.

### No personal data

This Actor returns the **agency name** — a business — and a boolean flag for whether
a listing comes from a private seller. It does **not** return any private seller's
name, phone number or email.

If you need personal contact data, this is not the right Actor, and under GDPR you
would need your own lawful basis for processing it.

### Limits — what this Actor does not do

- Search result pages only. Full listing detail pages are not fetched
- Poland (`otodom.pl`) only
- No listing photos, only the image count
- **No publication date.** Unlike some portals, Otodom does not expose it in search
  results, so `daysOnMarket` is empty here

### Pricing

**$2.00 per 1,000 listings** — $0.002 per listing — plus $0.00005 per Actor start.
Platform usage (compute and proxy) is included in that price and is not billed
to you separately.

A run that returns nothing costs nothing beyond the Actor start.

#### Cost control

Set **Max listings** before you press Start and the run's cost is fixed in advance:
1,000 listings is $2.00, 250 listings is $0.50. The Actor stops exactly at that
number, on every plan, and writes a `RUN_STATS` record with what it actually did.
Nothing here depends on your plan tier — the behaviour on the Free plan is the
same as on any other, just bounded by the credit the plan gives you.

### Development

```bash
npm test    # regenerates the fixture and runs offline parser tests, no network
```

# Actor input Schema

## `searchUrls` (type: `array`):

Open otodom.pl, set your filters, then copy the address of the RESULTS page. It must contain /wyniki/ — a single offer page (/oferta/...) will not work.

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

Stop after this many listings across all URLs.

## `maxPagesPerUrl` (type: `integer`):

0 means no limit — follow pagination to the end.

## `politeDelayMs` (type: `integer`):

Keeps request rate polite. Lower it only if you know what you are doing.

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

Polish residential proxy is recommended.

## `probeUrls` (type: `array`):

Paste any URL here and the Actor only checks whether it is reachable and whether the page carries the structured data this Actor reads — then stops without scraping and without charging per listing. The full result is written to the key-value store under PROBE\_RESULTS. Leave empty for normal runs.

## `skipWithoutPrice` (type: `boolean`):

Some entries on the portal are developer projects or listings with the price hidden. They arrive with an empty price, area and room count. Turn this on to drop them — they are filtered out before the dataset is written, so you are not charged for them. Measured on a 1,000-listing Warsaw run: 6.9% developer projects, 9.1% without a price.

## Actor input object example

```json
{
  "searchUrls": [
    "https://www.otodom.pl/pl/wyniki/sprzedaz/mieszkanie/mazowieckie/warszawa/warszawa/warszawa"
  ],
  "maxItems": 1000,
  "maxPagesPerUrl": 0,
  "politeDelayMs": 700,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "PL"
  },
  "probeUrls": [],
  "skipWithoutPrice": false
}
```

# Actor output Schema

## `listings` (type: `string`):

All listings returned by this run, one object per listing.

## `listingsCsv` (type: `string`):

The same listings as a CSV file.

# 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 = {
    "searchUrls": [
        "https://www.otodom.pl/pl/wyniki/sprzedaz/mieszkanie/mazowieckie/warszawa/warszawa/warszawa"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("skubadev/otodom-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 = { "searchUrls": ["https://www.otodom.pl/pl/wyniki/sprzedaz/mieszkanie/mazowieckie/warszawa/warszawa/warszawa"] }

# Run the Actor and wait for it to finish
run = client.actor("skubadev/otodom-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 '{
  "searchUrls": [
    "https://www.otodom.pl/pl/wyniki/sprzedaz/mieszkanie/mazowieckie/warszawa/warszawa/warszawa"
  ]
}' |
apify call skubadev/otodom-scraper --silent --output-dataset

```

## MCP server setup

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