DEV Community

Cover image for How to Scrape Storia.ro Listings in Bucharest Without a Paid API
Tim Zinin
Tim Zinin

Posted on Originally published at apify.com

How to Scrape Storia.ro Listings in Bucharest Without a Paid API

The problem

Bucharest property workflows rarely start from a clean API. Listings arrive as exports — from owners, agents, partner feeds, or your own CRM — and every source shapes its fields differently: prices as text, rooms embedded in titles, timestamps in local formats, no licence context attached. Before any analyst can review a batch, someone has to normalize the records, deduplicate them, and document where each row came from. Done manually, that is spreadsheet work with no audit trail.

The paid alternatives are portal-side: scraping subscriptions or data contracts that re-sell marketplace content. If your organization already holds authorized exports, what you need is a normalization and evidence layer — deterministic IDs, freshness, explicit gaps, and a review queue — not another crawler.

What the actor does

The Storia Romania Real Estate Listings actor (Store slug storia-bucharest) is the Authorized Bucharest Property Export Normalizer. It processes buyer-owned, owner-authorized, or licensed property records and makes zero external requests — no marketplace, portal, browser, proxy, DNS, or child-Actor calls.

Per the README, each run:

  • Validates a closed input contract: unknown properties fail closed, malformed rows become explicit free diagnostics when other valid work exists, and duplicate (sourceRecordId, sourceName) identities are suppressed before billing.
  • Delivers one stable normalized Dataset row per accepted unique record, preserving the familiar listing fields (title, price, currency, deal type, property type, rooms, area, location, posting date) plus a deterministic stableId and rowDigest.
  • Carries provenance on every row: source name, source URL, licence statement, buyer-supplied retrieval time, and a changesMade transformation disclosure.
  • Computes freshness against your freshnessHours threshold (default 168) using only the supplied retrieval time, and a confidence score that measures contract completeness with visible gaps — not factual truth.
  • Routes every accepted row to review_required with safeToAutomate=false, and records settlement-neutral billing intent; the authoritative paid/free/withheld/unknown reconciliation lives in the current-run KVS OUTPUT.

Legacy portal-style inputs (deal_type, property_type, city, max_items, max_pages) still parse but perform no source request — they emit one free migration diagnostic explaining how to move to records. The README is unambiguous about boundaries: the actor never verifies availability, price, ownership, or legal status, and it takes no external action.

Example: input and output

The documented input submits bounded records with provenance:

{
  "schemaVersion": "2.0",
  "authorization": "I confirm I may process and commercially use these submitted records.",
  "sourceContext": "buyer_owned_export",
  "batchName": "storia-bucharest-evidence-review",
  "freshnessHours": 168,
  "records": [
    {
      "sourceRecordId": "demo-ro-001",
      "title": "Authorized Bucharest apartment export",
      "entityName": "Example property",
      "category": "residential-property",
      "occurredAt": "2026-08-01T09:00:00.000Z",
      "url": "https://exports.example.com/ro/demo-ro-001",
      "sourceName": "Licensed Romanian property export",
      "sourceLicense": "Buyer confirms owner, agent, or commercial feed permission.",
      "sourceRetrievedAt": "2026-08-01T10:00:00.000Z",
      "changesMade": "Whitespace and numeric representation normalized.",
      "attributes": {
        "price": 195000,
        "currency": "EUR",
        "deal_type": "sale",
        "property_type": "apartment",
        "rooms": 3,
        "area_sqm": 78,
        "location": "Bucharest, Romania",
        "posted_date": "2026-08-01T09:00:00.000Z"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The README does not ship a full dataset-row fixture; its field dictionary defines the delivered row: recordType, stableId, found, the listing fields above, observedAt, freshness, change, decisionConfidence with gaps, evidence, decision (always review_required), recommendedAction, priority, safeToAutomate=false, failureDiagnostics, billing, and rowDigest. For an accepted one-record run, the README's illustrative run binding is {"status":"SUCCEEDED","evidenceAccepted":true,"datasetRows":1,"resultFoundDelta":1} — one paid result-found event per delivered row. A live sample dataset is linked on the actor page.

Pricing and the free limit

Pay-per-event: $0.005 per actor start plus $0.002 per delivered normalized record (result-found), with subscription discounts on higher Apify tiers (the Store headline works out to $1.70 per 1,000 records at the entry tier). At list prices, 100 records cost $0.005 + 100 × $0.002 = $0.205 per run. Apify's free plan includes $5 of usage credits per month, which covers about 24 full 100-record runs — roughly 2,500 normalized records — before you pay anything. Migration, invalid, and duplicate diagnostics are free.

Try it

Start with a single authorized record, read the KVS OUTPUT receipt before the Dataset rows, then scale: Storia Romania Real Estate Listings.

For AI agents and MCP

The actor takes JSON in and returns structured JSON via the Apify API, so agents can call it directly, and the README includes an Apify MCP server setup plus an "MCP tool call" playbook. The playbook's rule is the one agents must encode: read KVS OUTPUT first, require the current runId, route review_required rows to a human with licence, gaps, freshness, and digest attached, and never treat safeToAutomate=false as permission to message, trade, publish, or modify an external system.

Top comments (0)