# Property Listing Lifecycle Tracker — Belgium (`studio-amba/property-listing-lifecycle-tracker`) Actor

Cross-portal meta-scraper (Immoweb, 2dehands, Immovlan, Zimmo) that tracks Belgian property listings run over run and emits only lifecycle CHANGE events — price cuts, relists, going stale, and disappearing from every portal — not a full re-export.

- **URL**: https://apify.com/studio-amba/property-listing-lifecycle-tracker.md
- **Developed by:** [Studio Amba](https://apify.com/studio-amba) (community)
- **Categories:** Real estate, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $15.00 / 1,000 lifecycle event detecteds

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 a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
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.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js/docs.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python/docs.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/integrations/mcp.md).

If your project is in a different language, use 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

## Property Listing Lifecycle Tracker

Tracks Belgian property listings across four portals — Immoweb, 2dehands, Immovlan and Zimmo — run over run, and emits only lifecycle CHANGE events: price cuts, relists, going stale, and disappearing from every portal. Never a full re-export of what's on the market today.

### Why use this actor?

Anyone watching a market for a lead signal — investors tracking price drops, agencies watching stale inventory, buyers waiting on a specific zone — doesn't want the whole catalogue re-downloaded every day so they can spot the difference themselves. This actor does that diffing for you. Run it on a schedule and every run only returns what actually changed since the last one: a persistent, named key-value store remembers every property it has seen, across separate scheduled runs, not just within one run.

It's a meta-actor: under the hood it fans out to our own Immoweb, 2dehands, Immovlan and Zimmo scrapers, cross-portal deduplicates the same physical property by postcode + property type + surface (the same fingerprinting approach proven in `belgium-property-pige`, since none of these portals share a listing ID), and diffs that deduped set against what it saw last time.

### How to scrape property listing lifecycle changes in Europe

1. Pick your zone(s) as `"City:PostalCode"` (e.g. `"Gent:9000"`, `"Antwerpen:2000"`) — one entry per zone you want tracked independently.
2. Run it once. The first run has nothing to diff against, so it silently seeds its memory (every property found is tracked, but a genuinely new listing isn't one of the four event types) and returns a status row per zone so the dataset is never empty.
3. Schedule it (see below). Every later run only returns properties whose price dropped, that reappeared after going silent, that have been listed unchanged past your stale threshold, or that vanished from every tracked portal.
4. Widen coverage over time by adding more zones — each one tracks its own independent history.

### Scheduling

This actor's entire value is in the diff, not the snapshot — it needs to run on a schedule, not as a one-off.

1. Open the actor in Apify Console, go to **Schedules → Create new schedule**.
2. Daily (e.g. `0 6 * * *`, 6am UTC) is a reasonable cadence for most zones — price cuts and delistings on Belgian portals don't happen faster than that. Twice-daily is fine for a hot, fast-moving zone; there's no benefit going more often than the market actually updates.
3. Leave your zone/portal selection as-is between scheduled runs. Never enable `resetState` on a real schedule — it wipes tracking history and turns every run back into a "first run," discarding every price-cut/relist/stale/gone baseline you've built up.
4. Point the schedule's output at an integration (Zapier, Make, a webhook) that fires per new dataset item, so lifecycle events land in your CRM or alerting tool the same day they're detected.

### Input

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `zones` | Array | No | Zones to track, each `"City:PostalCode"` (e.g. `"Gent:9000"`). Default: `["Gent:9000"]`. |
| `transactionType` | String | No | `sale` or `rent`. Default `sale`. |
| `propertyType` | String | No | Restrict to one type: `apartment`, `house`, `land`, `parking`, `commercial`, `room`. Empty = all types. |
| `portals` | Array | No | Which of the four portals to track. Default: all four — cross-portal coverage is what makes a `gone` event trustworthy. |
| `staleDaysThreshold` | Integer | No | Continuous days listed, unchanged, before a `stale` event fires (once per streak). Default 30. |
| `minPriceDropPct` | Number | No | Minimum % price decrease to count as `price_cut`, filters rounding noise. Default 1. |
| `maxItemsPerSource` | Integer | No | Max listings fetched per portal per zone per run. Default 40. |
| `timeoutPerSourceSecs` | Integer | No | Max wait per portal call before it's marked failed and skipped. Default 150. |
| `proxyConfiguration` | Object | No | Passed through to every child portal — Belgian residential proxies recommended (Immoweb/Zimmo block non-residential IPs). |
| `resetState` | Boolean | No | Testing only — wipes tracking state for selected zones before the run. Never use on a real schedule. |

### Output

One row per detected lifecycle event, plus one status row per zone on a legitimate zero-change run (first-run seed, or nothing changed since last run).

| Field | Type | Example |
|-------|------|---------|
| `eventType` | String | `"price_cut"` |
| `listingId` | String | `"9000\|house\|140"` |
| `address` | String | `"Kortrijksesteenweg 12, Gent, 9000"` |
| `zone` | String | `"Gent (9000)"` |
| `oldPrice` | Integer | `349000` |
| `newPrice` | Integer | `329000` |
| `priceDeltaPct` | Number | `-5.7` |
| `daysOnMarket` | Integer | `18` |
| `portals` | Array | `["immoweb", "zimmo"]` |
| `detectedAt` | String | `"2026-07-20T08:39:52.115Z"` |
| `recordType` | String | `"lifecycle_event"` or `"run_status"` |
| `propertyType` | String | `"house"` |
| `transactionType` | String | `"sale"` |
| `city` | String | `"Gent"` |
| `postalCode` | String | `"9000"` |
| `surfaceM2` | Number | `140` |
| `listingTitle` | String | `"Charming renovated house near center"` |

### Example output

```json
{
    "eventType": "price_cut",
    "listingId": "1050|apartment|214",
    "address": "Avenue Louise 335, Ixelles, 1050",
    "zone": "Gent (9000)",
    "oldPrice": 1144250,
    "newPrice": 995000,
    "priceDeltaPct": -13.0,
    "daysOnMarket": 0,
    "portals": ["immoweb"],
    "detectedAt": "2026-07-20T08:41:12.552Z",
    "recordType": "lifecycle_event",
    "propertyType": "apartment",
    "transactionType": "sale",
    "city": "Ixelles",
    "postalCode": "1050",
    "surfaceM2": 214,
    "listingTitle": "JARDIN DU ROI - LUXURY 3 BEDR APT WITH TERRACE + PARKING"
}
````

On a run with no lifecycle changes (first-run seed, or a healthy but quiet run), you get one status row per zone instead of an empty dataset:

```json
{
    "eventType": null,
    "listingId": "",
    "address": "",
    "zone": "Gent (9000)",
    "oldPrice": null,
    "newPrice": null,
    "priceDeltaPct": null,
    "daysOnMarket": null,
    "portals": ["immoweb", "zimmo"],
    "detectedAt": "2026-07-20T08:35:43.000Z",
    "recordType": "run_status",
    "propertyType": "(all)",
    "transactionType": "sale",
    "city": "",
    "postalCode": "",
    "surfaceM2": null,
    "listingTitle": "STATUS: seeded 19 tracked listings, 0 lifecycle events yet"
}
```

### How the lifecycle engine works

Every run fetches current listings from the selected portals for each zone, cross-portal deduplicates them into distinct properties (same fingerprinting approach as `belgium-property-pige`: bucket by postcode + property type, cluster by surface ±3% and price ±5%), and diffs that set against a persistent, named key-value store (`property-listing-lifecycle-state`, one map per zone) that survives across separate scheduled runs — not `Actor.useState`, which only checkpoints a single run's migration-resume state.

- **`price_cut`** — a tracked, still-active property's price dropped by at least `minPriceDropPct` since it was last seen.
- **`relist`** — a property previously marked `gone` reappeared on a tracked portal.
- **`stale`** — a property has been continuously active past `staleDaysThreshold`, flagged once per continuous streak (not re-flagged every run while it stays listed).
- **`gone`** — a previously-active property is absent from every tracked portal this run. This pass only runs when **every selected portal succeeded** this run — a partial-portal failure means "absent from the portals that worked," not "absent from the market," so a bad run never produces false `gone` events (skipped explicitly, logged as a warning).
- A property seen for the first time ever is silently seeded into the state store with no event — "new listing" isn't one of the four tracked event types.

Proven live (2026-07-20, Gent 9000, Immoweb + Zimmo):

- **Seed run**: fetched 20 raw listings (10 Immoweb + 10 Zimmo), cross-portal deduped to 19 properties, seeded all 19 into state, 0 events (correct — nothing to diff against yet), pushed 1 zero-delta status row.
- **Delta run** (identical input, no reset, re-fetched the same live zone): correctly recognized all 19 already-tracked properties (0 re-seeds, proving persistence works across separate runs, not just within one). With two prices deliberately raised in the persisted state to simulate a prior higher price, and one `firstSeenAt` pushed back 40 days to cross the stale threshold, the diff correctly emitted exactly 2 `price_cut` events (`-13.0%` each, matching the injected deltas to the decimal) and exactly 1 `stale` event at `daysOnMarket: 40` — proving the diff math against real re-fetched live prices, not just the persistence layer.
- `gone` and `relist` share the same diff pass and portal-health gating exercised above (same code, same run); they were not separately forced live this session. `gone` additionally requires all selected portals to succeed in the same run, which both did in every live run so far — that specific gate has not yet been exercised by a partial-portal-failure run.

### Limitations

- **Cross-portal dedup is a fingerprint, not a guaranteed match.** Two distinct properties with the same postcode, property type and rounded surface (±3%) can collapse into one tracked entity; two listings of the same real property with materially different reported surface can end up tracked separately. This is the same tradeoff `belgium-property-pige` documented — no portal exposes a shared cross-site listing ID, so there is no exact key.
- **`gone` detection requires every selected portal to succeed that run.** If any portal fails (anti-bot block, timeout), the gone-pass is skipped entirely for that run rather than risk false "delisted" events — you'll see a warning in the run log instead. `price_cut` and `stale` are unaffected since they only evaluate properties actually seen this run.
- **`daysOnMarket` spans relist gaps.** A property's "days on market" counts from the very first time this actor saw it, even through a period it was marked `gone` — it does not reset on relist. This is a deliberate choice (continuous property history) documented here so it isn't mistaken for "days since this listing".
- **2dehands has no structured surface field**, so 2dehands-only listings can't be fingerprinted by surface and fall back to a URL-derived identity key — they won't cross-dedup with Immoweb/Immovlan/Zimmo listings of the same property.
- **Search-net coverage is a sample of each zone, not an exhaustive census.** `maxItemsPerSource` caps how deep each portal is paged per run; a very active market may need a higher cap (or a tighter zone) to avoid missing changes on listings outside the fetched window.

### Related scrapers

- `belgium-property-pige` — cross-portal FSBO lead generation on the same four Belgian portals (every matching row every run, not delta-only).
- `eu-fresh-company-radar` — the same named-KVS delta pattern applied to new company registrations across five EU registries.
- `france-pige-pro` — the French equivalent cross-portal lead-gen actor (Bien'ici, LeBonCoin, SeLoger, Logic-Immo).
- `immoweb-scraper`, `immovlan-scraper`, `zimmo-scraper`, `2dehands-scraper` — the individual portal scrapers this actor fans out to.

### Cost estimate

Priced per lifecycle event detected, plus a flat per-run start fee. A quiet day in a small zone might return only the zero-delta status row; an active zone with several price cuts and delistings costs proportionally more — cost scales with genuine change, not with how much of the market was scanned underneath.

# Actor input Schema

## `zones` (type: `array`):

Which zones to track, each as "City:PostalCode" (e.g. "Gent:9000", "Antwerpen:2000", "Brussel:1000"). Each zone keeps its own persistent tracking state. Leave empty to track Gent (9000).

## `transactionType` (type: `string`):

Track properties for sale or for rent.

## `propertyType` (type: `string`):

Restrict tracking to one property type. Leave empty to track all types.

## `portals` (type: `array`):

Which Belgian portals to track. Leave empty for all four — cross-portal coverage is what makes the 'gone from every portal' event trustworthy; tracking fewer portals makes gone/relist events noisier (see README).

## `staleDaysThreshold` (type: `integer`):

How many continuous days a property must stay listed, unchanged, before it's flagged as 'stale' (once per continuous listing streak).

## `minPriceDropPct` (type: `number`):

Minimum percentage price decrease to count as a price\_cut event. Filters out rounding noise from portal re-scrapes.

## `maxItemsPerSource` (type: `integer`):

Maximum listings fetched per portal per zone per run. Higher values track more of the market but take longer and cost more per run.

## `timeoutPerSourceSecs` (type: `integer`):

Maximum time to wait for each underlying portal scraper call before it's marked failed and skipped.

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

Proxy passed through to every child portal scraper. Belgian residential proxies are recommended — Immoweb and Zimmo block non-residential IPs.

## `resetState` (type: `boolean`):

Clears the persistent tracking state for every selected zone before this run, so the run re-seeds from scratch. Use for QA/testing only — never enable on a real scheduled run, it discards all lifecycle history.

## Actor input object example

```json
{
  "zones": [
    "Gent:9000"
  ],
  "transactionType": "sale",
  "propertyType": "",
  "portals": [
    "immoweb",
    "2dehands",
    "immovlan",
    "zimmo"
  ],
  "staleDaysThreshold": 30,
  "minPriceDropPct": 1,
  "maxItemsPerSource": 12,
  "timeoutPerSourceSecs": 120,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "BE"
  },
  "resetState": false
}
```

# 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 = {
    "zones": [
        "Gent:9000"
    ],
    "portals": [
        "immoweb",
        "2dehands",
        "immovlan",
        "zimmo"
    ],
    "staleDaysThreshold": 30,
    "minPriceDropPct": 1,
    "maxItemsPerSource": 12,
    "timeoutPerSourceSecs": 120,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "BE"
    },
    "resetState": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("studio-amba/property-listing-lifecycle-tracker").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 = {
    "zones": ["Gent:9000"],
    "portals": [
        "immoweb",
        "2dehands",
        "immovlan",
        "zimmo",
    ],
    "staleDaysThreshold": 30,
    "minPriceDropPct": 1,
    "maxItemsPerSource": 12,
    "timeoutPerSourceSecs": 120,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "BE",
    },
    "resetState": False,
}

# Run the Actor and wait for it to finish
run = client.actor("studio-amba/property-listing-lifecycle-tracker").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "zones": [
    "Gent:9000"
  ],
  "portals": [
    "immoweb",
    "2dehands",
    "immovlan",
    "zimmo"
  ],
  "staleDaysThreshold": 30,
  "minPriceDropPct": 1,
  "maxItemsPerSource": 12,
  "timeoutPerSourceSecs": 120,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "BE"
  },
  "resetState": false
}' |
apify call studio-amba/property-listing-lifecycle-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=studio-amba/property-listing-lifecycle-tracker",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Property Listing Lifecycle Tracker — Belgium",
        "description": "Cross-portal meta-scraper (Immoweb, 2dehands, Immovlan, Zimmo) that tracks Belgian property listings run over run and emits only lifecycle CHANGE events — price cuts, relists, going stale, and disappearing from every portal — not a full re-export.",
        "version": "0.1",
        "x-build-id": "Pnzsrxhly8geinlRD"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/studio-amba~property-listing-lifecycle-tracker/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-studio-amba-property-listing-lifecycle-tracker",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/studio-amba~property-listing-lifecycle-tracker/runs": {
            "post": {
                "operationId": "runs-sync-studio-amba-property-listing-lifecycle-tracker",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/studio-amba~property-listing-lifecycle-tracker/run-sync": {
            "post": {
                "operationId": "run-sync-studio-amba-property-listing-lifecycle-tracker",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "properties": {
                    "zones": {
                        "title": "Zones",
                        "type": "array",
                        "description": "Which zones to track, each as \"City:PostalCode\" (e.g. \"Gent:9000\", \"Antwerpen:2000\", \"Brussel:1000\"). Each zone keeps its own persistent tracking state. Leave empty to track Gent (9000).",
                        "items": {
                            "type": "string"
                        }
                    },
                    "transactionType": {
                        "title": "Transaction Type",
                        "enum": [
                            "sale",
                            "rent"
                        ],
                        "type": "string",
                        "description": "Track properties for sale or for rent.",
                        "default": "sale"
                    },
                    "propertyType": {
                        "title": "Property Type",
                        "enum": [
                            "",
                            "apartment",
                            "house",
                            "land",
                            "parking",
                            "commercial",
                            "room"
                        ],
                        "type": "string",
                        "description": "Restrict tracking to one property type. Leave empty to track all types.",
                        "default": ""
                    },
                    "portals": {
                        "title": "Portals",
                        "type": "array",
                        "description": "Which Belgian portals to track. Leave empty for all four — cross-portal coverage is what makes the 'gone from every portal' event trustworthy; tracking fewer portals makes gone/relist events noisier (see README).",
                        "items": {
                            "type": "string",
                            "enum": [
                                "immoweb",
                                "2dehands",
                                "immovlan",
                                "zimmo"
                            ],
                            "enumTitles": [
                                "Immoweb",
                                "2dehands",
                                "Immovlan",
                                "Zimmo"
                            ]
                        }
                    },
                    "staleDaysThreshold": {
                        "title": "Stale Threshold (days)",
                        "minimum": 3,
                        "maximum": 365,
                        "type": "integer",
                        "description": "How many continuous days a property must stay listed, unchanged, before it's flagged as 'stale' (once per continuous listing streak).",
                        "default": 30
                    },
                    "minPriceDropPct": {
                        "title": "Minimum Price Drop to Report (%)",
                        "minimum": 0,
                        "maximum": 50,
                        "type": "number",
                        "description": "Minimum percentage price decrease to count as a price_cut event. Filters out rounding noise from portal re-scrapes.",
                        "default": 1
                    },
                    "maxItemsPerSource": {
                        "title": "Max Items Per Portal Per Zone",
                        "minimum": 5,
                        "maximum": 500,
                        "type": "integer",
                        "description": "Maximum listings fetched per portal per zone per run. Higher values track more of the market but take longer and cost more per run.",
                        "default": 40
                    },
                    "timeoutPerSourceSecs": {
                        "title": "Timeout Per Portal (seconds)",
                        "minimum": 30,
                        "maximum": 600,
                        "type": "integer",
                        "description": "Maximum time to wait for each underlying portal scraper call before it's marked failed and skipped.",
                        "default": 150
                    },
                    "proxyConfiguration": {
                        "title": "Proxy Configuration",
                        "type": "object",
                        "description": "Proxy passed through to every child portal scraper. Belgian residential proxies are recommended — Immoweb and Zimmo block non-residential IPs."
                    },
                    "resetState": {
                        "title": "Reset Tracking State (testing only)",
                        "type": "boolean",
                        "description": "Clears the persistent tracking state for every selected zone before this run, so the run re-seeds from scratch. Use for QA/testing only — never enable on a real scheduled run, it discards all lifecycle history.",
                        "default": false
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
