# Bilbasen Scraper — Denmark Cars, EV Battery & Dealers (`memo23/bilbasen-scraper`) Actor

Bilbasen.dk scraper for Denmark's car market. One row per car with numeric specs, battery kWh, WLTP range, consumption, the fitted-equipment list, full-resolution photos, and the dealer block with CVR and phone. Price banding gets past the site's 3,000-ad page wall to the whole market.

- **URL**: https://apify.com/memo23/bilbasen-scraper.md
- **Developed by:** [Muhamed Didovic](https://apify.com/memo23) (community)
- **Categories:** E-commerce, Lead generation, AI
- **Stats:** 2 total users, 1 monthly users, 64.7% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.40 / 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

## Bilbasen Scraper — Denmark Cars, EV Battery & Dealers

Turn any [Bilbasen](https://www.bilbasen.dk) search into clean, structured data. Denmark's largest car marketplace, one flat row per car: the full spec record as numbers rather than Danish text, the fitted-equipment list, every photo at full resolution, the dealer's block down to their CVR number, and the phone number the ad page only shows behind a button.

![How the Bilbasen Scraper works](https://raw.githubusercontent.com/muhamed-didovic/muhamed-didovic.github.io/main/assets/how-it-works-bilbasen.png)

#### ✨ Why use this scraper

- **Figures, not display strings** — the site prints `35.000 km`, `74,0 kWh` and `Rækkevidde 461 km`. The row carries `35000`, `74` and `461`, so a Danish dataset merges with any other market without cleanup.
- **Past the 3 000-ad wall** — bilbasen stops paginating at 100 pages of 30, however many cars match. A search larger than that is sliced into price bands and reassembled, so the electric dealer segment comes back whole at 16 741 rather than capped at 3 000.
- **The phone number the ad hides** — an ad page reports `phoneNumberType: DynamicVirtual` and no number. It is read from the dealer's own page instead, once per dealer and cached, so 86 fetches covered 120 ads.
- **A short run says so** — every band is compared against the site's own count, and a run that comes up short ends FAILED with the shortfall recorded. You never get a half dataset that looks complete.
- **One browser solve, then plain HTTP** — the AWS WAF challenge is solved once and the token carries the rest of the run, so you are not paying for a browser per ad.

#### 🎯 Use cases

| Who | What for |
|---|---|
| EV analyst | Track used electric prices against battery capacity, range and real consumption |
| Dealer group | Watch competitors' stock, pricing and how many cars each one lists |
| Lead generation | A dealer directory with address, CVR, website and phone, built from live stock |
| Price monitoring | Follow a model's asking price across the whole Danish market |
| Market research | Segment by body type, drivetrain, model year or price band |

#### 📥 Supported inputs

- A search URL — `https://www.bilbasen.dk/brugt/bil?fuel=3&sellertypes=dealer`
- A direct ad URL — `https://www.bilbasen.dk/brugt/bil/bmw/ix3/charged-m-sport-5d/6926237`
- No URL at all: set make, model, price, year, fuel or dealers-only and the search is built for you

#### ⚙️ How it works

Every bilbasen page sits behind an AWS WAF JavaScript challenge. A browser solves it once, and the resulting token is handed to a plain HTTP client for the rest of the run, re-solved only when it expires.

A search is planned before it is crawled. The site's own `hits` count is probed for the whole query and for each candidate price band, and any band still over the ceiling is halved until it fits — so the ladder is measured rather than hand-written, and keeps working as the market grows.

Each ad's own page is then read for everything the search payload cannot give. The whole listing sits in one `_props` JSON object, which is parsed once rather than scanned with regexes, and every figure is taken from a known path inside it.

#### ⚙️ Input parameters

| Field | Type | Default | Notes |
|---|---|---|---|
| `startUrls` | array | — | Search or ad URLs |
| `make` / `model` | string | — | Builds the search path |
| `priceFrom` / `priceTo` | integer | — | DKK. Pinning a range turns price banding off |
| `yearFrom` / `yearTo` | integer | — | Model year |
| `fuelType` | enum | — | Petrol, Diesel, Electric, the hybrids |
| `dealersOnly` | boolean | `false` | Trade sellers only |
| `includeDetails` | boolean | `true` | Off returns ids and URLs only |
| `scrapePhone` | boolean | `true` | One extra fetch per dealer, not per car |
| `enrichEmails` | boolean | `false` | Look up each dealer's email on their own site |
| `maxItems` | integer | `1000` | Always set it — the EV dealer search alone runs to 16 741 |
| `maxConcurrency` | integer | `5` | Ad pages fetched at once |
| `failOnIncomplete` | boolean | `true` | Fail rather than return a short dataset |
| `completenessTolerancePercent` | integer | `1` | The larger of this and the absolute tolerance applies |
| `completenessToleranceAbsolute` | integer | `25` | Covers ads added and removed mid-run |

#### 📊 Output overview

One row per car. Nested objects keep related values together: `price` carries the amount and currency, `dealer` the whole seller block, `location` the point.

#### 📦 Output sample

```json
{
  "adId": 6926237,
  "url": "https://www.bilbasen.dk/brugt/bil/bmw/ix3/charged-m-sport-5d/6926237",
  "title": "BMW iX3 Charged M-Sport 5d",
  "make": "BMW",
  "model": "iX3",
  "variant": "Charged M-Sport 5d",
  "modelYear": 2023,
  "bodyType": "SUV",
  "price": { "amount": 389900, "currency": "DKK" },
  "priceType": "Kontantpris",
  "newPriceDkk": 524720,
  "mileageKm": 35000,
  "firstRegistered": "2023-09",
  "fuel": "El",
  "batteryCapacityKwh": 74,
  "rangeKm": 461,
  "consumptionWhPerKm": 185,
  "chargingTimeDc1080Min": 28,
  "powerHp": 286,
  "torqueNm": 400,
  "accelerationSec": 6.8,
  "topSpeedKmh": 180,
  "colour": "Carbonsortmetal",
  "periodicTaxDkk": 920,
  "equipment": ["19 tommer Alufælge", "3 zone klima", "Adaptiv fartpilot"],
  "imageUrls": ["https://billeder.bilbasen.dk/bilinfo/a4258f06-….jpeg?class=S1600X1600"],
  "hasVideo": true,
  "dealer": {
    "id": 1956,
    "name": "Bayern AutoGroup Odense A/S",
    "type": "Dealer",
    "street": "Sivmosevænget 1",
    "zipCode": 5260,
    "city": "Odense S",
    "website": "https://www.bag.dk/brugte-biler",
    "cvr": "26038561",
    "listingCount": 37,
    "profileUrl": "https://www.bilbasen.dk/find-en-forhandler/bilforhandler--id1956"
  },
  "phone": "63132736",
  "location": { "lat": 55.353703, "lon": 10.403891, "zipCode": 5260, "city": "Odense S", "region": "Fyn" }
}
```

#### 🗂 Key output fields

| Field | What it holds |
|---|---|
| `adId` / `url` | The site's own id and the canonical ad URL |
| `make` / `model` / `variant` / `modelYear` / `bodyType` | What the car is |
| `price` | `{ amount, currency }` in DKK, with `priceType` naming the basis |
| `newPriceDkk` | List price when new, where the ad states it |
| `mileageKm` | Odometer as a number |
| `firstRegistered` / `producedAt` | `YYYY-MM`; the site publishes month precision, never a day |
| `batteryCapacityKwh` / `rangeKm` / `consumptionWhPerKm` | Pack size, WLTP range, and real consumption |
| `chargingAcKw` / `chargingDcKw` / `chargingTimeDc1080Min` | Charging figures, where the ad fills them in |
| `powerHp` / `torqueNm` | Both halves of the ad's `286 hk/400 nm` |
| `equipment` | Every fitted extra the ad lists, in the site's own wording |
| `imageUrls` | Every gallery photo at `S1600X1600` (1600×1066), in the ad's order. No dealer logos, no video stills |
| `dealer` | `{ id, name, type, street, zipCode, city, website, cvr, listingCount, profileUrl }` |
| `phone` | The dealer's number, read from their own page |
| `contactEmail` | The dealer's email, when `enrichEmails` ran. `contactWebsite` names the site it came from |
| `location` | `{ lat, lon, zipCode, city, region }` to six decimals |
| `searchMatchCount` | What the site reported for the search this row came from |

#### ❓ FAQ

**How often are the fields actually populated?** Measured over 120 electric dealer ads from 80 dealers in October 2026: price, title, model year, mileage, first registration, battery capacity, range, power, body type, description, equipment, phone, the dealer's CVR and listing count, and the point all at 100%. Consumption 99%, photos 96% (median 18 per ad), dealer website 94%, colour 92%, torque 88%, new price 58%, AC charging 49%, DC charging 46%, DC charge time 8%, production date 2%. Where a field is empty it is empty on the site too.

**Why are the charging figures so patchy?** Because bilbasen prints a dash for them on about half the ads. The fields exist on every ad page; most sellers leave them unfilled. Battery capacity and range, which are the figures that matter for an EV, come through on every car.

**Why does a big search take price bands?** The site serves at most 100 pages of 30 ads for any one query and then simply stops, with no error. The electric dealer segment reports 16 741 ads, so without banding you would get 3 000 of them and no sign that the rest existed. The run probes the site's own count per band and halves any band that still will not fit; on the EV dealer search that came to 12 bands whose counts summed to exactly the 16 741 reported.

**Can I turn banding off?** Pin `priceFrom`/`priceTo` yourself and the search is crawled as given — widening a range the caller chose would change their query.

**Can I get the dealers' email addresses?** Switch on `enrichEmails`. Each dealer's own website is read once per run, however many cars they list, and the best address found is written to `contactEmail`. Measured on 76 Danish dealers in October 2026: 59 published a usable address, 78%. A lookup that finds nothing costs nothing, and a dealer is charged once rather than once per car.

**Why is there no equipment on some rows?** There is. Equipment came back on all 120 ads measured, a median of 39 entries and up to 72.

#### 💬 Support

Issues and feature requests go to the Issues tab on the actor's page.

#### 🤖 For AI Agents & LLM Apps

**Purpose:** scrape bilbasen.dk, Denmark's largest car marketplace, into one flat row per car, including numeric EV figures, the fitted-equipment list, the dealer block and the revealed phone number.

**Output fields:** `adId`, `url`, `title`, `make`, `model`, `variant`, `modelYear`, `bodyType`, `price {amount, currency}`, `priceType`, `newPriceDkk`, `mileageKm`, `firstRegistered`, `producedAt`, `fuel`, `batteryCapacityKwh`, `rangeKm`, `consumptionWhPerKm`, `chargingAcKw`, `chargingDcKw`, `chargingTimeDc1080Min`, `powerHp`, `torqueNm`, `accelerationSec`, `topSpeedKmh`, `towWeightKg`, `weightKg`, `bootLiters`, `doors`, `colour`, `periodicTaxDkk`, `description`, `equipment`, `imageUrls`, `hasVideo`, `dealer {id, name, type, street, zipCode, city, website, cvr, listingCount, profileUrl}`, `phone`, `contactEmail`, `contactWebsite`, `emailEnrichment`, `location {lat, lon, zipCode, city, region}`, `searchMatchCount`, `detailScraped`, `sourceUrl`, `scrapedAt`.

**Behaviours an agent should know:**

- Always set `maxItems` — the electric dealer search alone runs to 16 741 cars.
- Prices are DKK. `priceFrom`/`priceTo` switch off automatic price banding.
- `firstRegistered` and `producedAt` are `YYYY-MM`, not full dates.
- A run that cannot reach the site's reported count ends FAILED by design; set `failOnIncomplete: false` if a partial dataset is acceptable.
- `scrapePhone` costs one request per dealer, not per car, so leaving it on is cheap on a large run.
- `enrichEmails` is off by default and billed per address found, not per car.

### ⚠️ Disclaimer

This scraper collects only publicly available information from bilbasen.dk. It does not log in, bypass any paywall, or collect personal data beyond the business contact details that sellers publish on their own listings. You are responsible for using the output in line with bilbasen.dk's terms and with applicable law, including GDPR where the data concerns identifiable people. Dealer contact details are published by the dealers themselves for the purpose of being contacted about their stock.

### SEO Keywords

bilbasen scraper, bilbasen.dk api, denmark car scraper, danish used car data, bilbasen listings export, scrape danish car marketplace, denmark ev listings, electric car data denmark, bilbasen dealer data, danish car dealer leads, cvr lookup cars, bilbasen price monitoring, used car data denmark, car listings json denmark, bilbasen phone numbers, danish vehicle specifications, ev battery capacity denmark, wltp range data, car market analysis denmark, bilbasen bulk export

# Actor input Schema

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

Bilbasen search or listing URLs to scrape. Supports search pages (https://www.bilbasen.dk/brugt/bil/bmw) and single listings (https://www.bilbasen.dk/brugt/bil/bmw/530e/20-touring-m-sport-aut/6959802). Leave empty to use the filter fields below.

## `make` (type: `string`):

Car make to search for (e.g. BMW, Audi, Toyota, Volkswagen). Used to build the search URL when no startUrls are provided.

## `model` (type: `string`):

Car model to search for (e.g. 3-serien, A4, Yaris). Requires make to be set.

## `priceFrom` (type: `integer`):

Minimum price in DKK.

## `priceTo` (type: `integer`):

Maximum price in DKK.

## `yearFrom` (type: `integer`):

Minimum model year.

## `yearTo` (type: `integer`):

Maximum model year.

## `fuelType` (type: `string`):

Fuel type filter. Leave unset for every fuel.

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

Maximum number of listings to scrape (0 = unlimited). Apify charges per result, so set this conservatively on your first run.

## `includeDetails` (type: `boolean`):

Fetch the full detail page for each listing (seller info, phone, full description, all specs). Disable for faster list-only scraping.

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

Maximum number of search-result pages to scrape (30 listings per page). 0 = unlimited.

## `proxy` (type: `object`):

Proxy configuration. A residential proxy is recommended for reliable access.

## `dealersOnly` (type: `boolean`):

Return only cars listed by a trade seller, skipping private sellers.

## `scrapePhone` (type: `boolean`):

Fetch each dealer's phone number from their own page. The ad page hides it behind a button, so this costs one extra request per dealer — not per car.

## `maxConcurrency` (type: `integer`):

How many ad pages to fetch at once.

## `failOnIncomplete` (type: `boolean`):

End the run as FAILED when it delivers fewer ads than bilbasen reported for the search, instead of returning a short dataset that looks complete.

## `completenessTolerancePercent` (type: `integer`):

How far below the site's own count the run may land before it fails, as a percentage. The larger of this and the absolute tolerance applies.

## `completenessToleranceAbsolute` (type: `integer`):

How many ads the run may miss before it fails. Covers the churn of ads published and withdrawn while the run is in flight.

## `enrichEmails` (type: `boolean`):

Look up each dealer's email address on their own website. Charged only per address actually found, and a dealer is looked up once per run however many cars they list. Measured on 76 Danish dealers: 59 published a usable address.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.bilbasen.dk/brugt/bil"
    }
  ],
  "maxItems": 100,
  "includeDetails": true,
  "maxPages": 0,
  "dealersOnly": false,
  "scrapePhone": true,
  "maxConcurrency": 5,
  "failOnIncomplete": true,
  "completenessTolerancePercent": 1,
  "completenessToleranceAbsolute": 25,
  "enrichEmails": false
}
```

# 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.bilbasen.dk/brugt/bil"
        }
    ],
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("memo23/bilbasen-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.bilbasen.dk/brugt/bil" }],
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("memo23/bilbasen-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.bilbasen.dk/brugt/bil"
    }
  ],
  "maxItems": 100
}' |
apify call memo23/bilbasen-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,memo23/bilbasen-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/iKEbwq8LXvZfiDXG9/builds/FF2SHTFpy7VJ7E2Ry/openapi.json
