# KVK Company Search Scraper (`devilscrapes/kvk-company-search-scraper`) Actor

Search the Dutch Chamber of Commerce (KVK) Handelsregister by company name or activity keyword and get clean company rows: KVK number, branch number, trade names, legal form, status, address and activity. Deduplicated, with a town filter.

- **URL**: https://apify.com/devilscrapes/kvk-company-search-scraper.md
- **Developed by:** [DevilScrapes](https://apify.com/devilscrapes) (community)
- **Categories:** Lead generation
- **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?

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

<p align="center">
  <img src=".actor/icon.svg" width="160" alt="Devil Scrapes mark" />
</p>

## KVK Company Search Scraper

**💰 $5.00 / 1 000 company records**  ·  plus a $0.20 start fee per run  ·  pay per record delivered

*The devil's in the data.* 😈

Search the Dutch Chamber of Commerce business register (KVK Handelsregister) by company
name or activity keyword and get clean, deduplicated rows: KVK number, branch number,
trade names, legal form, status, registration date, visiting and postal address and the
activity description. Run one query or many, narrow them to a town, and let us page
through the results for you.

### 🎯 What this scrapes

The KVK Handelsregister is the official register of Dutch companies, and its public
search has no export. This Actor runs the same search the kvk.nl search page runs (for
example `bakkerij` or `loodgieter`), walks the results ten records at a time, and returns
one row per register entry: the KVK number and branch number, the registered name, current
and former trade names, the statutory name, legal form (code and Dutch description),
whether the registration is active, whether the entry is a head office or a branch,
registration date, visiting address (street, house number with suffix, postcode, town),
postal address when the register lists one, and the activity description. Each row also
records the query that found it and the number of records the register reports for it.

### 🔥 Features

- 🛡️ **We rotate browser fingerprints** (curl-cffi Firefox, Chrome and Safari
  profiles) when a request is refused, so the retry looks like a different browser.
- 🔁 **We retry with exponential backoff** on rate limits and server errors,
  honouring `Retry-After`. Up to 5 attempts per page.
- 🌐 **We rotate Apify Proxy sessions** on every retry, with a fresh exit each time.
- 🧊 **Run-wide deduplication** on KVK number plus branch number, across pages and across
  queries in one run.
- 🧱 **Fault isolation.** A page that cannot be read after its retries is skipped and the
  walk continues; one query that fails doesn't end the others. The status message tells
  you what was skipped.
- 📍 **Town filter.** Add a `place` and every query is narrowed to that town.

### 💡 Use cases

- **Sales and lead generation:** list every registered business of a type in a town, with
  its KVK number and address.
- **KYC and onboarding:** check that a company exists, is active, and which legal form and
  address it is registered under.
- **Market analysis:** count and map businesses by activity, legal form and town.
- **Data enrichment:** attach a KVK number and registered address to a list of company names.

### ⚙️ How to use it

1. Add one or more search terms to `queries`, for example `bakkerij` and `loodgieter`.
2. Optionally set `place` to a town such as `Utrecht` to narrow every query.
3. Set `maxResultsPerQuery` and `maxResults` to bound how much you pull.
4. Run it, then export to JSON, CSV, or Excel, or pull the dataset straight from the Apify API.

### 📥 Input

| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| `queries` | `array<string>` | yes | none | Company names or activity keywords. Blanks and duplicates are dropped. |
| `place` | `string` | no | none | Town added to every query, for example `Utrecht`. |
| `maxResultsPerQuery` | `integer` | no | `100` | Row cap per query (1-10000). |
| `maxResults` | `integer` | no | `100` | Hard cap on total rows across the run (1-100000). |
| `proxyConfiguration` | `object` | yes | Apify Proxy | Datacenter by default; see Limitations. |

#### Example input

```json
{
  "queries": ["bakkerij", "loodgieter"],
  "maxResultsPerQuery": 100,
  "maxResults": 200,
  "proxyConfiguration": { "useApifyProxy": true }
}
```

### 📤 Output

One row per deduplicated register entry.

| Field | Type | Notes |
|---|---|---|
| `kvkNummer` | `string` | 8-digit KVK number. |
| `vestigingsnummer` | `string` | 12-digit branch number; with `kvkNummer` the dedupe key. |
| `name` | `string \| null` | Registered name shown in the search result. |
| `tradeNames` / `formerTradeNames` | `array<string>` | Current and former trade names. |
| `statutoryName` | `string \| null` | Statutory name of the legal entity, when listed. |
| `legalFormCode` / `legalForm` | `string \| null` | e.g. `BV` and Besloten Vennootschap. |
| `isActive` / `isBranch` | `boolean \| null` | Registration status and branch flag. |
| `registrationType` / `registrationDate` | `string \| null` | e.g. Hoofdvestiging, and the ISO date. |
| `street` / `houseNumber` / `postalCode` / `city` | `string \| null` | Visiting address. |
| `postalAddress` | `string \| null` | Postal address on one line, when listed. |
| `activityDescription` | `string \| null` | Activity description from the register. |
| `recordId` | `string \| null` | Search index id of the record. |
| `searchQuery` | `string` | Effective query that found the record. |
| `totalResults` | `integer \| null` | Records the register reports for the query. |
| `position` | `integer` | 1-based position within the query results. |
| `source` | `string` | `kvk-handelsregister`. |
| `scrapedAt` | `string` | UTC ISO-8601 run timestamp. |

#### Example output record

```json
{
  "kvkNummer": "18078744",
  "vestigingsnummer": "000010349758",
  "name": "Bakkerij Saada",
  "tradeNames": ["Bakkerij Saada"],
  "formerTradeNames": [],
  "legalFormCode": "VOF",
  "legalForm": "Vennootschap Onder Firma",
  "isActive": true,
  "isBranch": true,
  "registrationType": "Hoofdvestiging",
  "registrationDate": "2005-06-01",
  "street": "Verdiplein",
  "houseNumber": "97",
  "postalCode": "5049NR",
  "city": "Tilburg",
  "activityDescription": "Bakkerij",
  "searchQuery": "bakkerij",
  "totalResults": 15940,
  "position": 4,
  "source": "kvk-handelsregister",
  "scrapedAt": "2026-10-13T07:39:44Z"
}
```

### 💰 Pricing

Pay-Per-Event: you pay only when these events fire.

| Event | USD | What it is |
|---|---:|---|
| `actor-start` | $0.20 | One-off start charge per run, charged with the first delivered batch |
| `result-emitted` | $0.005 | Per deduplicated company record written to the dataset |

Example: 1 000 rows = **$5.20 per run** ($0.20 start + 1000 x $0.005). No subscription,
no minimum.

### 🚧 Limitations

- **About a thousand records per query.** The register serves the first part of a
  query's results only (1 000 records were reachable in testing; far deeper requests are
  refused). For wider coverage, split a broad query by town with `place`.
- **Search is by name or keyword.** Lookup by KVK number, postcode-only search and legal-form
  or status filters are not offered yet.
- **Town filter is search text.** `place` is added to the query, so it matches records whose
  searchable text contains that town.
- **Overlapping queries overlap.** A record matching two queries is returned once, under the
  first query that found it.
- **Datacenter proxy by default.** If the register starts refusing it, switch
  `proxyConfiguration` to a residential group with a Dutch exit.
- **A query with no matches is a success.** It finishes with zero rows and a status message
  naming what was searched.

### ❓ FAQ

**Do I need a KVK account or API key?**
No. This reads the public search on kvk.nl.

**How many records can I pull per query?**
Up to `maxResultsPerQuery`, bounded by the register's result window of roughly a thousand
records per query. The `totalResults` column tells you the query's full size.

**Why did my run return fewer rows than `maxResults`?**
Either the query has fewer records than your cap, the register's result window ended it, or
another query already returned the same records. The status message says which.

**What happens if a page is blocked?**
We retry up to 5 times with a fresh browser fingerprint and proxy session each time. If a page
still can't be read it is skipped and counted in the status message. The run ends as failed only
when a query was blocked and nothing at all could be collected.

**Can I run this on a schedule?**
Yes. Use Apify Schedules to re-run your queries and catch newly registered businesses.

### Use with n8n

Call the Actor from the Apify node in n8n, Make or Zapier, pass `queries` and `place` from your
workflow, and read the dataset items back as rows.

### 💬 Your feedback

Found a bug, or need a field we don't return yet? Open an issue on the Actor's Issues tab. We
read every one.

# Changelog

This Actor's version history is a separate document: https://apify.com/devilscrapes/kvk-company-search-scraper/changelog.md

# Actor input Schema

## `queries` (type: `array`):

Company names or activity keywords to look up in the Handelsregister, for example 'bakkerij' or 'loodgieter'. Each query is searched separately; a record found by several queries is returned once.

## `place` (type: `string`):

Narrow every query to one town, for example 'Utrecht'. It is added to the search text, so 'bakkerij' becomes 'bakkerij Utrecht'. Leave empty to search the whole country.

## `maxResultsPerQuery` (type: `integer`):

Cap on records collected per query. The register serves roughly the first thousand records of a query; split broad queries by town for deeper coverage.

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

Hard cap on total deduplicated records emitted across the whole run.

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

Apify Proxy configuration. Datacenter exits are the default; switch to a residential group with a Dutch exit if you see the register refusing them.

## Actor input object example

```json
{
  "queries": [
    "bakkerij",
    "loodgieter"
  ],
  "maxResultsPerQuery": 100,
  "maxResults": 200,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `output` (type: `string`):

All dataset items as JSON.

## `datasetItemsCsv` (type: `string`):

Same data exported to CSV.

## `datasetView` (type: `string`):

Open the run dataset in the Console.

# 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 = {
    "queries": [
        "bakkerij",
        "loodgieter"
    ],
    "maxResultsPerQuery": 100,
    "maxResults": 200,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("devilscrapes/kvk-company-search-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 = {
    "queries": [
        "bakkerij",
        "loodgieter",
    ],
    "maxResultsPerQuery": 100,
    "maxResults": 200,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("devilscrapes/kvk-company-search-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 '{
  "queries": [
    "bakkerij",
    "loodgieter"
  ],
  "maxResultsPerQuery": 100,
  "maxResults": 200,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call devilscrapes/kvk-company-search-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,devilscrapes/kvk-company-search-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/BuJRzaXrqe6khUBDs/builds/eHQjeWsx0vZq7pXFq/openapi.json
