# Booli Scraper - Swedish Real Estate Listings & Sold Prices (`studio-amba/booli-scraper`) Actor

Scrape property listings and historical sold prices from Booli.se, Sweden's #2 real estate portal. Extract prices, locations, sizes, rooms, floors, monthly fees, images, and agency details for apartments, houses, and holiday homes across Swedish cities. No login needed.

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

## Pricing

from $5.00 / 1,000 result scrapeds

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?

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

## Booli Scraper

Extract property listings and historical sold prices from [Booli.se](https://www.booli.se), Sweden's #2 real estate portal. This actor pulls prices, sizes, rooms, floors, monthly fees, images, and agency details for apartments, houses, townhouses, and holiday homes across Swedish cities — including Booli's flagship feature that most competing scrapers skip: closed-sale (slutpris) history, not just current asking prices.

### Why use this actor?

Booli is Sweden's second-largest property portal, and the only major one that publishes a full history of what homes actually sold for, not just what they're listed at. That makes it useful for price-trend analysis, comparable-sale research, and investment due diligence in a way a for-sale-only feed can't match.

Use cases include market analysis, price-per-square-meter benchmarking by district, relocation and mortgage-appraisal research, academic housing studies, and competitor tracking for proptech and real estate agency tools.

### How to scrape Booli data

1. Go to the actor's page on the Apify Store
2. Choose a city (Stockholm, Göteborg, Malmö, Uppsala, Lund, Jönköping, Sundsvall, or all of Sweden) or paste a Booli.se search or listing URL
3. Pick search type: for-sale listings (till salu) or sold prices (slutpriser)
4. Click "Start" to run the scraper
5. Download results as JSON, CSV, or Excel when complete

The actor fetches pages through Bright Data's Web Unlocker to get past Booli.se's Cloudflare managed challenge, then extracts data from the site's own embedded Next.js/Apollo GraphQL cache for maximum reliability — no fragile CSS scraping for the fields that matter most.

### Input

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `searchType` | String | No | `sale` (för-sale, default) or `sold` (historical sold prices) |
| `city` | String | No | `sweden`, `stockholm` (default), `goteborg`, `malmo`, `uppsala`, `lund`, `jonkoping`, or `sundsvall` |
| `customAreaId` | String | No | Advanced: Booli's own numeric area ID for any other Swedish municipality. Overrides `city`. |
| `objectType` | String | No | Advanced: filter by property type as Booli's site uses it, e.g. `Lägenhet` (apartment), `Villa`, `Radhus` (townhouse), `Fritidshus` (holiday home) |
| `startUrls` | Array | No | Booli.se search result pages or individual `/annons/{id}` / `/bostad/{id}` listing URLs. Overrides `city`/`searchType`. |
| `maxResults` | Integer | No | Maximum listings to return. Search pages return 35 listings each; the actor paginates automatically. |
| `brightDataApiKey` | String | No | Your own Bright Data API key for the Web Unlocker zone. Leave empty to use the built-in unlocking service. |

### Output

Each result contains:

| Field | Type | Example |
|-------|------|---------|
| `title` | String | `"Lägenhet, Ringvägen 9A, Södermalm"` |
| `price` | Number | `7620000` |
| `currency` | String | `"SEK"` |
| `listingType` | String | `"sale"` or `"sold"` |
| `propertyType` | String | `"Lägenhet"` |
| `tenureForm` | String | `"Bostadsrätt"` (for-sale listings only — not present on sold listings) |
| `address` | String | `"Ringvägen 9A"` |
| `district` | String | `"Södermalm"` |
| `city` | String | `"Stockholm"` |
| `latitude` / `longitude` | Number | `59.3112` / `18.0714` |
| `rooms` | Number | `2` |
| `surface` | Number | `63.6` |
| `floor` | Number | `1` |
| `pricePerSqm` | Number | `54900` (sold listings, and any listing scraped via a direct `startUrls` detail link; `null` for for-sale listings from a `searchType: sale` search crawl — see Limitations) |
| `monthlyFee` | Number | `4264` (for-sale listings only — not present on sold listings) |
| `listPrice` | Number | `7500000` (present on most for-sale listings and roughly 3 in 4 sold listings) |
| `soldDate` | String | `"2026-07-15"` (sold listings only) |
| `publishedDate` | String | `"2026-08-20"` (for-sale listings only) |
| `imageUrl` | String | Primary listing photo URL (nearly always present on for-sale listings; present on roughly 1 in 3 sold listings) |
| `imageUrls` | Array | All listing photo URLs (same availability pattern as `imageUrl`) |
| `agencyName` | String | `"Fastighetsbyrån"` (for-sale listings only — not present on sold listings) |
| `agencyUrl` | String | Agency profile URL, when available (for-sale listings only) |
| `booliId` | String | `"6242228"` |
| `url` | String | Full Booli.se listing URL |
| `scrapedAt` | String | ISO 8601 timestamp |

### Example output

```json
{
    "title": "Lägenhet, Ringvägen 9A, Södermalm",
    "price": 7620000,
    "currency": "SEK",
    "listingType": "sale",
    "propertyType": "Lägenhet",
    "address": "Ringvägen 9A",
    "district": "Södermalm",
    "city": "Stockholm",
    "rooms": 3,
    "surface": 74.5,
    "pricePerSqm": 102282,
    "monthlyFee": 4264,
    "listPrice": 7500000,
    "publishedDate": "2026-08-20",
    "imageUrl": "https://bcdn.se/images/cache/32010988_1024x0.jpg",
    "agencyName": "Fastighetsbyrån",
    "booliId": "6242228",
    "url": "https://www.booli.se/annons/6242228",
    "scrapedAt": "2026-08-28T09:30:00.000Z"
}
```

### Cost estimate

This actor fetches every page through Bright Data's Web Unlocker (one request per search page, not a full browser session). Approximate costs:

- **~35 results (1 search page)**: $0.01-0.03 in platform credits
- **~350 results (10 search pages)**: $0.10-0.30 in platform credits
- **Individual listing URLs (`startUrls`)**: roughly one Web Unlocker request per listing

Actual usage cost only settles once the run reports SUCCEEDED — reading the dataset from a still-running run will undercount what you'll actually be charged.

### Tips for best results

- **Start small** — test with `maxResults: 20` before running large scrapes.
- **Use `searchType: sold` for price research** — Booli's slutpriser history is its main edge over a for-sale-only competitor feed, and covers years of closed transactions per area.
- **Direct listing URLs get fully structured numeric fields** — pass an `/annons/{id}` or `/bostad/{id}` URL in `startUrls` to get exact `rooms`, `surface`, and `floor` as numbers straight from the detail page, rather than parsed from the search-page preview text.
- **`customAreaId` unlocks any Swedish municipality** — the curated `city` list covers the largest markets; for anywhere else, browse booli.se and read the `areaIds` value out of the URL.

### Limitations

- Booli.se fronts every page with a genuine Cloudflare managed challenge. This actor routes requests through Bright Data's Web Unlocker to get past it.
- `areaIds` are opaque numeric IDs with no human-readable URL slug on Booli's side — there's no way to search by free-text city name server-side, so this actor ships a curated list plus a numeric override.
- Search-page listings carry `rooms` and `surface` as parsed plaintext (Booli doesn't expose them as separate numeric fields at the search level); detail-page scrapes via `startUrls` return fully structured numeric values instead.
- **`pricePerSqm` is null for `searchType: sale` search crawls.** Booli's search-result preview only lists a price-per-m² figure (`kr/m²`) for sold listings (`searchType: sold`) — for-sale search results show the monthly fee (`kr/mån`, captured in `monthlyFee`) in that same slot instead, and no price-per-m² anywhere in the preview data. To get `pricePerSqm` on for-sale listings, pass individual `/annons/{id}` URLs in `startUrls` — the detail page always carries it as a structured field.
- Price data on for-sale listings reflects Booli's own asking/estimate price, not a guaranteed final sale price.
- **Sold listings don't carry `tenureForm`, `agencyName`, or `agencyUrl`.** Booli's sold-property search results are a different underlying object type from for-sale listings and simply don't include these fields — always `null`/absent for `listingType: "sold"` items.
- **`imageUrl`/`imageUrls` coverage is much lower on sold listings** (roughly 1 in 3) than on for-sale listings (nearly all). Booli doesn't keep photos indexed for most historical sales.
- The actor scrapes the public website. No login or authentication is used.

### Related scrapers

- [Imobiliare.ro Scraper](https://apify.com/itsnotyouitsme/imobiliare-scraper) — Romanian real estate listings
- [Immoweb Scraper](https://apify.com/itsnotyouitsme/immoweb-scraper) — Belgium's #1 real estate portal
- [Idealista Scraper](https://apify.com/itsnotyouitsme/idealista-scraper) — Spanish real estate portal
- [Zimmo Scraper](https://apify.com/itsnotyouitsme/zimmo-scraper) — Belgian real estate listings
- [Aruodas Scraper](https://apify.com/itsnotyouitsme/aruodas-scraper) — Lithuanian real estate listings
- [Flatfox Scraper](https://apify.com/itsnotyouitsme/flatfox-scraper) — Swiss rental listings

### Need this data on a schedule, or a custom version?

We run this scraper as a managed service for businesses: scheduled runs,
deduplication, delta detection, and delivery to your inbox, Google Sheets,
or API — maintenance included. We can also build a custom version with your
exact fields and filters, or combine multiple sources into one feed.

See [studioamba.dev/services](https://studioamba.dev/services/) or email
<hello@studioamba.dev> for a free data sample.
We maintain 700+ European web scrapers and answer within one business day.

# Actor input Schema

## `searchType` (type: `string`):

For-sale listings or historical sold prices (slutpriser) — Booli's flagship differentiator over most competing portals.

## `city` (type: `string`):

City to search. Booli area IDs are opaque numeric IDs with no readable URL slug, so this is a curated list of the confirmed largest markets. Use 'Custom Area ID' below for any other Swedish municipality.

## `customAreaId` (type: `string`):

Advanced: Booli's internal numeric area ID for a municipality not in the City list above. Find it by browsing booli.se and reading the 'areaIds' query parameter from the URL. Overrides the City field when set.

## `objectType` (type: `string`):

Optional: filter by Booli's own objectType URL parameter, e.g. 'Lägenhet' (apartment), 'Villa', 'Radhus' (townhouse), 'Fritidshus' (holiday home). Leave empty for all property types. Value is passed through as-is to booli.se.

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

Booli.se search result pages (e.g. https://www.booli.se/sok/till-salu?areaIds=1) or individual listing URLs (/annons/{id} or /bostad/{id}). Overrides City/Search Type when provided.

## `maxResults` (type: `integer`):

Maximum number of listings to scrape. Search pages return 35 listings each; the actor paginates automatically until this limit is reached.

## `brightDataApiKey` (type: `string`):

Optional: your own Bright Data API key for the Web Unlocker zone. Leave empty to use the built-in unlocking service.

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

Legacy field, kept for backwards compatibility. Fetching now goes through the built-in Bright Data Web Unlocker, so this is ignored.

## Actor input object example

```json
{
  "searchType": "sale",
  "city": "stockholm",
  "startUrls": [
    {
      "url": "https://www.booli.se/sok/till-salu?areaIds=1"
    }
  ],
  "maxResults": 50,
  "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 = {
    "startUrls": [
        {
            "url": "https://www.booli.se/sok/till-salu?areaIds=1"
        }
    ],
    "maxResults": 50,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("studio-amba/booli-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 = {
    "startUrls": [{ "url": "https://www.booli.se/sok/till-salu?areaIds=1" }],
    "maxResults": 50,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("studio-amba/booli-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 '{
  "startUrls": [
    {
      "url": "https://www.booli.se/sok/till-salu?areaIds=1"
    }
  ],
  "maxResults": 50,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call studio-amba/booli-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,studio-amba/booli-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/CEFtVxFoizUpsljWx/builds/vfySPa0tCEmLRpVKP/openapi.json
