# Hemnet.se Real Estate Scraper (`artsiom_k/hemnet-scraper`) Actor

Sweden real estate scraper for Hemnet.se — active for-sale listings plus a historical realized-price (sold) archive, with delta mode.

- **URL**: https://apify.com/artsiom\_k/hemnet-scraper.md
- **Developed by:** [Artsiom Kunitsyn](https://apify.com/artsiom_k) (community)
- **Categories:** Real estate
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.50 / 1,000 results

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/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## hemnet-scraper

Scrapes real estate listings from [Hemnet.se](https://www.hemnet.se), Sweden's dominant real
estate portal — active for-sale listings plus Hemnet's own historical realized-price archive
("Slutpriser") — by reading the site's own server-rendered Apollo GraphQL state, not a separate API.

### Contents

- [Key features](#key-features)
- [Output](#output)
- [Input](#input)
- [Input examples](#input-examples)
- [Incremental (delta) mode](#incremental-delta-mode)
- [How to scrape Hemnet.se](#how-to-scrape-hemnetse)
- [FAQ](#faq)

### 🔑 Key features

- **Two genuinely different datasets, one actor.** `entityType: for_sale` covers the active
  market; `entityType: sold` covers Hemnet's own realized-price archive — real sold price,
  asking price, and the difference between them, for completed sales. This second dataset alone
  runs into the thousands of records even for a single mid-size municipality.
- **No login, no API key.** Every listing's full data — price, area, rooms, broker, construction
  year, tenure — is embedded directly in the page's own server-rendered JSON; this actor reads
  that, it doesn't call a private endpoint.
- **Delta mode built in.** Every run classifies each listing as `new`, `changed`, `unchanged`, or
  `delisted` against a persisted baseline, so a scheduled re-run only bills for what actually
  changed.

### 📋 Output

One dataset item per listing (see [`.actor/dataset_schema.json`](.actor/dataset_schema.json) for
the full field list) plus a `change_type` field.

**Example — for-sale listing:**

```json
{
  "source": "hemnet",
  "external_id": "21786044",
  "url": "https://www.hemnet.se/bostad/villa-8rum-trelleborg-trelleborgs-kommun-johannesdalsvagen-3-21786044",
  "country": "SE",
  "region": "Skåne län",
  "municipality": "Trelleborgs kommun",
  "street_address": "Johannesdalsvägen 3",
  "postal_code": "23192",
  "listing_type": "for_sale",
  "property_type": "Villa",
  "tenure": "Äganderätt",
  "asking_price_sek": 5495000.0,
  "price_sek": 5495000.0,
  "living_area_sqm": 190.0,
  "land_area_sqm": 804.0,
  "rooms": 8.0,
  "construction_year": 2006,
  "broker_name": "Emma Bergh",
  "agency_name": "Erik Olsson Fastighetsformedling",
  "published_at": "2026-09-11T21:20:47.271000+00:00",
  "days_on_hemnet": 11,
  "change_type": "new"
}
```

**Example — sold listing** (`price_sek` is the final selling price, not the asking price):

```json
{
  "source": "hemnet",
  "external_id": "6229528360611106625",
  "listing_type": "sold",
  "asking_price_sek": null,
  "price_sek": 6000000.0,
  "price_per_sqm_sek": 77922.0,
  "sold_at": "2026-09-22T18:15:00+00:00",
  "change_type": "new"
}
```

**Known gaps:**

- `latitude`/`longitude` are always `null` — not present on the detail-page objects this actor
  reads (only on a lighter search-card object it doesn't use).
- `asking_price_sek` is sometimes `null` on `sold` records — not every historical record has one
  recorded.
- `agency_name` is reconstructed from the agency's own Hemnet profile URL slug, so diacritics and
  exact spacing may not perfectly match the company's real display name.
- No rent/lease category exists — Hemnet is sale-only (verified live: no working rental section).

### ⚙️ Input

See [`.actor/input_schema.json`](.actor/input_schema.json) for the full schema. Key parameters:

| Parameter | Type | Default | Description |
|---|---|---|---|
| `entityType` | String | `for_sale` | `for_sale` (active listings) or `sold` (realized-price archive). |
| `propertyTypes` | Array | *(all)* | Optional filter on property type (Villa/Lägenhet/etc). Applied after fetching — see note below. |
| `maxItems` | Integer | `50` | Stop after pushing this many items. Clear it (`null`) for a full run. |
| `maxPages` | Integer | *(25, the sitemap's own cap)* | How many pages of Hemnet's own sitemap to walk (100 listings/page). The sitemap itself only ever exposes a rolling ~2,500-item recent window per entity type — not the full historical catalog. |
| `mode` | String | `auto` | `auto`/`full`/`incremental` — see [Incremental mode](#incremental-delta-mode). |
| `delaySeconds` | Number | `0.5` | Politeness delay between requests. |
| `impersonate` | String | `chrome` | curl\_cffi TLS-impersonation target — Hemnet is Cloudflare-fingerprinted against plain HTTP clients. |
| `proxyConfiguration` | Object | off | Apify Proxy config. Off by default — clean without one in testing. |

**Note on `propertyTypes`:** Hemnet's sitemaps don't expose property type cheaply enough to filter
before fetching, so this is a filter applied after each listing is already fetched — it reduces
what's pushed, not the request volume. A run with this filter set doesn't update the incremental
baseline (its coverage of the full scope is incomplete by construction).

### 🧪 Input examples

**Quick preview** — default 50-item cap on active listings:

```json
{
  "entityType": "for_sale"
}
```

**Sold-price archive, houses only:**

```json
{
  "entityType": "sold",
  "propertyTypes": ["Villa", "Radhus"],
  "maxItems": 500
}
```

**Scheduled tracking run** — full, uncapped:

```json
{
  "entityType": "for_sale",
  "mode": "incremental",
  "maxItems": null,
  "maxPages": null
}
```

### 🔄 Incremental (delta) mode

Every run classifies each listing as `new`, `changed` (price moved), `unchanged`, or `delisted`,
against a baseline persisted per `entityType`.

- `mode: auto` (default) — first run pushes everything (`full`); later runs push only
  `new`/`changed`/`delisted`.
- A page-capped run (`maxPages` below the sitemap's real page count) or a `propertyTypes`-filtered
  run doesn't cover the full scope, so it never updates the baseline or reports delistings — same
  rule as every other actor in this collection.

### 🚀 How to scrape Hemnet.se

1. Open the actor in Apify Console and go to the **Input** tab.
2. Pick an **entityType** — `for_sale` for the live market, `sold` for realized prices.
3. `maxItems` defaults to 50 (a quick preview) — clear it along with `maxPages` for a full run.
4. Click **Start**.
5. Browse results in the **Output** tab, or download as JSON/CSV/Excel, or pull them via the API.

### ❓ FAQ

**Is it legal to scrape Hemnet.se?** It's legal to collect publicly available listing data such as
prices, descriptions, and locations. Results may contain limited personal data (broker names for
professional listings); scrape it only with a legitimate purpose under GDPR.

**Why is `asking_price_sek` sometimes null on sold listings?** Not every historical record in
Hemnet's own archive has one recorded — see [Output](#output).

**Does this cover rentals?** No — Hemnet itself has no rental section (verified live), only sales.

### Search keywords

hemnet scraper, hemnet.se scraper, sweden real estate scraper, swedish property scraper, hemnet
slutpriser scraper, hemnet sold prices, bostad scraper, villa scraper sweden, lägenhet scraper,
real estate delta scraper, real estate price tracking, sweden property data feed

# Actor input Schema

## `entityType` (type: `string`):

"for\_sale": active listings currently on the market. "sold": Hemnet's own historical realized-price archive ("Slutpriser") — a genuinely separate dataset of completed sales with both asking and final price.

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

Optional push-time filter on property type (Villa/Lägenhet/etc, in Swedish as Hemnet itself labels them). Leave empty for all types. Note: this is a filter applied after fetching, not a discovery-level scope, so it does not reduce request volume, and a run with this filter set does not update the incremental-mode baseline (see README).

## `maxPages` (type: `integer`):

How many pages of Hemnet's own sitemap to walk (100 listing URLs per page; the sitemap itself is capped at 25 pages, a rolling recent-item window, not Hemnet's full catalog). Leave empty for the full 25-page window. A capped run does not update the incremental-mode baseline.

## `maxItems` (type: `integer`):

Stop after pushing this many dataset items. Defaults to 50 — a fast, cheap preview, and what keeps an unconfigured run within Apify's automated 5-minute QA check. Raise it or clear it (set to null) for a full run.

## `delaySeconds` (type: `number`):

Politeness delay between consecutive sitemap-page and detail-page requests.

## `impersonate` (type: `string`):

curl\_cffi browser TLS-impersonation target. Hemnet is Cloudflare-fingerprinted against plain HTTP clients (verified live) — "chrome" is confirmed clean and is the default; firefox/safari also verified clean if this one ever starts getting blocked.

## `mode` (type: `string`):

"auto" (recommended): full scan on the first run for a given entity type, incremental (new/changed only) afterwards. "full": always push every listing and refresh the baseline. "incremental": always push only new/changed listings.

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

Apify Proxy configuration. Residential is the default — verified live that Hemnet's Cloudflare protection blocks Apify's own datacenter/cloud infra IPs outright (clean from a dev IP with no proxy, but 403s on real Apify infra without one), while Residential passed.

## Actor input object example

```json
{
  "entityType": "for_sale",
  "propertyTypes": [],
  "maxItems": 50,
  "delaySeconds": 0.5,
  "impersonate": "chrome",
  "mode": "auto",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("artsiom_k/hemnet-scraper").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("artsiom_k/hemnet-scraper").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 '{}' |
apify call artsiom_k/hemnet-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,artsiom_k/hemnet-scraper"
        }
    }
}
```

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/XBMHPdkraGmz9Rzuh/builds/lua44qunI5BnP70hp/openapi.json
