# DBA.dk Scraper (`s-r/dba-scraper`) Actor

- **URL**: https://apify.com/s-r/dba-scraper.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** E-commerce, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## DBA.dk Scraper

Search Denmark's largest classifieds site and get every ad back as a row: asking price in kroner, the seller's town, how long the ad has been up, whether it ships, and whether the item is used or new.

No login, no cookie, no API key. It reads the ordinary search page a Danish visitor sees, from an exit inside Denmark, so the results are the ones DBA actually serves that market.

### What you get

- **Price as a number**, parsed out of Danish formatting. `9.800 kr.` is nine thousand eight hundred, not nine point eight, and the space before `kr.` is a non-breaking one that breaks naive parsers
- **Wanted ads separated from offers.** Two thirds of the cards on a cheapest-first search are not priced listings at all: `Gives væk` is a giveaway and `Ønsker at købe` is someone *buying*. `price_type` names which, so a wanted ad never lands in your price average
- **Seller town on every row**, which is what turns a price list into a pickup decision
- **Ad age in DBA's own wording** (`14 t.`, `5 dage`, `1 min.`), so you can tell a fresh listing from one that has been sitting for a week
- **Condition and availability** joined from the page's own structured data, with the join rate reported rather than assumed
- **`buy_now` and `shipping_ready` flags** for DBA's Køb nu instant purchase and Fiks færdig managed shipping, which decide whether an ad is transactable or just a phone number
- **Paid placements flagged**, not silently mixed in. `is_promoted` marks them, and because DBA repeats them across pages the walk deduplicates on ad id
- **The search URL from your browser works as input**, filters and all
- **A run summary that says what happened**: pages fetched, ads returned, how many carried a price, how many were enriched

### Why DBA is worth reading

DBA is where Denmark buys and sells second-hand. It is the Danish equivalent of Marktplaats or Kleinanzeigen, and for a large set of categories, bikes, tools, furniture, phones, it is the only liquid market in the country. If you want to know what a used cargo bike actually sells for in Aarhus, DBA is the answer and there is no second source.

The catch is that DBA moved onto Schibsted's recommerce platform, and the move quietly broke the URL everyone had bookmarked. The old `/soeg/?soegeord=` form still returns HTTP 200 and a megabyte of ads, but on the new path the search term is ignored and what comes back is the homepage. Nothing in the response says so. A scraper built on the old URL returns a full dataset of the wrong ads, and there is no error to catch. This actor uses the canonical path, which is the difference between a working walk and a silent one.

### Input

| Field | Type | Required | Default | What it does |
|---|---|---|---|---|
| `query` | string | one of the two | `cykel` | What to search for. Danish terms work best |
| `url` | string | no | – | A dba.dk search URL to read instead. Takes precedence |
| `sort` | select | no | `relevance` | Relevance, newest, or price ascending/descending |
| `price_min` | integer | no | – | Lower bound in kroner |
| `price_max` | integer | no | – | Upper bound in kroner |
| `limit` | integer | no | `60` | Ads to return, 1 to 1000. A page carries about 54 |
| `retries` | integer | no | `3` | Retry attempts per page |

### Output

```json
{
  "position": 4,
  "item_id": "24515848",
  "url": "https://www.dba.dk/recommerce/forsale/item/24515848",
  "title": "TILBUD – CENTURION HELIUM 2026",
  "subtitle": "Centurion",
  "price": 4890.0,
  "price_text": "4.890 kr.",
  "price_type": "fixed price",
  "currency": "DKK",
  "location": "Brønshøj",
  "posted_label": "7 min.",
  "buy_now": false,
  "shipping_ready": false,
  "is_promoted": false,
  "badges": null,
  "condition": "UsedCondition",
  "availability": "InStock",
  "description": "Centurion Helium 2026, ny model",
  "image": "https://images.dbastatic.dk/dynamic/default/item/...",
  "enriched": true,
  "query": "cykel"
}
```

### Use cases

**Pricing your own second-hand stock.** Run your category sorted by newest, take the median of `price`, and you have the number the market is actually asking this week rather than what a guide says. Because `posted_label` is on every row you can weight fresh listings higher, which matters in categories where prices move.

**Watching a specific model.** Schedule a narrow query with `price_max` set to what you would pay, and every run returns only the ads worth acting on. `buy_now` tells you which of them you can buy immediately instead of negotiating.

**Regional price differences.** `location` on every row means you can group by town and see what a Copenhagen listing costs against the same item in Jutland. That spread is the arbitrage that second-hand traders live on, and it is invisible from the site's own interface.

**Supply monitoring for a category.** Run the same query daily and count rows. A rising count means supply is building and prices are about to soften. Diffing on `item_id` between runs tells you what is new and what sold.

**Finding dealers among the private sellers.** Ads with `shipping_ready` and `buy_now` set are overwhelmingly traders rather than individuals. Filtering on those two flags is a cheap way to separate the professional supply from the private one.

### How it compares

| | this actor | `blackfalcondata/dba-listings-scraper` | the other seven DBA actors |
|---|---|---|---|
| Per 1.000 ads | **$1,20** | $2,00 | mostly no per-item rate at all |
| Actor-start fee | **$0,002** | $0,01 | $0,00005 to $0,005 |
| Uses the working search URL | **yes** | not verifiable | not verifiable |
| Price parsed for Danish formatting | **yes** | – | – |
| Seller town | **yes** | – | – |
| Condition and availability | **yes** | – | – |
| Paid placements flagged and deduplicated | **yes** | – | – |
| Monthly users | new | 3 | 1 to 2 each |

Honest about the other side: `blackfalcondata` has 1.470 runs behind it and this actor has none, which is a real signal and the only one in that table that favours them. Nine DBA actors exist and none has broken three monthly users, so nobody in this category has proven anything yet.

### Pricing

Two events. `run_start` costs $0,002 per run. `listing` costs $0,0012 per ad written to the dataset, which is $1,20 per 1.000. Pages that stay blocked or return nothing never reach the dataset and are never billed. All pricing is pay-per-event, with no per-compute-unit charges.

### Limits and gotchas

- **The legacy search URL is a trap.** `dba.dk/soeg/?soegeord=x` returns 200 and a page full of ads, but on the current platform the search term is dropped and those are the homepage's ads. If you paste a URL into `url`, use the `/recommerce/forsale/search?q=` form, which is what the site's own canonical link points at.
- **A page carries about 54 ads, and one of them is usually paid.** Promoted ads repeat across pages, so the ad count you get back is slightly lower than pages × 54 after deduplication. That is correct behaviour, not loss.
- **Condition comes from structured data joined on title.** The structured block carries no ad id, so ads with duplicate titles (two ads both called "Cykel") stay unenriched rather than being guessed at. The summary reports how many rows were enriched.
- **A third of cards are not priced offers.** The price row holds an amount, `Gives væk` (a giveaway, reported as 0 kr.) or `Ønsker at købe` (a wanted ad, where the poster is buying). `price_type` distinguishes all three plus `not stated`. Filter to `fixed price` before averaging anything.
- **Cheapest-first is mostly giveaways.** DBA sorts free items ahead of paid ones, so `price_asc` on a broad term returns a page of `Gives væk` rows. That is the site's ordering, not a parse failure; the run summary's `priceStates` shows the split.
- **DBA is Denmark only.** Results are Danish, because the site localises by country. There is no country input, because there is no other country.
- **Ad age is a label, not a timestamp.** DBA prints `14 t.` rather than a date, so `posted_label` is returned verbatim rather than converted into a time you would have to trust.

### FAQ

**Can I scrape DBA.dk without an API key?**
Yes. The search page is served to anonymous visitors and that is what this actor reads. There is no credential field in the input.

**Why does my search return the wrong ads?**
Almost certainly the legacy URL. `/soeg/?soegeord=` no longer searches on DBA's current platform, and it fails silently. Pass a search term in `query` and let the actor build the URL, or use the `/recommerce/forsale/search?q=` form.

**How many ads can I get in one run?**
Up to 1.000, which is about 19 pages. Beyond that, narrow the query or add a price band; deep pagination on a broad term returns increasingly stale ads anyway.

**Does it return the seller's name or phone number?**
No. The search page does not carry them, and this actor reads only the search page. It returns the seller's town, which is what the card shows.

**Are prices negotiable prices or final?**
They are asking prices, and only where `price_type` is `fixed price`. `buy_now` is the flag that tells you the seller has committed to a fixed price you can pay immediately.

### Related Actors

- [Marktplaats Scraper](https://apify.com/s-r/marktplaats-scraper) — the same job for the Dutch classifieds market
- [Willhaben Scraper](https://apify.com/s-r/willhaben-scraper) — Austrian classifieds across four verticals
- [Vinted Scraper](https://apify.com/s-r/vinted-scraper) — second-hand fashion across nine European markets

# Actor input Schema

## `query` (type: `string`):

What to search for on dba.dk. Danish terms work best. Optional if you pass a search URL instead.

## `url` (type: `string`):

A dba.dk search URL to read instead of a search term. Paste it from your browser after applying any filters. Takes precedence over the search term.

## `sort` (type: `string`):

How to order the ads.

## `price_min` (type: `integer`):

Lower price bound in Danish kroner.

## `price_max` (type: `integer`):

Upper price bound in Danish kroner.

## `limit` (type: `integer`):

How many ads to return, 1 to 1000. A page carries roughly 54.

## `retries` (type: `integer`):

Retry attempts per page, each with a rotated user agent and TLS fingerprint.

## Actor input object example

```json
{
  "query": "cykel",
  "url": "https://www.dba.dk/recommerce/forsale/search?q=iphone",
  "sort": "relevance",
  "limit": 60,
  "retries": 3
}
```

# Actor output Schema

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

One row per classified ad.

## `summary` (type: `string`):

Ads returned, pages fetched, how many carried a price and a location, and how many were enriched from the page's structured data.

## `errors` (type: `string`):

Per-page failures with a code and a redacted message.

# 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 = {
    "query": "cykel",
    "limit": 60,
    "retries": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/dba-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 = {
    "query": "cykel",
    "limit": 60,
    "retries": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/dba-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 '{
  "query": "cykel",
  "limit": 60,
  "retries": 3
}' |
apify call s-r/dba-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,s-r/dba-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/MzwIMBt3bFbGu5lcz/builds/PpzUG2vPztBsbUEXo/openapi.json
