# Otodom + Imovirtual + Storia API — Poland's, Portugal's and Romania's real-estate portals (OLX Group): property search with the site's own filters, full listing detail with prices, fees, seller and new-development units, and location lookup

> Search sale or rental listings on otodom.pl (site=pl), imovirtual.com (pt) or storia.ro (ro): property type, location (an id from `locations`), price, price per m², area, rooms, floor, build year, advertiser type (private / agency / developer), market (primary / secondary), listing age, keyword in the description, extras, building type, sort and page. Each row: listing id, title, URL, total price, price per m², monthly administrative fee, area, rooms, floor, location down to the neighbourhood, advertiser type and agency, dates, promoted flag, photos. Plus the source's own total and page count. Every filter is checked against the source's own echo: a value the site did not apply is refused, never silently ignored.
> ReefAPI engine `otodom` · 3 endpoints · clean JSON, no scraping or browsers to manage.

## How to call
- **Endpoint:** `POST https://api.reefapi.com/otodom/v1/<action>` with a JSON body.
- **Auth:** header `x-api-key: <YOUR_REEFAPI_KEY>` — create one free (1,000 credits, no card): https://reefapi.com/signup
- **Response (every call):** `{ ok: boolean, data: ..., meta: { record_count, credits, ... }, error: { code, message } }` — branch on `ok`. Failed calls are free except verified SHEIN NOT_FOUND on product/detail and price (4 credits).
- **One key + one shared credit pool** across every ReefAPI API. Per-call credits are listed on each endpoint below.
- **Use it from an AI agent (MCP):** connect `https://api.reefapi.com/mcp` (remote streamable-http). Send the key as `Authorization: Bearer <key>`, or put it in the URL (`?key=<key>`) when the client has no header field, as ChatGPT does.

## Endpoints

### POST https://api.reefapi.com/otodom/v1/search — 3 credits
Search sale or rental listings on otodom.pl (site=pl), imovirtual.com (pt) or storia.ro (ro): property type, location (an id from `locations`), price, price per m², area, rooms, floor, build year, advertiser type (private / agency / developer), market (primary / secondary), listing age, keyword in the description, extras, building type, sort and page. Each row: listing id, title, URL, total price, price per m², monthly administrative fee, area, rooms, floor, location down to the neighbourhood, advertiser type and agency, dates, promoted flag, photos. Plus the source's own total and page count. Every filter is checked against the source's own echo: a value the site did not apply is refused, never silently ignored.

**Parameters:**
- `site` (enum, optional, default "pl") — Which portal: `pl` = otodom.pl (Poland, prices in PLN), `pt` = imovirtual.com (Portugal, EUR), `ro` = storia.ro (Romania, mostly EUR). Same platform, same fields. [one of: pl, pt, ro]
- `transaction` (enum, optional, default "sell") — Sale or rent. For `rent`, `price` is the monthly rent. [one of: sell, rent]
- `estate` (enum, optional, default "flat") — Property type. `investment` lists new-development projects (inwestycja / empreendimento / ansamblu); flat results also contain development rows (`kind`=investment). [one of: flat, house, plot, commercial, hall, garage, room, investment]
- `location` (string, optional) — Location id as the site writes it in its URLs, taken from the `locations` action (`location_id`), e.g. `mazowieckie/warszawa/warszawa/warszawa` (Warsaw), `malopolskie/krakow/krakow/krakow`, `lisboa` (pt), `cluj/cluj--napoca` (ro). Omit for the whole country. An unknown id is rejected (INVALID_PARAM).
- `min_price` (number, optional) — Minimum price (total price; monthly rent for `rent`), in the site's currency.
- `max_price` (number, optional) — Maximum price (total; monthly rent for `rent`).
- `min_price_per_m2` (number, optional) — Minimum price per m².
- `max_price_per_m2` (number, optional) — Maximum price per m².
- `min_area` (number, optional) — Minimum area in m².
- `max_area` (number, optional) — Maximum area in m².
- `rooms` (array, optional) — Number of rooms, one or more (`2` or `2,3`); `6+` = six or more (the site's own top class). Several values are combined with OR in ONE request. [one of: 1, 2, 3, 4, 5, 6+]
- `floors` (array, optional) — Floor(s): `cellar`, `ground`, `1`…`10`, `above_10`, `garret`. Several values combine with OR. [one of: cellar, ground, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, above_10, garret]
- `min_build_year` (integer, optional) — Built in or after this year.
- `max_build_year` (integer, optional) — Built in or before this year.
- `owner_type` (enum, optional) — Who placed the ad (the site's own advertiser filter). [one of: private, agency, developer]
- `market` (enum, optional) — Primary market (new build from a developer) or secondary. [one of: primary, secondary]
- `days_since_created` (enum, optional) — Only listings added in the last N days. [one of: 1, 3, 7, 14, 30]
- `description` (string, optional) — A word that must appear in the listing description (the site's own keyword filter), e.g. `taras`, `garaż`.
- `extras` (array, optional) — Required extras (AND): balcony, terrace, garage, lift, basement, garden, separate_kitchen. [one of: balcony, terrace, garage, lift, basement, garden, separate_kitchen]
- `building_type` (array, optional) — Building type(s): block, tenement, house, apartment, infill, ribbon, loft. [one of: block, tenement, house, apartment, infill, ribbon, loft]
- `sort` (enum, optional, default "relevance") — Result order (the site's own options). [one of: relevance, newest, price_asc, price_desc, area_asc, area_desc]
- `page` (integer, optional, default 1) — Result page. Past the last page the source would re-serve the last page; the engine returns 0 rows with `page_clamped: true` and `last_page` instead.
- `limit` (enum, optional, default "36") — Rows per page requested from the site: 24, 36, 48 or 72 (the site's own options; the actual count varies). [one of: 24, 36, 48, 72]

**Returns:** results[]{listing_id (ID… token — pass to `listing`), ad_id, kind (ad|investment), title, url, estate, transaction, price, currency, price_type (sale_price|monthly_rent|from_price_per_m2), price_on_request, price_hidden, price_per_m2, maintenance_fee_monthly (czynsz), investment_price_from_per_m2, area_m2, plot_area_m2, rooms, floor, floor_number, location{region, city, district, neighbourhood, street, location_id, full_name, levels[]}, advertiser_type (private_person|agency|developer|business), is_private_owner, advertiser_name, agency{agency_id, name, type, logo}, created_at, first_created_at, pushed_up_at, is_promoted, is_exclusive, is_crosslisted, tags[], development{}, investment{state, estimated_delivery, units, …}, image, images[], image_count} + total, pages, page, per_page, last_page, has_more, page_clamped, decoy_rows_dropped, location{location_id, name, level, children[]}

**Example request body:**
```json
{
  "site": "pl",
  "transaction": "sell",
  "estate": "flat",
  "location": "mazowieckie/warszawa/warszawa/warszawa"
}
```

### POST https://api.reefapi.com/otodom/v1/listing — 2 credits
Full listing by `listing_id` (the ID… token from a search row) or URL: price, the price as the site prints it, monthly administrative fee (czynsz), deposit, price per m², area, rooms, floor, building floors, build year, building type / material, ownership, heating, construction status, extras, all characteristics, full description, photos, floor plans, location with coordinates, dates, and the advertiser (type, contact name, phone numbers as published, agency name / page / logo, owner address). For a new-development project: price-from per m², area range, number of units and the first page of its units with price and area.

**Parameters:**
- `listing_id` (string, optional) — The ID… token at the end of a listing URL (`…-ID4Diz3`), returned as `listing_id` in every search row. The numeric `ad_id` does not address a page on the site. Provide listing_id OR url.
- `url` (string, optional) — An otodom.pl, imovirtual.com or storia.ro offer / development URL (the site is taken from the URL).
- `site` (enum, optional, default "pl") — Which portal: `pl` = otodom.pl (Poland, prices in PLN), `pt` = imovirtual.com (Portugal, EUR), `ro` = storia.ro (Romania, mostly EUR). Same platform, same fields. [one of: pl, pt, ro]

**Returns:** listing{listing_id, ad_id, site, kind, title, url, status, estate, transaction, market, price, currency, price_type, price_display, price_printed, price_mismatch, price_on_request, price_negotiable, price_per_m2, maintenance_fee_monthly, deposit, investment_price_from_per_m2, area_m2, plot_area_m2, rooms, floor, building_floors, build_year, building_type, building_material, building_ownership, construction_status, heating, windows_type, available_from, extras[], attributes{}, characteristics[]{key, value, display, currency}, features[], description, location{…, latitude, longitude}, images[], floor_plans[], links{}, created_at, modified_at, pushed_up_at, reference_id, is_exclusive, advertiser_type, seller{type, contact_name, phones[], owner_id, owner_name, owner_address, agency{agency_id, name, type, url, logo}}, investment{state, number_of_properties, area_from_m2, area_to_m2, total, returned, units[]{ad_id, listing_id, url, price, area_m2, rooms, floor, price_per_m2}, groups[]}, development{}}

**Example request body:**
```json
{
  "url": "https://www.otodom.pl/pl/oferta/bezposrednio-29-m-widok-na-las-petla-i-pkp-goclawek-ID4Diz3"
}
```

### POST https://api.reefapi.com/otodom/v1/locations — 1 credit
Location lookup: type a place name (`Kraków`, `Mokotów`, `Lisboa`, `Cluj`) and get the site's own location ids with level (voivodeship / county / city / district / village…) and full name. Feed `location_id` to `search`. Diacritics are optional.

**Parameters:**
- `query` (string, required) — Place name or its beginning (2+ characters). The site matches loosely: a string that is no place still returns its closest spellings, so check `full_name`.
- `site` (enum, optional, default "pl") — Which portal: `pl` = otodom.pl (Poland, prices in PLN), `pt` = imovirtual.com (Portugal, EUR), `ro` = storia.ro (Romania, mostly EUR). Same platform, same fields. [one of: pl, pt, ro]

**Returns:** locations[]{location_id, name, full_name, level, parent_ids[]}

**Example request body:**
```json
{
  "site": "pl",
  "query": "Kraków"
}
```

## At scale
- **Volume:** 5M+ requests a day, measured at 60 requests a second across the fleet with no
  central bottleneck. Per-key limits are raised for high-volume accounts; volume pricing on request.
- **Missing a source:** tell us a site we do not cover and it becomes an engine. A customer asked
  for bestprice.gr on 21 Sep 2026 and it was in the catalog on 22 Sep.
- **Support:** 2 minute median time from a question in the live chat to the first answer. Setup
  help included, no support tier to buy.
- **One key, one credit pool** across every API. No per-site plans, no separate subscriptions.

## More
- Try it live, no code: https://reefapi.com/playground?engine=otodom
- Human docs page: https://reefapi.com/docs/otodom
- Overview page: https://reefapi.com/otodom-api
- Every ReefAPI API in one file (for your AI): https://reefapi.com/llms-full.txt
