# Krak Scraper — Danish Business Directory Leads (Virksomheder) (`worktech/krak-scraper`) Actor

Scrape Danish business leads from krak.dk and degulesider.dk: company name, phone, email, address, postal code, city and website. Search by keyword and city (elektriker Aarhus) and export B2B leads to JSON, CSV or Excel. Pay per result — $2 per 1,000 records.

- **URL**: https://apify.com/worktech/krak-scraper.md
- **Developed by:** [Per Schondell](https://apify.com/worktech) (community)
- **Categories:** Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 business records

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/platform/actors/running/actors-in-store#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

## Krak Scraper — Danish Business Directory Leads (Virksomheder)

The **krak scraper** that turns krak.dk **and degulesider.dk** — Denmark's two Yellow Pages sites — into a clean B2B lead list: company name, **phone number, address, postal code, city, website** and (optionally) **email** for any business category in any Danish town. A true **Danish business directory scraper** and a practical **proff.dk alternative** for lead generation: type `elektriker` + `Aarhus`, press Start, export **Denmark B2B leads** to JSON, CSV, or Excel.

Both sites are run by the same company on the same platform and hold the same company data, so **one actor covers both** — pick the directory in the input, or paste start URLs from either.

**Business listings only** — the actor collects company records, not private persons.

### What can you do with it?

- **Get Danish business leads with phone numbers** — *find virksomheder med telefonnummer* — every record carries the phone shown on the directory, normalized for dialers and CRMs
- **Export krak.dk and degulesider.dk search results to CSV or Excel** — *hent virksomhedsliste til Excel* — one clean row per company, stable columns, ready for import
- **Build B2B prospect lists by trade and city** — *find alle elektrikere i Aarhus* — `tømrer` in Odense, `revisor` in København, `VVS` nationwide
- **Scrape company websites and addresses** — *find hjemmeside og adresse på virksomheder* — the company's own website is matched straight off the result page, no extra cost
- **Enrich leads with email** — *find e-mailadresse på virksomheder* — opt-in mode opens each company's detail page, where the directory keeps the email
- **Feed AI agents and pipelines** — structured JSON output via API, webhooks, or the Apify MCP server, so agents can call this scraper as a tool

### Pricing

**$2 per 1,000 business records** ($0.002 per record). You pay only for records actually delivered to your dataset — a blocked or empty search costs nothing.

| Scenario | Records | Run cost |
|----------|---------|----------|
| Electricians in Aarhus | ~500 | ~$1 |
| One trade across the 20 largest Danish cities | ~3,000 | ~$6 |
| Nationwide category sweep | ~10,000 | ~$20 |

#### Listing mode vs enriched mode — read this before switching on email

Residential proxy traffic is billed by Apify on **your** account, separately from the actor price. The two modes are worlds apart there (measured on the sibling eniro.se actor, which runs the identical platform):

| Mode | Fields | Proxy data | Approx. proxy cost |
|------|--------|-----------|--------------------|
| **Listing** (default) | name, phone, address, postal code, city, **website**, profile URL | ~0.017 MB/record | **~$0.45 per 1,000 records** |
| **Enriched** (`fetchContactDetails: true`) | + email from each company's detail page | ~1.89 MB/record | **~$18.75 per 1,000 records** |

Enriched mode is fully supported but **opt-in**, and it costs far more in proxy traffic than the actor itself. Most people do not need it: **many company websites are already in listing mode**, because the directory puts them on the result page. Switch on **Fetch email & website** only when you specifically need email addresses, and cap `maxResultsPerSearch` for the first enriched run.

### How to scrape krak.dk (step by step)

1. Click **Try for free** — you need a free Apify account.
2. Leave **Directory to scrape** on `krak.dk`, or switch it to `degulesider.dk`.
3. Enter one or more **Search terms** in Danish (e.g. `elektriker`, `tømrer`, `VVS`).
4. Enter **Locations** (e.g. `København`, `Aarhus`, `Odense`) — or leave empty to search all of Denmark.
5. Optionally adjust **Max results per search**. Leave **Fetch email & website** off unless you need emails (see the proxy-cost table above — roughly $18.75 per 1,000 records of proxy traffic on your account).
6. Click **Start**, then open the **Dataset** tab and export as JSON, CSV, Excel, HTML, or XML.

Prefer pasting URLs? Put krak.dk or degulesider.dk listing pages straight into **Start URLs** instead of search terms — both hosts can be mixed in one run, and each record records which one it came from.

#### Input example

```json
{
  "site": "krak.dk",
  "searchTerms": ["elektriker", "tømrer"],
  "locations": ["Aarhus", "Odense"],
  "maxResultsPerSearch": 200,
  "requestDelayMs": 5000,
  "fetchContactDetails": false,
  "includeSoleTraders": false
}
```

#### Output example (one dataset record)

```json
{
  "name": "Saabye El",
  "phone": "40000001",
  "email": "kontakt@saabye-el.dk",
  "address": "Bredkildegade 10A",
  "postalCode": "8240",
  "city": "Risskov",
  "categories": ["elektriker"],
  "website": "http://www.saabye-el.dk",
  "profileUrl": "https://www.krak.dk/saabye+el+risskov/65940853/firma",
  "site": "krak.dk",
  "sourceUrl": "https://www.krak.dk/elektriker+aarhus/firmaer",
  "searchTerm": "elektriker",
  "location": "Aarhus",
  "scrapedAt": "2026-08-18T10:30:00+00:00"
}
```

Missing values are explicit `null`, so CSV and Excel exports always have the same columns.

#### Run status and the run SUMMARY

Every run writes a `SUMMARY` record to the run's key-value store, and the run status follows one rule: **a run that delivered rows succeeds.** If some searches burned while others delivered, the run is **SUCCEEDED** with those failures listed in the SUMMARY — so a partial run is never discarded and re-run, and you are never charged twice for the same rows.

| SUMMARY field | What it means |
| --- | --- |
| `recordsPushed` | Rows delivered to the dataset **and charged** — the two are always equal, and this **includes rows delivered by a search that burned later on**. |
| `failures` | One entry per search that burned: the search term, the location, the host and the named error. |
| `failedCombinations` | How many searches are in `failures`. |
| `listingsSeen` | Listings the directory showed, before filtering — what makes a thin dataset explainable. |
| `soleTradersFiltered` | Of those, dropped as sole traders listed under a personal name (set `includeSoleTraders` to keep them). |
| `unreadableEntriesSkipped` | Of those, dropped because they carried no readable name at all — the schema-break signal. |

The run is marked **FAILED** when it delivered nothing at all — and then the status message says why — and in one other case: if contact enrichment dies platform-wide (a detail-page wall), the run aborts and is failed even though rows were already delivered. In that case the SUMMARY still reports — and you were still charged for — everything delivered up to the abort, so **do not re-run it blind**: check `recordsPushed` first.

### Use it from code or AI agents

```bash
curl -X POST "https://api.apify.com/v2/acts/worktech~krak-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"site":"krak.dk","searchTerms":["elektriker"],"locations":["Aarhus"],"maxResultsPerSearch":100}'
```

The actor also works with n8n, Make, Zapier, webhooks, and the **Apify MCP server** — its structured output is designed to be consumed by AI agents directly.

### Why this scraper?

- **Two directories, one actor** — krak.dk and degulesider.dk run on the same platform and hold the same records. You do not need two subscriptions, two runs, or two mental models, and every record names the host it came from.
- **The URL scheme that actually works** — the directory silently ignores `?where=` and `?page=` query parameters and serves the same national top-25 regardless. This actor uses the path-based scheme (`/elektriker+aarhus/firmaer/2`) that the site's own `rel="canonical"` and `rel="next"` tags declare, so city filtering and pagination really apply.
- **Danish company names handled properly** — Danish legal forms (`ApS`, `A/S`, `I/S`, `K/S`) and Danish trade words are both recognised, so real companies like `Saabye El` and `Harlev El` are not thrown away as private persons by a business-only filter written for another language. Six of 25 companies on our first captured page were lost that way before this was fixed.
- **Pagination that actually stops** — this platform advertises a "next page" on every result page, even the last one. The actor ends a search when a page comes back with no companies, instead of grinding through empty pages on your proxy bill.
- **Cloudflare-aware by design** — anti-detect Firefox (Camoufox) with locale/geo alignment, cookie-consent handling, paced fresh-tab detail fetches, and local exponential backoff on blocks.
- **No silent failures** — if the directory changes its page structure, the run FAILS with a named error instead of quietly handing you an empty dataset and a proxy bill. A blocked search surfaces as a named error in the run summary instead of quietly returning nothing, and a sustained contact-page wall aborts the run rather than delivering half-empty records. You are never billed for records you didn't receive: data is pushed before it is charged.

Also from worktech: [Eniro Scraper](https://apify.com/worktech/eniro-scraper) for Sweden, [Gulesider Scraper](https://apify.com/worktech/gulesider-scraper) for Norway, and [Lean Shopify Scraper](https://apify.com/worktech/lean-shopify-scraper).

### FAQ

**What is the difference between krak.dk and degulesider.dk?**
Branding, mostly. They are run by the same company on the same platform, and the same search returns the same companies with the same company ids on both. Pick either — the actor handles both identically and tags each record with its source.

**Is this a proff.dk alternative for lead generation?**
Yes in outcome: both cover Danish businesses. This actor scrapes the Yellow Pages sites, whose result pages already expose phone, address and (for many companies) the company's own website — which is what makes cheap bulk lead lists possible.

**Do I get email addresses?**
When the directory has one, yes — but only in enriched mode. Email lives on the company detail page, so switch on **Fetch email & website** (adds roughly $18.75 per 1,000 records of residential-proxy cost on your account). Coverage varies by trade — not every Danish business publishes an email.

**Do I need enriched mode to get websites?**
No. Many company websites come out of the default listing mode at no extra proxy cost. Enriched mode is about **email**, and it fills in websites the result page did not show.

**Can I export the leads to CSV or Excel?**
Yes. Every run produces a dataset downloadable as CSV, Excel, JSON, HTML, or XML from the Apify Console, or via API.

**Does it collect private persons?**
No. The actor is business-only by policy. Sole-trader businesses listed under a personal name (common for tradespeople) are excluded by default and only included if you switch on **includeSoleTraders**.

**What should I put in Search terms — Danish or English?**
Danish, matching the directory's own categories: `elektriker` (electrician), `tømrer` (carpenter), `VVS` (plumbing), `tandlæge` (dentist), `revisor` (accountant), `frisør` (hairdresser).

**Why is the run slower than a plain HTTP scraper?**
Both sites sit behind Cloudflare bot protection. The actor uses a real anti-detect browser with human-like pacing — that's what makes the data come out correct and complete. The default listing-only mode is the fast one; enriched mode adds one detail-page load per company.

**Is it legal to scrape krak.dk?**
The actor collects publicly visible business information (the same data any visitor sees) and no private-person data. Whether scraping fits your use case depends on your jurisdiction and the site's terms — when in doubt, consult a lawyer.

### Limitations

- **Email coverage depends on the directory** — companies without a published email simply have `email: null`, in both modes.
- **Requires Danish residential proxies for reliable runs** — the default proxy configuration is Apify residential, country DK. Datacenter proxies will get challenged far more often.
- **Detail-page enrichment (opt-in) adds one page load per company** — expect roughly 3–7 seconds and ~1.89 MB of proxy traffic per enriched record.
- **Hyphenated `El-` names are withheld when ambiguous.** `El-Installation` (a trade compound) and `El-Sayed` (a family name) look identical to any rule. When the second part is not a recognised trade word the record is treated as a private person and withheld — publishing an individual is a privacy breach, withholding a company is not. Switch on **includeSoleTraders** to get them.
- **Pagination is capped at 50 pages (~1,250 records) per search combination** as a runaway guard — split very large categories by city to get everything.
- **A search returns companies from a wider area than the town you typed.** The directory pads thin result sets with nearby businesses — a 9-result search still fills 25 rows per page and keeps going. Each record carries its own `city` and `postalCode` so you can filter after export.
- **Running both directories doubles the rows, not the coverage** — they hold the same data. Scrape one, unless you specifically want both source URLs.

### Status

**v0.1** — sibling of the production Eniro Scraper for Sweden, which runs on the same Eniro Group platform. Unit tests pass offline against pages captured live from krak.dk *and* degulesider.dk, including a real Cloudflare challenge page.

# Actor input Schema

## `site` (type: `string`):

Both Danish Yellow Pages sites run on the same platform and hold the same company data. krak.dk is the default; degulesider.dk is the consumer-facing brand. Start URLs always keep their own host, whatever is selected here.

## `searchTerms` (type: `array`):

Business categories or keywords to search, in Danish (e.g. elektriker, tømrer, VVS, revisor). Each term is combined with every location.

## `locations` (type: `array`):

Danish cities or towns to search in (e.g. København, Aarhus, Odense). Leave empty to search all of Denmark.

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

Paste krak.dk or degulesider.dk listing URLs directly, e.g. https://www.krak.dk/elektriker+aarhus/firmaer — used instead of or in addition to search terms. Both hosts may be mixed in one run.

## `maxResultsPerSearch` (type: `integer`):

Cap on business records per search term x location combination. 0 = no cap (scrapes every result page).

## `requestDelayMs` (type: `integer`):

Politeness delay between paginated listing requests. Lower is faster but raises the chance of Cloudflare rate blocks.

## `fetchContactDetails` (type: `boolean`):

Email lives only on each company's detail page. COST NOTE: this fetches one detail page per company through the residential proxy — measured at roughly 1.89 MB per record, about $18.75 per 1,000 records of Apify proxy cost on YOUR account, on top of the $2/1,000 actor price. Off by default: the listing-only run delivers name, phone, address, postal code, city and (for many companies) the website at roughly $0.45 per 1,000 records of proxy cost.

## `includeSoleTraders` (type: `boolean`):

The directory lists some sole-trader businesses under the owner's personal name (e.g. a tradesperson listed as 'Mette Sørensen' rather than as a company). Off by default: only records with a company-style name are kept.

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

krak.dk and degulesider.dk sit behind Cloudflare — Danish residential proxies are strongly recommended.

## Actor input object example

```json
{
  "site": "krak.dk",
  "searchTerms": [
    "elektriker"
  ],
  "locations": [
    "Aarhus"
  ],
  "maxResultsPerSearch": 100,
  "requestDelayMs": 5000,
  "fetchContactDetails": false,
  "includeSoleTraders": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "DK"
  }
}
```

# Actor output Schema

## `businessRecords` (type: `string`):

No description

## `runSummary` (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 = {
    "searchTerms": [
        "elektriker"
    ],
    "locations": [
        "Aarhus"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "DK"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("worktech/krak-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 = {
    "searchTerms": ["elektriker"],
    "locations": ["Aarhus"],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "DK",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("worktech/krak-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 '{
  "searchTerms": [
    "elektriker"
  ],
  "locations": [
    "Aarhus"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "DK"
  }
}' |
apify call worktech/krak-scraper --silent --output-dataset

```

## MCP server setup

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