# Otodom Property Price Scraper (`madjad33/otodom-property-scraper`) Actor

Extract Otodom property listings as structured JSON: price, price per m2, floor area, rooms, floor and location. No personal data, ever.

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

## Pricing

from $3.00 / 1,000 property 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/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

## Otodom Property Price Scraper

Extract **Otodom** property listings as clean structured JSON or CSV: asking price, **price per m²**, floor area, room count, floor, and location. Any city, any property type, sale or rental. No account, no browser, no personal data.

Press **Start** and it returns 100 Warsaw flats immediately — every input field has a working default.

### What you get

One flat row per listing. No nested objects to unpick.

```json
{
  "listingId": "68305308",
  "title": "Duży taras | Klucze w 6msc | Przestronny metraż",
  "url": "https://www.otodom.pl/pl/oferta/duzy-taras-klucze-w-6msc-ID4CBkv",
  "transaction": "SELL",
  "estateType": "FLAT",
  "priceValue": 3367715,
  "priceCurrency": "PLN",
  "pricePerSqmValue": 26300,
  "areaSqm": 128.05,
  "rooms": 5,
  "floor": 9,
  "city": "Warszawa",
  "district": "Mokotów",
  "neighbourhood": "Służewiec",
  "street": "Wołoska",
  "agencyName": "New Home",
  "advertiserKind": "AGENCY",
  "scrapedAt": "2026-08-12T09:41:02.187Z"
}
```

| Field | Notes |
|---|---|
| `listingId` | Stable Otodom id — use it as your join key across runs |
| `priceValue` / `priceCurrency` | Asking price as a number. `null` when the seller hides it |
| `pricePerSqmValue` | Price per m², computed by Otodom — the field most analysts actually want |
| `areaSqm`, `terrainAreaSqm` | Floor area and plot area |
| `rooms`, `floor` | Converted from Otodom's word enums to integers (`FIVE` → `5`, `GROUND` → `0`) |
| `region`, `subregion`, `city`, `district`, `neighbourhood`, `street` | Address, most general to most specific. `district` and `neighbourhood` come from Otodom's own geocoding, so they are consistent across listings rather than typed by an agent |
| `agencyName`, `advertiserKind` | Agency trading name, and `AGENCY` / `PRIVATE` / `UNKNOWN` |
| `isExclusiveOffer`, `isPromoted` | Listing flags |
| `imageUrl`, `imageCount` | Primary photo and how many exist |
| `dateCreated` | First published — lets you measure time on market |

### Any search works

Point it at any Otodom search URL and change what you like:

| Want | URL |
|---|---|
| Warsaw flats for sale *(default)* | `/pl/oferty/sprzedaz/mieszkanie/warszawa` |
| Kraków rentals | `/pl/oferty/wynajem/mieszkanie/krakow` |
| Houses in Gdańsk | `/pl/oferty/sprzedaz/dom/gdansk` |

`maxItems` is a hard cap on billable results, so it is also your spend cap. A single city search runs to 500+ pages, so set it deliberately.

### Compliance, stated plainly

**This actor never returns personal data.** Not "stripped afterwards" — never collected.

That matters more here than on most sources. Every raw Otodom record contains an `organisationAssignedMember` object holding an agent's **first name, last name and mobile number**, sitting one key away from the price. The output schema is a fixed allowlist of 31 fields, and rows are *constructed* from that list rather than filtered against a blocklist — so those fields cannot reach your dataset even if Otodom renames or adds them. There are tests asserting exactly that against real captured records, plus a runtime guard that refuses to push a row carrying a forbidden field.

The agency **trading name** is returned, because a business is not a person.

**Private-seller listings are excluded by default.** Roughly 13% of listings are posted by private individuals rather than agencies. Even with every name and phone number removed, a precise address plus an asking price relates to an identifiable person. You can include them with `includePrivateSellers`, and when you do, the street and coordinates are removed while the price, area and district remain.

**robots.txt is respected in code**, not just in spirit: `/api/`, `/konto`, `/login` and registration paths are refused even if you pass them as input. **No login, ever** — everything is read from pages served to an anonymous visitor.

### Notes

Data is read from the `__NEXT_DATA__` payload Otodom server-renders for search engines, not from CSS selectors. That makes this unusually stable — structured data is a contract the site maintains deliberately, so it changes far less often than markup. It is also why no browser is needed, which is why it is cheap.

Duplicate listings are removed within a run: Otodom repeats promoted listings across pages, and you should not pay twice for the same row.

Listings with a hidden price are **skipped without charge**. You pay for results, not attempts.

**What this does not return:** GPS coordinates. Otodom's search payload carries a map radius but no latitude or longitude, so rather than ship an always-empty column the field is simply absent. `district` and `neighbourhood` are the finest geography available.

# Actor input Schema

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

Any otodom.pl search page. Change city, property type or transaction in the URL — e.g. /pl/oferty/wynajem/mieszkanie/krakow for Krakow rentals, or /pl/oferty/sprzedaz/dom/gdansk for houses in Gdansk. Account, login and /api/ paths are refused.

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

Hard cap on billable results. You are charged per listing returned, so this is also your spend cap.

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

Safety limit on pagination depth. A single city search can run to 500+ pages.

## `includePrivateSellers` (type: `boolean`):

Off by default. Listings by private individuals are excluded because a precise address plus an asking price relates to an identifiable person. Turning this on includes them with the street and coordinates removed; agency listings are unaffected either way. Names, phone numbers and agent contact details are never returned in any mode.

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

Parallel requests. The default is deliberately polite — this needs no browser, so it is already fast.

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

Optional. Otodom serves this data to plain HTTP clients, so no proxy is needed in normal use and leaving it off is cheaper.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.otodom.pl/pl/oferty/sprzedaz/mieszkanie/warszawa"
    }
  ],
  "maxItems": 100,
  "maxPagesPerSearch": 20,
  "includePrivateSellers": false,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `overview` (type: `string`):

Table of listings with price, price per m2, area, rooms, city and district.

## `allFields` (type: `string`):

Every field, including rent, plot area, neighbourhood, street, advertiser type and first-published date.

## `csv` (type: `string`):

The same results as a spreadsheet.

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

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

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

```

## MCP server setup

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