DEV Community

Cover image for How to Scrape SS.ge Real Estate in Tbilisi Without a Paid API
Tim Zinin
Tim Zinin

Posted on Originally published at apify.com

How to Scrape SS.ge Real Estate in Tbilisi Without a Paid API

The problem

Tbilisi property data is scattered across portals, agent inboxes, and licensed feeds. If you run a portfolio, an agency desk, or a market-analysis workflow, you typically already receive exports — from owners, agents, or a data partner — and the painful part is not finding listings, it is turning those heterogeneous exports into one consistent, reviewable evidence layer. Doing it by hand means reconciling spreadsheets, chasing missing provenance, and having no defensible answer to "where did this row come from and when was it retrieved?"

The paid alternatives push you toward portal scraping subscriptions or per-seat SaaS contracts. For teams who hold authorized exports, a normalization layer with stable identity, freshness, and explicit gaps is the more useful primitive — and it keeps source rights where they belong: with the buyer.

What the actor does

The SS.ge Georgia Real Estate Listings actor (Store slug ss-ge-tbilisi, kept for API continuity) is a provenance-preserving normalizer for Tbilisi property records. It is not affiliated with SS.ge, and its current version deliberately does not crawl the portal. It works in two modes:

  • Authorized-export mode (default). You submit buyer-owned, owner- or agent-authorized, or licensed-feed records as structured JSON. The actor validates a closed schema, deduplicates (sourceName, sourceListingId) identities, and makes zero source network requests — no browser, proxy, login, or URL fetching. Recorded listing and image URLs are never opened.
  • Live public observation mode (live_public_listing). Exactly one bounded, login-free request to a fixed, allowlisted public SS.ge Tbilisi results page, returning up to 20 source-visible listing cards (public_property_listing_observation rows). It is a single observation, not a crawl, and the description field is omitted because the public payload can contain broker contact text.

Every accepted unique record becomes one Dataset row carrying the property fields (URL, title, price, currency, deal type, property type, rooms, area, location, coordinates, posting date, description, images, floor), a deterministic stableId plus input/request/row digests, full provenance (source name, source URL, licence statement, retrieval time, changes made), freshness computed only from your supplied retrieval timestamp, an evidence-completeness confidence score with concrete gaps, and a decision block: review_required outcome, priority, and safeToAutomate: false. A run-level KVS OUTPUT receipt reconciles delivered, paid, free, withheld, and unknown counts. Legacy city_id-style inputs still parse but return a free migration diagnostic.

The README is explicit about boundaries: it does not verify availability, price, ownership, or investment quality, does not track changes across runs (change.status is always not_measured), and does not make property decisions — every row routes to human review.

Example: input and output

Authorized-export input (from the README; the example domains and listing are fictional fixtures):

{
  "schemaVersion": "2.0",
  "authorization": "I confirm I may process and commercially use these property records.",
  "sourceContext": "owner_or_agent_authorized_export",
  "batchName": "tbilisi-property-review",
  "freshnessHours": 168,
  "listings": [
    {
      "sourceListingId": "demo-tbilisi-001",
      "title": "Two-bedroom apartment",
      "price": 195000,
      "currency": "USD",
      "deal_type": "sale",
      "property_type": "Apartment",
      "rooms": 3,
      "area_sqm": 91,
      "location": "Tbilisi, Georgia",
      "posted_date": "2026-08-12T09:00:00.000Z",
      "url": "https://properties.example.com/listings/demo-tbilisi-001",
      "sourceName": "Example authorized property export",
      "sourceLicense": "Owner-authorized export for internal property review.",
      "sourceRetrievedAt": "2026-08-12T10:00:00.000Z",
      "changesMade": "Selected fields and normalized whitespace."
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The README's happy-path fixture for that record shows the shape of a delivered row (trimmed):

{
  "recordType": "licensed_property_record",
  "stableId": "licensed-property:…",
  "title": "Two-bedroom apartment",
  "price": 195000,
  "currency": "USD",
  "deal_type": "sale",
  "found": true,
  "partial": false,
  "freshness": { "status": "fresh", "basis": "buyer_supplied_source_retrieval_time" },
  "change": { "status": "not_measured", "basis": "single_authorized_export_record" },
  "decision": { "outcome": "review_required", "priority": "normal", "safeToAutomate": false },
  "billing": { "billingEligible": true, "eventName": "result-found", "settlementSource": "current_run_kvs_output" }
}
Enter fullscreen mode Exit fullscreen mode

Pricing and the free limit

Pay-per-event: $0.005 per actor start plus $0.002 per delivered normalized record (result-found); the README's own table prices 100 records at $0.205 including the start event. Apify's free plan gives $5 of usage credits per month, which covers about 24 full 100-record runs — roughly 2,500 individual normalized records — before any charge. Free diagnostics (duplicates, invalid records, legacy migration) are not billed as results.

Try it

Normalize one authorized record first, reconcile the Dataset against the KVS OUTPUT receipt, then scale the batch: SS.ge Georgia 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 with apify-client or the REST API; the README also ships an MCP server setup (https://mcp.apify.com with this actor's tool). Its warehouse recipe gives agents a deterministic contract: stableId as the natural identity, rowDigest for version comparison, and explicit instruction that the same stable ID with a different digest means "review a changed supplied record," not proof of a portal edit — with safeToAutomate: false preserved downstream.

Top comments (0)