Werk.nl Scraper $1๐ฐ Dutch Job Vacancies, Salaries & Contacts
Pricing
from $1.00 / 1,000 results
Werk.nl Scraper $1๐ฐ Dutch Job Vacancies, Salaries & Contacts
From $1/1K. Scrape job vacancies from werk.nl, the official Dutch government job board by UWV. Uses werk.nlโs vacancy data service for fast, low-cost runs. Returns 40+ fields per job, including title, salary, full description, recruiter contact, application link, and employer profile.
Pricing
from $1.00 / 1,000 results
Rating
0.0
(0)
Developer
Abot API
Maintained by CommunityActor stats
0
Bookmarked
9
Total users
6
Monthly active users
3 hours ago
Last modified
Categories
Share
werk.nl Scraper: Dutch Job Vacancies, Salaries and Recruiter Contacts
werk.nl Scraper turns the official Dutch public employment service job board (UWV's werk.nl) into structured data. It talks directly to werk.nl's own vacancy search and detail services, returning 40+ fields per vacancy, including data the website only shows on the detail page: the full description, salary indication, terms of employment, the complete employer profile and the recruiter contact (name, department, phone and e-mail) when werk.nl publishes them. Search by keyword, location and rich filters, or paste specific vacancy URLs or reference numbers, then export the results to JSON, CSV or Excel, or pull them straight into your app through the API.
Why This Scraper?
- Two ways to find vacancies. Search by keyword and location with rich filters, or paste specific vacancy detail-page URLs or bare reference numbers.
- Detail-page data included. With full vacancy details on (the default), get the complete description, salary indication, terms of employment, the full employer profile and the recruiter contact (name, department, phone and e-mail) when werk.nl publishes them.
- Rich search filters. Narrow by profession, education level, hours per week, working-hours type, driving licence, language, contract type, and country (for EURES cross-border vacancies).
- Flags and flat fields. Apprenticeship, internship and EURES-priority flags, plus flat convenience fields (
jobId,companyName,salaryMin,contactEmail,driversLicenseLabelsand more) next to the nested employer and contact objects. - Built for recurring monitoring. Incremental mode classifies every vacancy as new, updated or unchanged against your previous run (reappeared and expired need Emit expired on plus a complete scan), so a scheduled run returns only what changed.
- Resume large crawls. Continue an interrupted run from its previous run or dataset id without re-collecting or re-billing vacancies already gathered.
- Handles the site's daily maintenance window. A short maintenance window on werk.nl is detected and retried automatically rather than being read as zero results.
Use Cases
- Recruitment agencies and staffing firms: monitor new vacancies in a niche and see who is hiring (check
isAcquisitionNotAppreciatedbefore contacting an employer directly). - Job boards and aggregators: mirror UWV's public vacancy feed into your own site, app or newsletter.
- Labour-market researchers and policy analysts: track salary indications, contract types, hours and professions across regions and time.
- HR and compliance teams: watch for EURES cross-border, apprenticeship or internship vacancies in a specific sector or employer.
Data You Get
Sample shape: values are illustrative placeholders, not from a live record.
| Field | Example |
|---|---|
referenceNumber | 10000001 |
url | "https://www.werk.nl/nl/vacatures/10000001" |
title | "Junior Applicatiebeheerder" |
profession | "ICT-beheerder" |
organisation | "Sample Employer B.V." |
city / postcode | "Zwolle" / "8011AB" |
contractType | "Vast" |
minHours / maxHours | 32 / 40 |
studyLevel | "MBO" |
salaryIndication | "โฌ2.800 - โฌ3.400 per maand" |
createdDate / modifiedDate | "2026-05-01T00:00:00" / "2026-05-14T00:00:00" |
expirationDate | "2026-06-30" |
isApprenticeship / isInternship | false / false |
isEuresPriority | false |
description | "Als Applicatiebeheerder ondersteun je de IT-afdeling bij..." |
employer | { "organizationName": "Sample Employer B.V.", "website": "https://www.example.com", "sector": "Techniek", "address": { "city": "Zwolle" } } |
contact | { "name": "Recruiter Name", "department": "HR", "phoneNumber": "+31 6 00000000", "email": "vacatures@example.com" } |
applicationMethods | [{ "type": "Online formulier", "url": "https://www.example.com/solliciteren" }] |
applyUrl | "https://www.example.com/solliciteren" |
changeType | "NEW" (also UPDATED, UNCHANGED, REAPPEARED, EXPIRED; incremental mode only) |
scrapedAt | "2026-05-15T09:30:00+00:00" |
Turn on Include raw detail payload to also attach the untouched source record under raw. Rows also carry flat fields jobId, companyName, companyWebsite, location, address, requiredEducation, isLearningPosition, requiredExperience, requiredLanguages, otherRequirements, driversLicenses, driversLicenseLabels, employerFunctionTitle, employerVacancyId, contactDepartment, contactName, contactEmail, contactPhone, salary, salaryMin, salaryMax, salaryType, workTimeType, startDate, endDate, plus isRepost and repostOfId when repost detection applies. The dataset has three views: Vacancies (overview), Detail: contact, employer, salary, description and Changes (incremental mode). Records also carry identity and administrative fields such as key, externalReferenceId, employerInternalVacatureId, internalReferenceNumber, source, relevanceScore, workLocationType, workLocationTypeCode, countryCode, foreignCountry, foreignCity, distanceKm, contractTypeCode, contractStartDate, contractEndDate, workingHoursCode, salaryTypeCode, termsOfEmployment, functionName, functionCode, customDescription, descriptionHtml, isByEmployerDirectly and isAcquisitionNotAppreciated. With Incremental mode on, matching records also carry changedFields, firstSeenAt and lastSeenAt.
How to Use
- Pick a mode:
search(by keyword and location, with filters) orurl(paste vacancy links or reference numbers). - Fill in the fields for that mode, and turn Fetch full vacancy details on or off.
- Set Max vacancies to control run size and cost, then click Start.
- Download the dataset as JSON, CSV or Excel, or read it through the API.
Search by keyword and location:
{"mode": "search","keywords": ["accountant"],"location": "Utrecht","fetchDetails": true,"maxItems": 30}
Search with filters, sorted by newest:
{"mode": "search","keywords": ["verpleegkundige"],"educationLevels": ["5"],"contractType": ["1"],"hoursPerWeek": ["5"],"sortBy": "date","maxItems": 50}
Fetch by URL (all shapes): (URL mode accepts the four shapes below; if you leave urls untouched, URL mode now scrapes one example of each shape, capped by Max vacancies, instead of nothing):
| URL shape | Example | Behaviour |
|---|---|---|
| Vacancy detail page | https://www.werk.nl/nl/vacatures/67353746 | Fetches that vacancy |
| Vacancy search page | https://www.werk.nl/nl/vacatures | Runs an all-vacancies search, capped by Max vacancies |
| Search page with query string | https://www.werk.nl/nl/vacatures?trefwoord=developer | Searches that keyword (trefwoord, optionally plaats for the location), capped by Max vacancies |
| Vacancy JSON URL | https://www.werk.nl/werkzoekenden/mijn-werkmap/kia/publiek/zoekenvacatures/api/vacature/67353746 | Fetches that vacancy by its reference number |
A bare reference number also works. Any other URL is skipped with a warning in the log. startUrls is accepted as an alias of urls and is used whenever urls is empty or still holds the example links. In search mode, profession takes a plain job title: it is matched against werk.nl's own profession suggestions, and the run stops with a clear message when nothing matches.
Fetch specific vacancies by reference number:
{"mode": "url","urls": ["https://www.werk.nl/nl/vacatures/10000002","10000003"]}
Recurring monitoring with incremental mode (uncapped so a complete scan is possible):
{"mode": "search","keywords": ["developer"],"incrementalMode": true,"stateKey": "developer-nl-daily","emitExpired": true,"maxItems": 0}
Run it from your code
Python:
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("abotapi/werk-nl-scraper").call(run_input={"mode": "search", "keywords": ["accountant"], "location": "Utrecht"})for vacancy in client.dataset(run["defaultDatasetId"]).iterate_items():print(vacancy["title"], vacancy["organisation"], vacancy["city"])
JavaScript:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });const run = await client.actor('abotapi/werk-nl-scraper').call({ mode: 'search', keywords: ['accountant'], location: 'Utrecht' });const { items } = await client.dataset(run.defaultDatasetId).listItems();
Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.
Resume and recurring updates
- Resume (
resumeFromRunId) continues one interrupted run: paste its run or dataset id and the actor skips every vacancy already collected there, so you don't pay twice. - Incremental mode (
incrementalMode) is for recurring runs over the same search or URL list. Each vacancy is classifiedNEW,UPDATED(withchangedFields), orUNCHANGED(suppressed and not billed unlessemitUnchangedis on).EXPIREDis only produced in a run that scans the whole tracked search or URL list to its natural end: not capped by Max vacancies (set it to0), not resumed, and withemitExpiredon; in url mode every pasted reference must also resolve.REAPPEAREDis produced only for a vacancy that a complete Emit-expired scan marked as no longer present (whether or not that pass actually emitted anEXPIREDrow for it) and that is then found present again in a later run; it never appears before such a pass has happened. Every output field exceptscrapedAtis compared (nested employer and contact data included); numbers compare equal as25or25.0. List field names inignoreFieldsForChanges(dot notation for nested fields, for exampleemployer.name) to keep them in the output but leave them out of the comparison andchangedFields; changing that list never produces a wave of UPDATED rows.stateKeynames or shares the stored state. With incremental mode off, records carry nochangeTypefields.
Send results into your apps (MCP connectors)
Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step after the scrape: the Apify dataset is never changed.
What gets written to the connector: a condensed, human-readable summary of each record, not the full JSON. Each item becomes one entry with a title and its key fields flattened to plain text. The complete record always stays in the Apify dataset.
- Authorize a connector once under Apify โ Settings โ Integrations (Notion, Linear, Airtable, or Apify).
- Select it in the "Pipe results into your apps" input field. (If the picker is empty, you haven't authorized a connector yet.)
- For Notion, also set
notionParentPageUrlto the page where items should be created.
The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.
Input Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
mode | string | search | search (find vacancies by keyword/location/filters) or url (fetch specific vacancy detail pages by URL or reference number). |
keywords | array | prefill ["developer"] | Search term(s): job title, profession, keyword(s) or a reference number. One search runs per keyword. Leave empty to list all vacancies (search mode). |
location | string | (none) | City or postcode to search in, e.g. "Amsterdam" or "1011AB". Leave empty for the whole of the Netherlands. |
profession | array | (none) | Profession category filter, e.g. "Developer", "Verpleegkundige". Values must match werk.nl's profession names. |
educationLevels | array | (none) | Required education level, from Basisonderwijs through WO/doctor. |
hoursPerWeek | array | (none) | Working hours per week band. |
workingHours | array | (none) | Office hours (Kantoortijden) or other (Anders). |
drivingLicense | array | (none) | Required driving licence category (A, B, C, CE, AM). |
language | array | (none) | Required language skill. |
contractType | array | (none) | Contract type by code (1-3) or label: Vast, Mogelijk vast, Tijdelijk. |
country | array | (none) | Country of the work location, for EURES cross-border vacancies. |
sortBy | string | relevance | Result ordering: relevance or date (newest first); the Dutch aliases relevantie and datum are accepted. |
expandResults | boolean | false | Broaden the search to include related vacancies when there are few exact matches (werk.nl's "verruim zoekopdracht"). |
query | string | (none) | Single search term, used only when keywords is empty. |
educationLevel | string | (none) | Single education level by code or Dutch label (e.g. MBO); used only when educationLevels is empty. |
daysOld | integer | 0 | Only return vacancies published or modified within this many days (0 = no limit). Older vacancies are neither returned nor billed. |
descriptionFormat | string | all | Which description variants each row carries: all, text, html or markdown. |
descriptionMaxLength | integer | 0 | Truncate description fields to this many characters (0 = full). |
compact | boolean | false | Drop heavy nested fields (HTML description, employer and contact objects, application methods, extracted URLs, raw detail). |
excludeEmptyFields | boolean | false | Omit empty fields from each row. |
skipReposts | boolean | false | Drop a vacancy that repeats one already seen in the same run (same organisation, title, city and description under another reference number); dropped rows are not billed. Otherwise such rows carry isRepost: true and repostOfId. |
startUrls | array | (none) | Alias of urls; used when urls is empty or still holds the example links. |
urls | array | one example of each URL shape | URL mode only: vacancy detail-page URLs or bare reference numbers. An expired reference is skipped rather than returned as an empty row. |
fetchDetails | boolean | true (on by default) | Include each vacancy's full detail: complete description, salary, recruiter contact and full employer profile. Turn off for a listing-only run. Search mode only; URL mode always fetches the detail page regardless of this setting. |
includeRawDetail | boolean | false | Attach the untouched detail JSON under a raw field on each record. |
maxItems | integer | 20 | Maximum number of vacancies to return across all searches. This is the run's hard cap. Set to 0 for no limit. |
maxPagesPerSearch | integer | (none) | Optional secondary cap on result pages (20 vacancies each) per keyword. Leave empty to page freely until Max vacancies is reached. |
resumeFromRunId | string | (none) | Paste a previous run id or dataset id to continue a large crawl without returning or charging for vacancies already collected there. |
incrementalMode | boolean | false | Turn on for recurring monitoring of the same search or URL set: classifies vacancies as NEW, UPDATED or UNCHANGED against the previous run by default (REAPPEARED and EXPIRED need Emit expired too, see "Resume and recurring updates" above). |
stateKey | string | (none) | Name this monitoring campaign, or deliberately share state across differently-configured runs. Leave empty to derive a key automatically from the search and filter settings. |
emitUnchanged | boolean | false | Also return (and bill) vacancies unchanged since the last run, marked UNCHANGED. |
emitExpired | boolean | false | Also return (and bill) vacancies no longer found, marked EXPIRED. Only produced in a run that scans the whole tracked search or URL list to its natural end: not capped by Max vacancies (set it to 0), not resumed, and in url mode with every reference resolved. |
ignoreFieldsForChanges | array | [] | Incremental mode only: field names (dot notation for nested fields) left out of change detection and changedFields. They stay in the output. |
proxy | object | {"useApifyProxy": true} | Connection settings. The default is the cheapest option; the actor automatically escalates to a more reliable connection only if the default is being refused. |
mcpConnectors | array | (none) | Optional: send a condensed summary of each record to apps you authorized under Integrations. |
notionParentPageUrl | string | (none) | Notion connector only: page under which item pages are created. |
maxNotifyListings | integer | 50 | Cap on items written to each connector per run. Does not affect the dataset. |
Output Example
Sample shape: values are illustrative placeholders, not from a live record.
{"referenceNumber": 10000001,"url": "https://www.werk.nl/nl/vacatures/10000001","externalReferenceId": "SMP-2026-0142","source": "JIT","title": "Junior Applicatiebeheerder","profession": "ICT-beheerder","relevanceScore": 87,"createdDate": "2026-05-01T00:00:00","modifiedDate": "2026-05-14T00:00:00","expirationDate": "2026-06-30","city": "Zwolle","postcode": "8011AB","workLocationType": "Vaste werklocatie","workLocationTypeCode": 1,"contractType": "Vast","contractTypeCode": 1,"minHours": 32,"maxHours": 40,"studyLevel": "MBO","salaryTypeCode": 1,"salaryIndication": "โฌ2.800 - โฌ3.400 per maand","termsOfEmployment": "Marktconform salaris, reiskostenvergoeding en 25 vakantiedagen.","functionName": "Applicatiebeheerder","description": "Als Applicatiebeheerder ondersteun je de IT-afdeling bij het beheren en optimaliseren van bedrijfsapplicaties.","descriptionHtml": "<p>Als Applicatiebeheerder ondersteun je de IT-afdeling bij het beheren en optimaliseren van bedrijfsapplicaties.</p>","isApprenticeship": false,"isInternship": false,"isEuresPriority": false,"isByEmployerDirectly": true,"organisation": "Sample Employer B.V.","employer": {"organizationName": "Sample Employer B.V.","website": "https://www.example.com","sector": "Techniek","address": { "street": "Voorbeeldstraat", "houseNumber": "12", "postcode": "8011AB", "city": "Zwolle" }},"contact": {"name": "Recruiter Name","department": "HR","phoneNumber": "+31 6 00000000","email": "vacatures@example.com"},"applicationMethods": [{ "typeCode": 1, "type": "Online formulier", "url": "https://www.example.com/solliciteren" }],"applyUrl": "https://www.example.com/solliciteren","scrapedAt": "2026-05-15T09:30:00+00:00"}
Plan Requirement
No residential proxy is required. In our tests werk.nl served the search and detail data through the default Apify datacenter connection with no block observed, so the default proxy setting works as is. If the default connection is refused, the actor escalates automatically to a backup connection and logs: "Served over the backup connection (the default connection was unavailable)." When you pick the residential group and your plan does not include it, the log reads "The selected connection is unavailable to this run. Apify free plans do not include residential proxies." and the run continues on the backup connection. If no page could be loaded at all, the run ends as failed with one of these messages: "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required", "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested werk.nl pages", or "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later." Failing loudly replaces an empty success.
FAQ
How much does it cost?
You pay per vacancy returned, plus a surcharge for each vacancy whose detail page was actually fetched. In search mode this surcharge only applies when Fetch full vacancy details is on. In URL mode, a pasted vacancy's detail page is always fetched, since URL mode has no listing-only option, so its surcharge applies whenever the reference resolves, independent of the Fetch full vacancy details setting. A reference number that cannot be resolved (for example an expired vacancy) is never pushed or charged. Charges are: one actor-start event per run, one result event per vacancy pushed to the dataset, and one enrich event per vacancy whose detail page was fetched (the enrich event is charged once the vacancy is committed to your results). The Pricing tab shows current rates. Use Max vacancies to cap the cost of any run.
Is it legal to scrape werk.nl?
This actor collects only publicly available vacancy information from werk.nl, including recruiter contact details that employers themselves publish on public vacancy pages for the purpose of receiving applications. You are responsible for how you use the data: follow werk.nl's terms, the privacy law that applies to you (including how you store and use any contact details), and get legal advice if you plan commercial redistribution.
Can I get only new or changed vacancies on a schedule?
Yes. Schedule the actor from the Schedules tab and turn on Incremental mode. Each run then returns only new and updated vacancies for the same search or URL set (plus reappeared ones, only once an Emit expired pass has marked them gone), and unchanged ones are not billed unless Emit unchanged is on.
Why did my run finish with no vacancies instead of failing?
If every request to werk.nl is refused, or the site's daily maintenance window is active for the whole run, the run still finishes successfully with an empty dataset: it logs an error and sets a status message (in search mode, that no vacancies were returned; in url mode, that some or all references could not be resolved, which can also mean they are simply expired), rather than treating "nothing found" as an error. Run it again in a few minutes, and check that a connection is configured if it persists. A run deliberately fails outright when it cannot make sense of your own input, such as an unreadable resumeFromRunId, or incrementalMode combined with resumeFromRunId while a saved state already exists for that search; an unhandled error elsewhere in the run also fails it.
Does REAPPEARED happen automatically in incremental mode?
No. REAPPEARED is only produced for a vacancy that a prior run with Emit expired on marked as no longer present, after fully scanning the tracked search or URL list to its natural end (not capped, not resumed), and that is then found present again in a later run. On a fresh state key, or on a search that has never had a complete Emit-expired pass, you will only see NEW and UPDATED (plus UNCHANGED if Emit unchanged is on).
Can I use it with AI agents or MCP?
Yes. Call it from any Apify integration or MCP client, and use the connector field to push a summary of each vacancy into Notion, Linear or Airtable.
๐ Want more jobs data?
Pair this actor with these related scrapers from the same team:
| ๐ผ HH.ru Jobs Scraper Scrape HH.ru job listings with 50+ structured fields. Search by filters or URLs and... | ๐ท๏ธ Marktplaats.nl Scraper Scrape Marktplaats.nl listings by keyword or URL. Extract title, price, condition... |
| ๐ผ Jobs.cz Scrape Czech job listings from Jobs.cz. Search builder (keyword, location, field, salary... | ๐ผ Cwjobs UK Scrape tech and IT jobs from CWJobs.co.uk into clean, structured data. Search by keyword... |
| ๐ผ France Travail Scraper Scrape job offers from France Travail with titles, companies, locations, salaries... | ๐ Funda in Business Scraper Scrape Funda in Business commercial listings: offices, retail, industrial, catering and... |
๐ Browse all abotapi scrapers
๐ฌ Support
- ๐ Found a bug or a missing field? Open a ticket on the Issues tab. We usually reply within hours.
- ๐ ๏ธ Need another site, extra fields or a private build? Email contact@abotapi.com.
- โญ Enjoying it? A quick review on the actor page helps other users find it.