# Realitica Property Scraper & Listing Monitor (`luminar/realitica-balkan-property-radar-private-v1`) Actor

Scrape Realitica listings across the Balkans by location, filters, or URL. Export prices, €/m², rooms, images, and optional seller contacts. Monitor new listings, updates, price changes, and safely confirmed removals with coverage, summaries, duplicate clusters, and comparables.

- **URL**: https://apify.com/luminar/realitica-balkan-property-radar-private-v1.md
- **Developed by:** [Luka](https://apify.com/luminar) (community)
- **Categories:** Real estate
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.56 / 1,000 useful basic property listing or changes

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

Use this **Realitica property scraper** to collect listings across Balkan markets as structured rows, then reuse the same input as a **Balkan property monitor** for new listings, updates, price movements, and safely proven removals. The Actor also produces free coverage evidence, market summaries, duplicate-agency clusters, and explainable comparable-property rows.

Start with familiar location fields or paste exact Realitica URLs. Basic runs stay lightweight; richer detail pages are opened only when you request them or activate a detail-dependent filter.

![Realitica Property Scraper and Listing Monitor product preview](https://api.apify.com/v2/key-value-stores/DJgPR6wwS2tIaLDQW/records/realitica_balkan_property_radar--5073e6cf99d0dfef-realitica-property-data.png)

### 🚀 Start in 60 seconds

1. Open **Input** and choose a country, area, listing mode, and property type.
2. Keep **Output mode** on **Current snapshot** for the first exploratory run.
3. Set **Maximum listing rows** to a small number such as `50`.
4. Click **Start**, then open the **Property listings** dataset view.

```json
{
  "countries": ["Crna Gora"],
  "areas": ["Tivat"],
  "listingModes": ["sale"],
  "propertyTypes": ["Apartment"],
  "maxListings": 50,
  "stateMode": "current"
}
```

If you already have an exact Realitica results page or listing page, use `startUrls` instead:

```json
{
  "startUrls": [
    { "url": "https://www.realitica.com/hr/listing/3069833" }
  ],
  "maxListings": 50
}
```

### 🎯 Choose the right workflow

| What you need | Recommended input | What happens |
|---|---|---|
| Browse a market | Country, area, mode, and property type | The Actor builds bounded Realitica searches |
| Reuse an exact source scope | `startUrls` | Up to three supplied URLs replace the generated searches |
| Export all current listings | `stateMode: "current"` | Every useful listing row is returned |
| Monitor only useful changes | `stateMode: "changes"` | First run seeds; later comparable runs emit changes |
| Bedrooms, parking, sea distance, seller, or phone | Detail controls | Detail pages open automatically and successful enriched rows use the detail price |
| Compare one property with the fetched market | `referenceListing` | Free comparable rows explain each similarity score |

When `startUrls` contains at least one URL, it takes precedence over Countries, Areas, Listing modes, Property types, source price, posting-window, and sort fields. Local row filters still apply after the supplied pages are fetched.

Generated searches are limited to three country × listing-mode targets per run. For example, one country with three modes is valid; two countries with two modes would create four targets and is rejected before source work.

### 📦 What you get

The default dataset intentionally contains several useful record types. Use `recordType` to select the right rows:

| `recordType` | Purpose | Recommended view |
|---|---|---|
| `listing` | Structured current listings or change events | **Property listings** |
| `market_summary` | Count, price, and price-per-m² statistics | **Analysis** |
| `duplicate_cluster` | Likely cross-agency duplicates with matching reasons | **Analysis** |
| `comparable` | Reference-property matches with explainable scores | **Analysis** |
| `coverage` | Target results, limits, warnings, field coverage, and billing summary | **Coverage** |

Listing rows can include location, property type, mode, price, currency, surface, price per m², rooms, seller, source dates, images, state fields, and the canonical Realitica URL. Detail rows may additionally contain bedrooms, bathrooms, parking, distance from sea, description, phone, and a list of fields actually delivered.

Core export fields are `listingId`, `canonicalUrl`, `title`, `country`, `city`, `propertyType`, `listingMode`, `price`, `currency`, `surfaceM2`, `postedDate`, `postedDatePrecision`, `updatedDate`, `updatedDatePrecision`, `firstSeenAt`, `lastSeenAt`, `changeStatus`, `coverageState`, `coverageStatus`, and `collectedAt`. Source-dependent fields remain `null` when the fetched page does not expose them.

```json
{
  "recordType": "listing",
  "listingId": "example-id",
  "title": "Apartment in Tivat",
  "listingMode": "sale",
  "price": 180000,
  "currency": "EUR",
  "surfaceM2": 60,
  "pricePerM2": 3000,
  "changeStatus": "CURRENT",
  "coverageState": "COMPLETE"
}
```

For clean exports, filter `recordType == "listing"`; use the dedicated Analysis and Coverage views for the other rows. The `OUTPUT` key-value-store record provides a buyer-facing delivery summary, coverage, state counts, resource totals, and charge totals.

### 🎛️ Input guide

**Location search.** Countries, Areas, Listing modes, and Property types build the source queries. Source price, date, and sort controls narrow generated searches. Labels should match Realitica's current catalog.

**Exact URLs.** `startUrls` is an alternative source selector, not an additional filter. Once supplied, it replaces the generated location searches. Result limits and local filters still apply.

**Local filters.** Rooms, surface, price per m², currency, keywords, and sublocations filter normalized rows. A completed source request can therefore produce `FILTERED` coverage when valid source rows do not pass the selected local rules.

**Paid detail activation.** `includeDetails`, `includeContact`, `parking: true`, bedroom bounds, maximum sea distance, or a seller query opens detail pages automatically. A successfully enriched row receives one `detailed_listing_result` event instead of a basic event. Failed or unavailable detail can fall back to a basic row when delivery capacity remains.

**Comparables.** `referenceListing` scores only the listings already fetched in the run. It does not launch a broader premium search, and comparable evidence rows are free.

**Limits.** `maxListings` is the global listing-row ceiling. `maxPagesPerSearch` limits each source search. Reaching either while more data may remain produces `CAPPED` coverage rather than a false complete result.

#### Repeat runs & monitoring

Choose **Changes since the previous comparable run**, then repeat the same row-affecting input sequentially.

- The first Changes run emits observed listings as `SEEDED` and creates the baseline.
- Later comparable runs emit `NEW` listings and meaningful `UPDATED` rows, including price-change fields.
- Enable `includeUnchanged` only for audit exports; unchanged rows are free.
- A missing listing becomes `ENDED` with `availabilityStatus: "REMOVED"` only after two comparable complete absences.
- Capped, partial, blocked, failed, or filtered snapshots do not prove removal.

The Actor derives comparison scope from the source, filters, and detail options. Changing one of those settings creates a separate scope. Avoid overlapping runs for the same monitor.

### 💰 Pricing

This Actor uses pay per event. The configured schedule charges one start only after a usable verified source result or positively confirmed exhausted-empty result, plus one mutually exclusive event for each useful listing row.

```text
run charge = $0.005 verified actor start
           + delivered basic rows × your plan's listing_result price
           + delivered detailed rows × your plan's detailed_listing_result price
```

| Event | Free tier | Bronze | Silver | Gold |
|---|---:|---:|---:|---:|
| Validated Actor start (`actor_start`) | `$0.005` | `$0.005` | `$0.005` | `$0.005` |
| Basic listing or useful change (`listing_result`) | `$0.00080` | `$0.00072` | `$0.00064` | `$0.00056` |
| Detailed listing or useful change (`detailed_listing_result`) | `$0.00160` | `$0.00144` | `$0.00128` | `$0.00112` |

Free-tier examples:

- 25 basic Current rows: `$0.005 + 25 × $0.00080 = $0.025`
- 25 successfully detailed Current rows: `$0.005 + 25 × $0.00160 = $0.045`
- A successful verified Changes run with no paid change rows: `$0.005`

The detailed event replaces the basic event for that row; it is not added on top. Blocked, failed, and unverified source attempts do not charge the start. Unchanged listings, coverage, summaries, clusters, comparables, and warnings are free. If requested detail is not successfully delivered, an otherwise useful row can fall back to the basic event.

### ✅ Coverage you can trust

Coverage evidence distinguishes a real source result from a stopped, filtered, or unverified run:

- `COMPLETE` — the configured source window completed without a known cap.
- `EMPTY_CONFIRMED` — the source window completed and independently supports zero matching rows.
- `CAPPED` — a page, row, request, runtime, or billing ceiling stopped broader work.
- `PARTIAL` — useful data was delivered, but part of the requested work failed or remained unresolved.
- `FILTERED` — source rows were found, but local filters removed them from listing delivery.
- `BLOCKED` — the source denied reliable access for that target.
- `FAILED` — no trustworthy result or verified empty result was obtained.

Coverage rows remain in the mixed dataset even when no paid listing row is emitted. `targetResults`, warnings, raw and unique counts, duplicates removed, and field-coverage statistics explain what happened.

### 🔌 API and automation

Run with the Apify API by replacing `ACTOR_ID` and `APIFY_TOKEN`:

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/ACTOR_ID/runs?token=APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"countries":["Crna Gora"],"areas":["Tivat"],"listingModes":["sale"],"maxListings":50}'
```

Export dataset rows as JSON, CSV, Excel, XML, RSS, or JSONL through Apify dataset endpoints. For monitoring, first save the exact Changes input as an Apify Task, run it once to seed state, and only then add a non-overlapping schedule.

### ❓ FAQ and support

**Why were my country and area settings ignored?**\
Check `startUrls`. Any supplied Realitica URL takes precedence over the generated location search.

**Why did a basic-looking filter increase the row price?**\
Bedrooms, parking, sea distance, seller, and phone are detail-page fields. Requesting one automatically activates detail work. Successful enriched rows use the detailed price instead of the basic price.

**Why do some columns look empty?**\
The dataset contains listings, summaries, clusters, comparables, and coverage. Filter on `recordType`, or switch to the matching dataset view.

**Why did the first Changes run return SEEDED rows?**\
It created the baseline. Keep the same source, filters, and detail options, then run sequentially to receive later changes.

**Why was a missing listing not marked REMOVED?**\
Removal needs two comparable complete absences. A capped, partial, blocked, failed, or filtered run cannot safely prove disappearance.

For support, share the Apify run ID, the input with sensitive values removed, and the Coverage row or buyer-facing `OUTPUT` warning.

# Actor input Schema

## `countries` (type: `array`):

Enter Realitica country/state labels. Ignored when Start URLs are provided. Country × listing-mode combinations may create at most three source searches per run.

## `areas` (type: `array`):

Enter one or more Realitica area/city labels. Ignored when Start URLs are provided. Values are matched against the source-derived catalog.

## `listingModes` (type: `array`):

Choose the market to search. Each country × mode pair is one source target; the complete matrix may contain at most three targets. Ignored when Start URLs are provided.

## `propertyTypes` (type: `array`):

Use Realitica property-type labels such as Apartment or House. Leave empty for every type. Ignored when Start URLs are provided.

## `maxListings` (type: `integer`):

Global ceiling for unique listing rows. If the ceiling is reached while more results may remain, coverage is CAPPED instead of COMPLETE.

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

Start URLs take precedence over the location search. If at least one URL is supplied, Countries, Areas, Listing modes, Property types, source price, date, and sort controls are ignored. Local filters still apply to fetched rows.

## `priceMin` (type: `number`):

Adds Realitica's minimum-price filter to generated location searches. Ignored for Start URLs.

## `priceMax` (type: `number`):

Adds Realitica's maximum-price filter to generated location searches. Ignored for Start URLs.

## `subLocations` (type: `array`):

Narrow generated searches with source-backed neighborhood labels. Local matching may also refine the fetched rows.

## `currency` (type: `string`):

Optional currency label for local matching. The source currency remains authoritative in each output row.

## `publishedWindow` (type: `string`):

Applies Realitica's posting-window filter to generated searches. Ignored for Start URLs. Source dates remain day-precision unless a page proves more.

## `sortBy` (type: `string`):

Applies Realitica's supported sort order to generated searches. Ignored for Start URLs.

## `roomsMin` (type: `number`):

Locally keeps rows with at least this many total rooms when the source exposes a numeric value.

## `roomsMax` (type: `number`):

Locally keeps rows with at most this many total rooms when the source exposes a numeric value.

## `surfaceMin` (type: `number`):

Locally keeps rows at or above this source-backed surface.

## `surfaceMax` (type: `number`):

Locally keeps rows at or below this source-backed surface.

## `pricePerM2Min` (type: `number`):

Local filter calculated only when both numeric price and surface are available.

## `pricePerM2Max` (type: `number`):

Local filter calculated only when both numeric price and surface are available.

## `includeKeywords` (type: `array`):

Keeps a row when at least one normalized keyword appears in its title, location, excerpt, description, or seller fields.

## `excludeKeywords` (type: `array`):

Removes a row when any normalized keyword appears in its searchable text fields.

## `includeImages` (type: `boolean`):

Keeps image URLs already exposed by fetched listing pages. Images themselves are not downloaded.

## `stateMode` (type: `string`):

Current returns every listing. The first Changes run seeds the selected scope and marks observed listings SEEDED; later sequential runs emit useful NEW, UPDATED, or safely proven REMOVED changes.

## `firstSeenWindow` (type: `string`):

Used only in Changes mode. Filters by when this Actor first observed a listing; it is not the same as Realitica's posted date.

## `includeUnchanged` (type: `boolean`):

Used only in Changes mode. Enable for audit exports; UNCHANGED listing rows are included without a listing-result charge.

## `includeDetails` (type: `boolean`):

Opens each deduplicated listing page for richer fields. A successfully enriched row uses the detailed-listing price instead of, not in addition to, the basic-listing price.

## `bedroomsMin` (type: `number`):

Automatically opens detail pages, then locally keeps rows at or above this bedroom count.

## `bedroomsMax` (type: `number`):

Automatically opens detail pages, then locally keeps rows at or below this bedroom count.

## `parking` (type: `boolean`):

When enabled, automatically opens detail pages and keeps only rows whose fetched detail proves parking.

## `maxDistanceFromSeaMeters` (type: `number`):

Automatically opens detail pages, then applies this maximum distance when the source exposes a usable value.

## `sellerQuery` (type: `string`):

Automatically opens detail pages, then keeps rows whose seller or agency contains this text.

## `includeContact` (type: `boolean`):

Automatically opens detail pages and returns a public phone field when Realitica exposes one. Missing detail can fall back to a basic listing row.

## `referenceListing` (type: `string,object`):

Provide a listing ID, URL, or source-backed reference fields to score comparable rows already fetched in this run. Comparable rows and explanations are included without an extra event charge.

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

Stops each search after this many pages. If another page is visible, coverage is CAPPED instead of COMPLETE.

## Actor input object example

```json
{
  "countries": [
    "Crna Gora"
  ],
  "areas": [
    "Tivat"
  ],
  "listingModes": [
    "sale"
  ],
  "propertyTypes": [
    "Apartment"
  ],
  "maxListings": 50,
  "publishedWindow": "anytime",
  "sortBy": "default",
  "includeImages": true,
  "stateMode": "current",
  "firstSeenWindow": "anytime",
  "includeUnchanged": false,
  "includeDetails": false,
  "includeContact": false,
  "maxPagesPerSearch": 2
}
```

# Actor output Schema

## `dataset` (type: `string`):

Open the mixed-record dataset in its Overview view; use recordType or the dataset views to separate listings, summaries, duplicate clusters, comparables, and coverage.

## `output` (type: `string`):

Open OUTPUT for delivery totals, source coverage, state counts, resource totals, and event-charge totals.

# 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 = {
    "countries": [
        "Crna Gora"
    ],
    "areas": [
        "Tivat"
    ],
    "propertyTypes": [
        "Apartment"
    ],
    "maxListings": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("luminar/realitica-balkan-property-radar-private-v1").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 = {
    "countries": ["Crna Gora"],
    "areas": ["Tivat"],
    "propertyTypes": ["Apartment"],
    "maxListings": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("luminar/realitica-balkan-property-radar-private-v1").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 '{
  "countries": [
    "Crna Gora"
  ],
  "areas": [
    "Tivat"
  ],
  "propertyTypes": [
    "Apartment"
  ],
  "maxListings": 50
}' |
apify call luminar/realitica-balkan-property-radar-private-v1 --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,luminar/realitica-balkan-property-radar-private-v1"
        }
    }
}

```

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/pwJTPSdcPZfOmbugn/builds/aZaxGabM7Nrgfh2Xa/openapi.json
