# Nationale Vacaturebank Jobs Scraper (`devilscrapes/nationale-vacaturebank-jobs-scraper`) Actor

Scrape Nationale Vacaturebank (nationalevacaturebank.nl) job listings by keyword: title, employer, city and province, contract type, salary range, working hours, education level, dates, full text and vacancy URL. Deduplicated rows.

- **URL**: https://apify.com/devilscrapes/nationale-vacaturebank-jobs-scraper.md
- **Developed by:** [DevilScrapes](https://apify.com/devilscrapes) (community)
- **Categories:** Jobs
- **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>

## Nationale Vacaturebank Jobs Scraper

**💰 $3.00 / 1 000 vacancies**  ·  plus a $0.20 start fee per run  ·  pay per vacancy delivered

*The devil's in the data.* 😈

Pull job listings from Nationale Vacaturebank (nationalevacaturebank.nl), one of the
biggest Dutch job boards, as clean, deduplicated rows: title, employer, city and
province, contract type, salary range, working hours, education and career level,
dates, the full vacancy text and the public vacancy URL. Search by one keyword or
many, and let us page through the results for you.

### 🎯 What this scrapes

Nationale Vacaturebank lists tens of thousands of Dutch vacancies and offers no
anonymous export. This Actor runs a keyword search (for example `python` or
`verpleegkundige`), walks the result pages 100 vacancies at a time, and returns one
row per vacancy: the vacancy id and URL, title and normalised function title,
employer (name, slug, type), work location (city, municipality, province, postal
code, coordinates), contract type, salary minimum, maximum and period when the
advert states one, working hours per week, education level, career level,
categories, industries, start, end and expiry dates, the apply link, the source
feed and the full vacancy text as HTML and as plain text. Each row also records the
keyword that found it and the total number of vacancies the site reports for it.

### 🔥 Features

- 🛡️ **We rotate browser fingerprints** (curl-cffi Chrome, Firefox 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.** The same vacancy id never appears twice in your
  dataset, across pages or across keywords in one run.
- 🧱 **Fault isolation.** A page that cannot be read after its retries is skipped
  and the walk continues; one keyword that fails doesn't end the others. The status
  message tells you what was skipped.
- 💶 **Structured salary.** Minimum, maximum and period are separate numeric
  fields, and stay `null` when the advert does not state a salary.
- 📝 **Full text on tap.** Switch `includeDescription` off for a lighter dataset.

### 💡 Use cases

- **Recruiters and staffing agencies:** watch who is hiring for a role, and where,
  across the Netherlands.
- **Labour-market analysts:** measure vacancy volume, contract mix, salary bands and
  required education per keyword and province.
- **Job aggregators:** feed fresh Dutch vacancies into your own board.
- **HR-tech and salary teams:** track demand for a skill over time with a scheduled
  run.

### ⚙️ How to use it

1. Add one or more search terms to `keywords`, for example `python` and
   `verpleegkundige`.
2. Set `maxPages` (100 vacancies per page) and `maxItems` to bound how much you pull.
3. Run it, then export to JSON, CSV, or Excel, or pull the dataset straight from the
   Apify API.

### 📥 Input

| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| `keywords` | `array<string>` | yes | none | Job titles, skills or employers. Blanks and duplicates are dropped. |
| `maxPages` | `integer` | no | `3` | Result pages per keyword, 100 vacancies each (1-100). |
| `maxItems` | `integer` | no | `100` | Hard cap on total rows across the run (1-10000). |
| `includeDescription` | `boolean` | no | `true` | Include the full vacancy text (HTML and plain text). |
| `proxyConfiguration` | `object` | yes | Apify Proxy | Datacenter by default; see Limitations. |

#### Example input

```json
{
  "keywords": ["python", "verpleegkundige"],
  "maxPages": 2,
  "maxItems": 150,
  "includeDescription": true,
  "proxyConfiguration": { "useApifyProxy": true }
}
```

### 📤 Output

One row per deduplicated vacancy.

| Field | Type | Notes |
|---|---|---|
| `jobId` | `string` | Vacancy UUID, dedupe key. |
| `url` | `string \| null` | Public vacancy URL on nationalevacaturebank.nl. |
| `title` / `functionTitle` | `string` | Vacancy title and normalised function title. |
| `company` / `companySlug` / `companyType` / `companyWebsite` | `string \| null` | Employer or recruiter. |
| `city` / `municipality` / `province` / `postalCode` / `street` / `country` | `string \| null` | Work location. |
| `latitude` / `longitude` | `number \| null` | Work location coordinates. |
| `contractType` / `contractTypes` | `string \| null` / `array` | e.g. Vast, Tijdelijk, Stage. |
| `salaryMin` / `salaryMax` / `salaryType` | `number \| null` / `string \| null` | Null when the advert states no salary. |
| `workingHoursMin` / `workingHoursMax` | `number \| null` | Hours per week. |
| `educationLevel` / `educationLevels` / `careerLevel` | `string \| null` / `array` | e.g. WO, HBO, Ervaren. |
| `categories` / `industries` | `array<string>` | Site categories and industries. |
| `startDate` / `endDate` / `expirationDate` | `string \| null` | ISO 8601 UTC. |
| `applyUrl` / `applyOption` | `string \| null` | Where and how to apply. |
| `origin` / `referenceId` / `recruiterId` | `string \| null` | Source feed identifiers. |
| `numberOfApplies` | `integer \| null` | Applications counted by the site. |
| `description` / `descriptionText` | `string \| null` | Vacancy text as HTML and as plain text. |
| `searchKeyword` | `string` | Keyword that found the vacancy. |
| `totalResults` | `integer \| null` | Vacancies the site reports for that keyword. |
| `pageNumber` / `position` | `integer` | 1-based page and position within the page. |
| `source` | `string` | `nationalevacaturebank`. |
| `scrapedAt` | `string` | UTC ISO-8601 run timestamp. |

#### Example output record

```json
{
  "jobId": "4eedbe40-20f4-4c43-8b74-8b8ac205d627",
  "url": "https://www.nationalevacaturebank.nl/vacature/4eedbe40-20f4-4c43-8b74-8b8ac205d627/data-scientist",
  "title": "Data Scientist",
  "company": "Stedin",
  "companyType": "direct_employer",
  "city": "Rotterdam",
  "province": "Zuid-Holland",
  "country": "Nederland",
  "contractType": "Vast",
  "salaryMin": 5085.0,
  "salaryMax": 5085.0,
  "salaryType": "monthly",
  "workingHoursMin": 40.0,
  "workingHoursMax": 40.0,
  "educationLevel": "WO",
  "careerLevel": "Ervaren",
  "startDate": "2026-10-12T22:00:00Z",
  "endDate": "2026-12-11T23:00:00Z",
  "origin": "jobdigger",
  "searchKeyword": "python",
  "totalResults": 813,
  "pageNumber": 1,
  "position": 1,
  "source": "nationalevacaturebank",
  "scrapedAt": "2026-10-13T08:00:00Z"
}
```

### 💰 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 |
| `result-emitted` | $0.003 | Per deduplicated vacancy written to the dataset |

Example: 1 000 rows = **$3.20 per run** ($0.20 start + 1000 x $0.003). The start
charge applies to every run, including one that matches nothing. No subscription,
no minimum.

### 🚧 Limitations

- **Keyword search only.** Location, category and salary filters are not offered yet.
- **Overlapping keywords overlap.** A vacancy matching two keywords is returned once,
  under the first keyword that found it.
- **Live ordering moves.** Results are sorted newest first, so a new vacancy landing
  mid-run can shift the list; we dedupe by id, so a page may bring fewer new rows.
- **Salary is advert-dependent.** Many vacancies state none; those rows carry `null`.
- **Datacenter proxy by default.** If the site starts refusing it, switch
  `proxyConfiguration` to a residential group with a Dutch exit.
- **A keyword with no vacancies is a success.** It finishes with zero rows and a status
  message naming what was searched.

### ❓ FAQ

**Do I need a Nationale Vacaturebank account or API key?**
No. This reads the public vacancy search.

**How many vacancies can I pull per keyword?**
As many as `maxPages` x 100, up to what the site lists for the keyword. The
`totalResults` column tells you the keyword's full size.

**Why did my run return fewer rows than `maxItems`?**
Either the keyword has fewer vacancies than your cap, `maxPages` stopped the run
first, or another keyword already returned the same vacancies. 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 keyword was blocked and nothing at all could be collected.

**Can I run this on a schedule?**
Yes. Use Apify Schedules to re-run your keywords daily and catch newly posted
vacancies.

### 💬 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/nationale-vacaturebank-jobs-scraper/changelog.md

# Actor input Schema

## `keywords` (type: `array`):

Job titles, skills or employers to search on Nationale Vacaturebank, for example 'python' or 'verpleegkundige'. Each keyword is searched separately; vacancies found by several keywords are returned once.

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

Cap on result pages fetched per keyword (100 vacancies per page).

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

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

## `includeDescription` (type: `boolean`):

Add the complete vacancy text to every row, both as HTML and as plain text. Turn it off for a lighter dataset.

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

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

## Actor input object example

```json
{
  "keywords": [
    "python",
    "verpleegkundige"
  ],
  "maxPages": 2,
  "maxItems": 150,
  "includeDescription": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `datasetItems` (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 = {
    "keywords": [
        "python",
        "verpleegkundige"
    ],
    "maxPages": 2,
    "maxItems": 150,
    "includeDescription": true,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("devilscrapes/nationale-vacaturebank-jobs-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 = {
    "keywords": [
        "python",
        "verpleegkundige",
    ],
    "maxPages": 2,
    "maxItems": 150,
    "includeDescription": True,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("devilscrapes/nationale-vacaturebank-jobs-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 '{
  "keywords": [
    "python",
    "verpleegkundige"
  ],
  "maxPages": 2,
  "maxItems": 150,
  "includeDescription": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call devilscrapes/nationale-vacaturebank-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,devilscrapes/nationale-vacaturebank-jobs-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/L87EGOf3zzJMPkMrE/builds/d1MDpMhCACeJPseRG/openapi.json
