# VDAB Job Scraper - Belgium Flanders Vacancies & Employers (`scrapersdelight/vdab-jobs-scraper`) Actor

From $0.50 per 1,000 vacancies. Scrape VDAB, Flanders' public employment service: all 207,809 live Belgian vacancies by keyword, city, radius, region, job domain, contract, diploma, licence. Optional detail adds employer website, named hiring contact, apply/ATS URL. New-vacancy monitor + alerts.

- **URL**: https://apify.com/scrapersdelight/vdab-jobs-scraper.md
- **Developed by:** [Scrapers Delight](https://apify.com/scrapersdelight) (community)
- **Categories:** Jobs, Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 per job returneds

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

## 🇧🇪 VDAB Job Scraper — Belgium / Flanders vacancies, employers & hiring contacts

Scrape **VDAB** — *Vlaamse Dienst voor Arbeidsbemiddeling en Beroepsopleiding*, Flanders' public
employment service and by far the biggest job board in Belgium — into clean CSV, JSON or Excel.

**207,809 live vacancies** — measured on 2026-09-06, and every other number on this page was
re-measured the same day — filtered by keyword, city + radius, geo point,
VDAB region, job domain, occupation, contract type, working hours, shift pattern, diploma,
experience, driving licence and freshness. Turn on **detail enrichment** and every row also carries
the **employer's website, the named hiring contact, the direct apply/ATS URL and the street
address** — the fields that turn a job listing into a sales lead.

> **From $0.50 per 1,000 vacancies.** No login, no API key, no browser. VDAB's own public JSON
> endpoints, over plain HTTP.

***

### 🎯 What does VDAB Job Scraper do?

| | |
|---|---|
| 🔎 **Searches like the site does** | Every filter on vdab.be's own search page is a real server-side filter here — verified one by one against live result counts. |
| 🧾 **One row per vacancy** | De-duplicated on VDAB's own vacancy id before anything is billed. |
| 🏢 **Employer intelligence** | Employer legal name, website + host, city, description, and whether the poster is the **direct employer or a staffing agency**. |
| 👤 **Named hiring contact** | `contactPersonName` was present on **100%** of the 135 enriched vacancies in our 2026-09-06 sample. |
| 🔗 **Apply URL + ATS vendor** | The apply link leaks the employer's recruiting-tech vendor (`go4jobs.be`, `lagom.jobs`, …) in `applyAtsHost` — a technographic signal on **76%** of rows pooled over 135 enriched vacancies (37–100% depending on the employer mix your query selects). |
| 🗺️ **Full geography** | Work address, work city, and VDAB's own region code + label (e.g. `BE231A` / *Regio Aalst*). |
| 📄 **Full text** | Description, requirements and offer as plain text, and optionally HTML and Markdown. |
| 🧩 **Beats the 20,000-row API ceiling** | VDAB's search cannot page past ~20,000 results. Adaptive facet slicing splits an oversized query along VDAB's own live facet counts until every slice fits — see below. |
| 🔔 **New-vacancy monitor** | Remembers every vacancy id across scheduled runs and emits only what is new, with Slack / email / webhook alerts. |
| 🛠️ **Shortage-occupation flag** | `shortageOccupation` marks Flanders' official *knelpuntberoepen* — the roles employers cannot fill, and therefore the ones that convert. |

***

### 🧠 The one thing that makes this Actor different: the 20,000-row ceiling

**Measured live, 2026-09-06, page size 100:**

| Request | Rows | First id | Last id |
|---|---|---|---|
| `pagina 0` (offset 0) | 100 | 73445531 | 74071619 |
| `pagina 150` (offset 15,000) | 100 | 73777134 | 74469830 |
| `pagina 198` (offset 19,800) | 100 | 74245886 | 74065079 |
| `pagina 199` (offset 19,900) | 100 | 73959099 | 74500276 |
| `pagina 200` (offset 20,000) | 100 | **73959099** | **74500276** |
| `pagina 205` (offset 20,500) | 100 | **73959099** | **74500276** |
| `pagina 260` (offset 26,000) | 100 | **73959099** | **74500276** |

Past offset ~19,900 VDAB replays **one identical page forever**. A naive scraper keeps paging,
keeps "finding" rows, and — if it does not de-duplicate — **keeps charging you for them**. It will
also quietly tell you a 207,809-vacancy corpus is 20,000 vacancies.

This Actor handles it three ways:

1. **It stops when the page repeats.** The id-set of each page is compared with the previous page's;
   an exact repeat is the ceiling, and the slice ends there. There is also a hard offset guard.
2. **It slices.** Every search response carries VDAB's live per-facet counts. Any query bigger than
   the window is split recursively along those counts — preferring the axes that are true
   partitions (*experience → contract type → working hours*, where every vacancy has exactly one
   value) before falling back to *job domain*, *education*, *shift* or *licence*.
3. **It de-duplicates before billing.** Slices overlap by design; you are never charged twice.

**Measured plan for the whole public corpus, re-run 2026-09-06:**

```
root 207,807  ->  164 slices,  largest slice 16,836,  slices over the window: 0
15 probe calls to plan
```

A worked branch from that run:

```
experience=Beperkte ervaring (60,262)
  > contract=Tijdelijke jobs met optie vast (42,348)
    > working hours=Voltijds (40,932)
      > job domain: 24 branches, largest 16,836   <- fits
```

***

### 📊 Measured field fill rates

Pooled over **135 enriched vacancies across four different queries**, all run on **2026-09-06**
(nationwide 40 · keyword *verpleegkundige* 40 · one employer group 25 · Antwerpen +10 km 30 —
135/135 detail calls succeeded):

| Field | Fill | Example |
|---|---|---|
| `jobId`, `url`, `title`, `employerName` | **100%** | `74494729`, `Beenhouwer vast contract` |
| `employerKind` (agency / direct\_employer) | **100%** | `agency` |
| `locationLabel`, `contractLine`, `experienceLabel` | **100%** | `BEVEREN-KRUIBEKE-ZWIJNDRECHT`, `Vaste job` |
| `shortageOccupation`, `closed`, `openings` | **100%** | `true`, `false`, `1` |
| `firstPublishedAt`, `lastModifiedAt` | **100%** | `2026-08-31T11:50:55Z` |
| `employerGalleryId`, `employerLogoUrl` | 56% *(17–100%)* | `2662` |
| **detail →** `employerId`, `employerLegalName`, `employerCity` | **100%** | `10708373` |
| **detail →** `contactPersonName` | **100%** | `Karen Buys` |
| **detail →** `workRegionCode` / `workRegionLabel` / `workCity` | **100%** | `BE256A` / `Regio Roeselare - Izegem` |
| **detail →** `description` | **100%** | full plain-text job description |
| **detail →** `contractType`, `workingHours`, `jobDomains`, `requiredStudies` | **100%** | `onbepaalde duur`, `voltijds` |
| **detail →** `occupationCodes`, `occupationProfile`, `dataSources` | **100%** | `I110201-1` |
| **detail →** `requirements`, `offer` | 99% *(97–100%)* | |
| **detail →** `applicationAddress` | 99% *(95–100%)* | |
| **detail →** `workAddress` | 94% *(85–100%)* | |
| **detail →** `employerWebsite` + `employerWebsiteHost` | 88% *(67–100%)* | `https://www.go4jobs.be` |
| **detail →** `languages` | 85% *(56–93%)* | `Nederlands · Goed (B1)` |
| **detail →** `employerDescription` | 79% *(57–100%)* | |
| **detail →** `applyUrl` + `applyAtsHost` | 76% *(37–100%)* | `lagom.jobs` |
| **detail →** `externalReference` (employer's own ATS req id) | 63% *(7–100%)* | `publications-51775-B` |
| **detail →** `drivingLicences` | 18% *(0–42%)* | `CE zware vrachtwagen` |
| `duplicates[]` (cross-posted twins) | 7.1% of rows *(0–10.6%)* | pooled over 1,059 rows across nine runs on 2026-09-06 |
| `lastPublishedAt` | **1%** *(0–4%)* | VDAB almost never fills `laatstePublicatieDatum`: 1 row of the 135 above, 2 of 50 on the prefill run, 2 of 120 on a newest-first run. Treat this column as effectively empty and use `firstPublishedAt` / `lastModifiedAt`. |
| `isDuplicateOf` | **0%** unless `includeDuplicates` is on | Only a cross-posted twin row carries it — it points at the parent vacancy id. |

> **Read the ranges, not just the averages.** The optional fields are supplied by the employer, so
> their fill rate depends heavily on **which employers your query happens to select** — `applyUrl`
> was 100% on a single agency's postings and 37% on a general Antwerpen sweep. `drivingLicences` is
> only populated on roles that actually require one, so it is near-zero on office work and high on
> transport queries. The fields the lead-gen use case rests on — `jobId`, `title`, `employerName`,
> `locationLabel`, `contactPersonName`, `description`, `workAddress` — held at **94–100% on every
> one of the four samples**.

#### ⚠️ There is no salary field

VDAB's API returns **no compensation data at all** — not a range, not a band, not a "competitive".
No scraper can give you Belgian salary from this source. Anything advertising VDAB salary data is
inferring it. We would rather tell you than let you find out after you have paid.

***

### 🚦 Reliability — measured, not asserted

All of this was re-measured on **2026-09-06**, on the platform, through the Apify datacenter proxy:

| Test | Result |
|---|---|
| Search calls across 13 platform runs | **195/195 = 100%** (including one run that paged 175 times) |
| Detail fetches across the same runs | **745/746** — the one miss is a deliberate 404 probe of a vacancy id that does not exist |
| Longest single run's detail batch | **250/250 ok** (run `FKBo6r4OTvFQKaxLR`) |
| Concurrency-8 detail burst | **40/40 ok in 13.5 s** (run `6VbeINbbClKzXvqTU`) |
| 107 direct filter probes — every enum option plus every worked example | **107/107 = 100%** |
| Prefill health input, cold container to finish | **4.3 s, 50 rows** (run `husnhlRd0g7JeoAdv`) |
| Transport | Apify datacenter proxy, HTTP 200 on every call above |

**Datacenter proxy is the default** because it measures identical to residential and costs less.
No browser is used anywhere in this Actor.

`maxRequestRetries` defaults to **3**. Nothing needed a retry on 2026-09-06, but detail fetches were
measured at 19/20 first-try on an earlier day, so the retry budget stays.

***

### 💸 Pricing

Pay per event. No monthly rental, no platform-usage surcharge, no charge just for starting a run.

| Event | Price | When it fires |
|---|---|---|
| **Per job returned** | **$0.0005** ($0.50 / 1,000) | Each vacancy delivered to your dataset — **after** de-duplication |
| Per detail fetch | $0.0015 | Only when `includeDetail` is on — **including single-vacancy URLs** |
| Per scheduled run | $0.01 | Only in monitor mode — **and only once VDAB has actually answered a call** |
| Per new vacancy | $0.002 | Only in monitor mode |
| Per alert delivered | $0.002 | Slack / email / webhook |

**Worked examples**

| Run | Cost |
|---|---|
| 1,000 listing rows | $0.50 |
| 1,000 rows with full detail | $2.00 |
| 10,000 sales-lead rows with detail | $20.00 |
| The entire 207,809-vacancy corpus, listings only | ~$104 |
| Daily monitor on one saved search, ~30 new/day | ~$0.08/day |

De-duplication is **on by default and it matters**. It collapses cross-posted twins, overlapping
keyword crawls, overlapping slices and VDAB's own page-replay at the offset ceiling into one billed
row per vacancy id. **Measured 2026-09-06:** feeding the same keyword three times produced **369 raw
rows that collapsed to 123 unique vacancies — 246 duplicate charges avoided** (run
`bImyk2xPzvAT526z0`, `job-scraped: 123`). With `deduplicateById: false` the identical input billed
all **369** (run `NtFjbwlFIZohebcSd`, `job-scraped: 369`).

**A single-vacancy URL costs a detail event only if you asked for detail.** VDAB's
`$.vacatures/{id}` endpoint is the only source for a vacancy id, so the Actor reads it either way —
but with `includeDetail` **off** the detail columns are dropped again before the row is written and
**no** `job-detail-enriched` event is billed. **Measured 2026-09-06** on build 0.1.15: 10 vacancy
URLs with `includeDetail: false` → `job-scraped 10, job-detail-enriched 0` = **$0.0050** (run
`BlNkHJhfYYNcEiteo`, 10 rows, one 24-field key set). The same 10 URLs with `includeDetail: true` →
`job-scraped 10, job-detail-enriched 10` = **$0.0200** (run `Bu6KGcdGi4cAx4wyK`, 10 rows, one
64-field key set). Build 0.1.14 billed the detail event on **both** — 4x the listing-only price on
an option you had left off; 0.1.15 does not.

**Your `maxTotalChargeUsd` is a hard ceiling, including the monitor events.** Before the first row
ships, the run reserves the flat **$0.01 monitor-run fee** (and the one-off email-digest charge) out
of your cap, and prices each row at its *full* cost — $0.0005 row + $0.0015 detail if enabled, plus
in monitor mode $0.002 new-vacancy and $0.002 per alert channel — then delivers only what the cap
covers. **Measured 2026-09-06** (run `rsV7kgspboa4b9ctT`, `maxItems: 600` with `includeDetail` under
a **$0.06** cap): 30 rows delivered, all 30 enriched, `job-scraped 30 + job-detail-enriched 30` =
**$0.0600 exactly — the cap, never a cent over**, and the log names the 170 enriched rows that were
trimmed and not billed. A monitor run needs a cap of **at least ~$0.013**; below that the run
delivers nothing, bills nothing, and says so in its status message rather than billing you over your
own limit. **Measured 2026-09-07** (run `4k7OMlyD3fB1aEkQ4`, monitor mode under a **$0.005** cap):
0 rows, **every event 0**, status *"Nothing was delivered: this run's maximum charge cap does not
cover the flat $0.0100 monitor fee plus a single row. Nothing was charged."*

**The $0.01 monitor fee buys a check — a run that never reached VDAB is not charged for one.** The
fee fires only after at least one VDAB call has come back with a usable response. A monitor run that
hit its own run timeout before its first call, that started past VDAB's ~20,000-row offset ceiling,
or whose every call died on transport or a 404 checked nothing, so it bills nothing — which matters
most on an **Apify Schedule**, where a run timeout set too low would otherwise bill $0.01 every run
forever while delivering nothing. **Measured 2026-09-07 on build 0.1.19**, three structurally
different zero-check paths, all `monitor-run-completed: 0`:

| Zero-check path | Run | Charged |
|---|---|---|
| 32 s run timeout, 0 search calls | `7aXmotRLK29ZJwtyi` | **$0.0000** |
| `startPage: 500` x `pageSize: 50`, past the offset ceiling | `eHsyQOxCKbKOny8iO` | **$0.0000** |
| single vacancy URL, HTTP 404 | `YmRmOJaD7YeEd1WOL` | **$0.0000** |

Build 0.1.18 charged the fee on all three (run `nKpJxItZ8tsguEp6g`: 0 search calls, 0 rows,
`monitor-run-completed: 1` = $0.0100) *and* told the buyer "Nothing was charged." A monitor run that
really did check and found nothing new still bills the fee, and now **says the number**: run
`aflu6QJB9jY514fpr`, 0 rows, `monitor-run-completed: 1`, status *"No newly posted vacancies since the
previous monitor run. Charged for this run: $0.0100 (1 x monitor-run-completed)."*

***

### 🧰 Input — every field, and what it actually does

Each of these was verified against a live result count. **Every number below is the vacancy count
VDAB returned for that filter on 2026-09-06** — vacancies, the unit you are billed in, not VDAB's
facet "job openings" counts, which run about 1.10x higher. VDAB's index turns over daily, so read
them as scale, not as a promise.

#### What to scrape

- **`startUrls`** — paste any vdab.be search URL and its whole query string is applied
  (`trefwoord`, `locatie`, `afstand`, `jobdomein`, `arbeidscircuit`, `diplomaNiveau`, `ervaring`,
  `rijbewijs`, `internationaal`, `regio`, `bedrijfId`, `sort`, `pagina` — comma-separated **and**
  repeated parameters both work). A **single-vacancy URL** scrapes just that vacancy — with the
  full detail fields when `includeDetail` is on (billed as a detail fetch), and listing-level fields
  only, unbilled, when it is off.
- **`searchTerms`** — free-text keywords, one crawl each, merged and de-duplicated.

#### Where

- **`location`** + **`radiusKm`** — city or postcode (`Antwerpen` +5 km → **12,719**; `Antwerpen`
  +10 km → **18,227**; `Hasselt` +5 km → **4,210**; `9000` +10 km → **16,247**).
- **`latitude`** / **`longitude`** + **`radiusKm`** — geo radius (51.05 / 3.72 @ 25 km → **33,914**).
- **`regionCodes`** — VDAB region codes (`BE231A` → **2,952**). Several codes run as several crawls.

#### What kind of job

- **`jobDomains`** — 24 domains. Techniek 75,352 · Bouw 36,053 · Productie 35,571 · Logistiek en
  transport 29,878 · Verkoop 22,844 · Administratie 16,462 · Management 11,866 · Financieel 8,845 ·
  Gezondheid 6,308 · Dienstverlening 6,214 · Onderhoud 4,628 · Horeca en toerisme 4,572 ·
  ICT 3,604 · Marketing 3,061 · Land- en tuinbouw 2,401 · Creatief 2,229 · Onderwijs 2,182 ·
  Human resources 2,004 · Andere 1,372 · Aankoop 1,020 · Juridisch 888 · Overheid 765 ·
  Onderzoek en ontwikkeling 727 · Communicatie 356.
- **`occupationIds`** — exact-occupation UUIDs (see the table below).
- **`contractTypes`** — Tijdelijk met optie vast 112,178 · Vaste jobs 84,226 · Tijdelijke jobs
  6,110 · Zelfstandige 2,060 · Flexijob 2,034 · Studentenjobs 1,343 · Dienstencheque 995 ·
  Alternerende opleiding 161 · Werfreserve 158 · Examens overheid 10 · Tewerkstellingsmaatregelen 8.
- **`workingHours`** — Voltijds 195,928 · Deeltijds 12,055.
- **`workSchedule`** — Dagwerk 186,744 · 2-ploegen 14,978 · Weekend 7,405 · Nacht 3,682 ·
  3-ploegen 2,547 · Volcontinu 1,330 · Onderbroken dienst 614.
- **`educationLevels`** — Geen 87,524 · Secundair 67,127 · Bachelor 43,697 · Master 15,797 ·
  Graduaat/HBO5 10,547 · Lager onderwijs 8,903.
- **`specificDiplomaCode`** — exact study code (`600620` Verpleegkunde → **1,552**).
- **`experienceLevels`** — Beperkte 60,262 · Min. 2 jaar 58,969 · Niet van belang 51,313 ·
  Geen 28,758 · Min. 5 jaar 10,350.
- **`drivingLicences`** — B 50,688 · CE **4,678** · C 3,792 · BE 574 · D 269 · *Geen rijbewijs*
  149,740.
- **`international`** — Europa **317** · Grensregio 32 · Buiten Europa 4.

#### Freshness, employer & sorting

- **`postedWithin`** — *Past week* → **46,969**. *Today or yesterday* → **3,070 measured at 05:07
  UTC**; that one climbs steeply through the Belgian working day, so treat it as a floor rather
  than a constant. This is the monitoring lever.
- **`employerId`** — take the `employerGalleryId` off any row and get that employer group's whole
  hiring footprint (`3446` → **892 vacancies**, all Lagom Engineering entities). The single most
  useful lead-gen lever in this Actor.
- **`sortBy`** — `STANDAARD` or `DATUM` (newest first). VDAB's own site shows a *relevance* option,
  but its API answers HTTP 400 for it, so it is deliberately not offered here.

#### How much, how deep

`maxItems` · `pageSize` (1–200; >200 is clamped by VDAB) · `startPage` (**0-based**) · `maxPages` ·
`autoSliceOversizedQueries` · `includeDetail` · `includeDescriptionHtml` ·
`includeDescriptionMarkdown` · `includeDuplicates` · `includeRawJobPosting` (schema.org JobPosting) ·
`deduplicateById`.

> `maxItems` is a hard cap on rows **delivered and billed** — a run asking for 300 delivers exactly
> 300 and bills exactly 300 (measured 2026-09-06, run `syWYgKnZKyDUUnHL5`: 300 rows, 300 unique ids,
> `job-scraped: 300`), or every row the query has if that is fewer.
>
> `includeDuplicates` is the one flag whose extra rows can be **stubs**. VDAB's `dubbels` entry
> carries only the twin's id, title and employer name. **Listing-only** (`includeDetail` off) a twin
> row therefore arrives with 8 of 24 columns filled and `locationLabel`, `contractLine`,
> `firstPublishedAt` and the rest null — and it is still billed as a row (measured 2026-09-06, run
> `JcNg7xKYYRmad6sIm`: 24 twin rows of 250, 8 of 24 columns filled on every one). **With
> `includeDetail` on**, the twin's own detail record is fetched and back-fills those listing
> columns, so the twin arrives as a full row: run `XQhlubnOkQj4SVyrc` (2026-09-06) returned 24 twins
> at **50–60 of 64 columns filled, mean 57.6**, with `locationLabel`, `contractLine`,
> `firstPublishedAt`, `lastModifiedAt`, `openings`, `closed`, `employerKind` and
> `workScheduleLabels` populated on **24 of 24**. Only `experienceLabel` stays null — VDAB's detail
> record has no experience field at all. Off by default either way.

#### Monitor & alerts

`monitorMode` · `newOnlySince` · `alertOnNewJob` · `webhookUrl` · `slackWebhookUrl` ·
`emailRecipients`.

> `newOnlySince` is the one filter VDAB does not apply for us — this Actor applies it **client-side**
> to every row VDAB returns. So a cutoff nothing satisfies makes the run page the whole result set
> before it can report an empty answer, and it is charged nothing for those rows. Measured
> 2026-09-06 (run `Mc821vFibQKRNahpD`): `newOnlySince: 2027-01-01` on a 3,394-vacancy keyword took
> 175 search calls and 4 min 15 s to scan 3,391 rows and deliver 0. The run log and status message
> now name the count it dropped and the cutoff that dropped it, so this never reads as "VDAB has no
> such jobs". For a server-side freshness filter use **`postedWithin`** instead.

#### Performance

`proxyConfiguration` · `maxConcurrency` (1–8) · `requestTimeoutSecs` · `maxRequestRetries`.

***

### 📌 Example inputs

**Nursing leads around Ghent, with employer contact details**

```json
{
  "searchTerms": ["verpleegkundige"],
  "location": "Gent",
  "radiusKm": 25,
  "includeDetail": true,
  "maxItems": 500
}
```

**Every truck-driver vacancy that needs a CE licence, newest first**

```json
{
  "drivingLicences": ["CE"],
  "sortBy": "DATUM",
  "includeDetail": true,
  "maxItems": 2000
}
```

**One employer's entire hiring footprint**

```json
{ "employerId": 3446, "includeDetail": true, "maxItems": 0 }
```

**The whole corpus (adaptive slicing does the work)**

```json
{ "maxItems": 0, "pageSize": 200, "autoSliceOversizedQueries": true }
```

**Daily new-vacancy monitor for ICT roles, to Slack**

```json
{
  "jobDomains": ["JOBCAT10"],
  "postedWithin": "1",
  "sortBy": "DATUM",
  "monitorMode": true,
  "alertOnNewJob": true,
  "slackWebhookUrl": "https://hooks.slack.com/services/…",
  "maxItems": 0
}
```

***

### 🔑 Shortage-occupation IDs (`occupationIds`)

VDAB's 30 tracked occupations and their live vacancy counts, measured 2026-09-06:

| Occupation | ID | Vacancies |
|---|---|---|
| Vrachtwagenbestuurder | `142a1c8a-ded8-4b8e-b445-5da25dec3335` | 11,644 |
| Industrieel elektromecanicien | `953ddb4a-da61-4d28-b46f-c1efb64a5fc0` | 8,164 |
| Magazijnmedewerker | `bb86bb0c-8062-4cd5-a257-186606056bbe` | 7,880 |
| Winkelverkoper | `c6eda604-d594-4d41-ab67-3e2ef003641d` | 6,492 |
| Industrieel onderhoudstechnicus | `410a4483-7305-496a-93f2-613e6e5930fd` | 4,703 |
| Winkelmedewerker | `fe24420c-0cf8-4bbc-9fa6-9b1120e6aabf` | 4,196 |
| Insteller-omsteller verspaning | `fa01c16b-2027-4df2-8c51-feaa80db1129` | 3,663 |
| Accountant | `383e50a3-9d06-4821-b880-c6da89b6e809` | 3,866 |
| Manueel lasser | `4a95fd9c-8dd5-4e5f-a7cf-4a836cd4691d` | 3,425 |
| Productiemedewerker | `ff2b9b81-6c31-4528-8b5d-4995788c8cf5` | 3,047 |
| Polyvalent administratief medewerker | `d70607ef-e4cf-43a9-a340-a1b9e9d8e3f9` | 3,206 |
| Productieoperator voedingsindustrie | `af332f1f-8134-4799-92e7-00d6f2119e2d` | 2,865 |
| Werfleider | `bc1d48d2-763e-477b-b1e8-d0038fdd0a14` | 3,116 |
| Elektrotechnicus | `cfe84de1-a7d1-4b34-924d-764c2cb3c649` | 2,802 |
| Vertegenwoordiger B2B | `54a6cf77-d3ff-4691-b03d-f9e30b4790ef` | 2,631 |
| Teamleader productie | `9ef857e6-fc27-41f8-a6dd-2970daa31411` | 2,524 |
| Onderhoudsmecanicien personenwagens | `d0f506b4-0894-41bf-b1a4-e494c892bca3` | 2,526 |
| Verpleegkundige | `cee9ee58-43a9-4300-b6c4-8bf1f7b59c28` | 2,498 |
| Monteur installatietechnieken | `ff8e0023-a150-4c4e-8c48-9b635d2cbfa3` | 2,565 |
| Wegenwerker | `a8fc4ca1-3512-4e9c-ad95-13ba3a09c0f1` | 2,433 |
| Medewerker ruwbouw | `1eddda40-3023-4324-834e-c7b0df9e3531` | 2,241 |
| Technicus werf-, landbouw- en hefmachines | `da50b460-bfac-4eeb-b328-fc7c66ed6088` | 2,207 |
| Verantwoordelijke kwaliteitscontrole | `3d87a629-d78d-4fee-8597-3b5daf475447` | 2,250 |
| Operator montage en assemblage | `5cb21c1f-a279-4654-ad0c-4419dfc55006` | 1,999 |
| Commercieel medewerker binnendienst | `9cbe7cd0-9ff7-4723-ac13-7610e52bee6a` | 2,007 |
| Projectleider bouw en openbare werken | `b11bf6de-c048-4539-90ee-d969c2956c1b` | 1,943 |
| Kraanbestuurder | `30093422-8571-4ff4-97f9-5a1bab6a2ee9` | 1,800 |
| Dakwerker | `adf8d084-3819-45d5-a429-0471b3fa4432` | 1,866 |
| Plaatser buitenschrijnwerk | `3911e32d-b57f-49db-a9de-7a0c2e748d91` | 1,703 |
| Heftruckchauffeur | `823d8e67-9c45-4e84-a7aa-f9675585c7fd` | 1,553 |

***

### 🙋 FAQ

**Does this need a VDAB login or API key?**
No. It reads the same public JSON endpoints the vdab.be job-search page itself calls from your
browser. No account, no key, no cookies.

**Does it use a browser?**
No. Plain HTTP over Apify's cheapest datacenter proxy. That is why it is fast and cheap.

**Can I really get all 207,809 vacancies?**
Yes, with `maxItems: 0` and adaptive slicing on — that is exactly what slicing exists for.
**What was actually measured 2026-09-06** (run `0pbBGqAi7Tfr9GXk4`, a *reachability* sweep at
`maxPages: 1, pageSize: 1`): the corpus planned into **164 slices in 27 s** off 15 probe calls, and
every one of the 164 slices was then entered and answered with a row — 179 search calls, 154 unique
rows after de-duplication, 4 m 17 s. That proves every slice is reachable; it is **not** a
207,809-row extraction, and this Actor does not claim one it has not run. At `pageSize: 200` a full
sweep is roughly 1,040 page calls on top of the planning, so budget about an hour and set the run
timeout accordingly (the Actor defaults to 3,600 s and always flushes what it has collected before
the timeout hits).

**Read the slice counter, and trust it.** The run log now reports slices **crawled out of slices
planned** — `1 of 164 planned query slice(s) crawled (partial sweep)` — and warns you by name when
the two differ and why (time limit, charge cap, or `maxItems` reached). **Measured 2026-09-06** (run
`5Tw54aMLArLR6Xv0A`, whole corpus under a **$0.05** cap): 100 rows, 18 search calls, *"Partial
coverage: 1 of 164 planned slice(s) were actually crawled (the run's charge cap was spent). Treat
these rows as a sample of the query, not the whole of it."* Build 0.1.14 printed `164 query slice(s)
crawled` for that same run — the planned count, which reads as a swept corpus.

**Why does my run return fewer rows than VDAB's headline number?**
Three honest reasons. (1) A single un-sliced query cannot exceed ~20,000 — turn slicing on.
(2) Slices that must split on *job domain*, *education*, *shift* or *licence* rather than a true
partition can miss the handful of vacancies that carry no value on that axis; the run log warns you
by name when this happens. (3) VDAB's index changes while you crawl — it moved by 5 vacancies over
the 20 minutes of testing on 2026-09-06 alone. **Measured 2026-09-04:** re-crawling the same
328-vacancy set through a partition axis recovered 327 of 328 (99.7%), and through the
non-partition *job domain* axis 324 of 328 (98.8%) — that small gap is live index churn between
passes, not a parser bug.

**Is salary included?**
No. VDAB's API has no compensation field. See above.

**What is the difference between `employerName` and `employerLegalName`?**
`employerName` is the name on the listing (which for an agency posting is the agency's brand).
`employerKind` tells you whether the poster is the direct employer or a staffing agency —
`GEWONE_BEDRIJVEN` vs `INTERMEDIAIREN` in VDAB's own vocabulary. `employerLegalName`,
`employerWebsite` and `employerId` come from the detail record.

**Can I get every vacancy from one company?**
Yes. Take `employerGalleryId` from any row and pass it as `employerId`. Measured: `3446` returns
892 vacancies.

**How does monitor mode avoid re-reporting the same vacancy?**
It keeps its seen-set in a **named** key-value store (`vdab-jobs-monitor-state`) that survives
across runs. **Verified 2026-09-06:** run `vhignWJ24TLtZVfsm` returned 25 new vacancies and
remembered their ids; run `c6PluVtnp87nwsa1P` on the identical input loaded those 25 back, skipped
every one of them and returned the **next 25 unseen** — **zero id overlap between the two runs**.
A vacancy is only committed to the seen-set once it has actually been delivered, so a row your
charge cap trimmed away is still new next run rather than silently lost.

**Does it cover Wallonia and Brussels?**
No — VDAB is the Flemish service. Le Forem (Wallonia) and Actiris (Brussels) are different sites and
belong in different Actors.

**What happens if VDAB returns nothing?**
The run finishes **SUCCEEDED with a status message**, not FAILED, and the message ends with the
run's **real** charged total read out of the billing counters — `Nothing was charged.` when that is
true, otherwise `Charged for this run: $X.XXXX (…)`. It never promises you a zero it did not bill.
The message also says *which* of the seven possible reasons the run was empty, rather than guessing:

- your **`maxTotalChargeUsd`** did not cover a single row → the message says that and tells you to
  raise it.
- you passed a **single-vacancy URL** and VDAB answered 404 → the message says the vacancy id does
  not exist or has been taken offline, and does **not** claim a search came back empty (no search
  was run).
- **the run hit its own time limit** before a row could be delivered → the message says so, names
  what it had already measured (search calls, vacancies scanned, VDAB's own result count if it got
  that far) and tells you to raise the run timeout. It never blames your filters for your timeout.
  **Measured 2026-09-06** (run `CxaVD3ejx91NXBnId`, keyword `verpleegkundige` under a **32 s** run
  timeout): 0 rows, 0 charged, status *"This run hit its 32s time limit before it could deliver a
  row — this is NOT an empty VDAB result and your filters are not the problem."* On build 0.1.14 the
  identical input said *"No VDAB vacancies matched this search (VDAB reported 0 results)"* for a
  keyword VDAB holds **3,394** vacancies for.
- **no search call was ever sent** for another reason it can name — the charge cap was spent first,
  or your `startPage` x `pageSize` already starts past VDAB's ~20,000-row offset ceiling. A run that
  never asked VDAB anything is never reported as VDAB answering nothing.
- **monitor mode** found nothing newly posted since the previous run → *"No newly posted vacancies
  since the previous monitor run. Charged for this run: $0.0100 (1 x monitor-run-completed)."* This
  is the one empty run that **is** billed — the $0.01 bought a real check — and it says so
  (run `aflu6QJB9jY514fpr`, 2026-09-07). A monitor run that never reached VDAB is charged nothing;
  see Pricing above.
- the search matched plenty but your **`newOnlySince`** cutoff dropped every row → the message says
  so and names the count it scanned, because that is a filter you set, not an empty job board.
- VDAB's search genuinely matched nothing, across search calls that were actually made and answered
  → *"No VDAB vacancies matched this search (VDAB reported 0 results) across N search call(s)."*
  This is the only message that blames the query, and it is only reachable when a search really ran.

A run where *every* API call failed for a transport reason does **fail loudly** — including a
vacancy-URL run whose fetches got no response at all — so you never mistake a transport problem for
"there are no such jobs".

**Which language is the data in?**
Dutch — VDAB's labels are Dutch and this Actor passes them through rather than inventing
translations. Codes (`JOBCAT10`, `BE231A`, `CE`) are language-neutral.

***

### ⚖️ Legal & fair use

- This Actor reads **public, un-authenticated** VDAB endpoints — the same ones vdab.be's own search
  page calls. It does not log in, does not solve challenges, and does not touch anything behind
  `/mijnvdab/` or any other private path.
- **robots.txt, checked line by line on 2026-09-04.** `https://www.vdab.be/robots.txt` contains no
  `Disallow` rule matching `/rest/`, the path this Actor uses. It *does* contain
  `Disallow: /api/vindeenjob/`, `Disallow: /include/vacature/` and `Disallow: /vac/` — **this Actor
  calls none of those.** VDAB also publishes
  `Sitemap: https://www.vdab.be/sitemap/vindeenjob/vacatures/index.xml`, i.e. it invites crawlers to
  index exactly these vacancy pages.
- Request volume is modest and rate-limited (default concurrency 5, measured clean at 8).
- **Personal data is your responsibility.** With `includeDetail` on, rows can contain a named hiring
  contact. That is personal data under the GDPR. You are the controller for whatever you do with it —
  have a lawful basis, honour erasure requests, and do not use it for anything a Belgian recruiter
  would not expect. Turn `includeDetail` off if you only need the vacancy data.
- You are responsible for complying with VDAB's terms of use in your jurisdiction and use case.

***

*Built by ScrapersDelight. Found a field VDAB returns that this Actor does not? Open an issue on the
Actor page — the API surface is wide and we would rather ship it than have you post-process it.*

# Actor input Schema

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

Paste any vdab.be search URL - e.g. https://www.vdab.be/vindeenjob/vacatures?trefwoord=verpleegkundige\&locatie=Gent\&afstand=25 - and every filter in its query string is applied. A single-vacancy URL (https://www.vdab.be/vindeenjob/vacatures/74494729) scrapes just that vacancy - with the full detail fields when 'Fetch full detail' is on (billed as one detail fetch), and listing-level fields only, with no detail charge, when it is off (measured 2026-09-06, run BlNkHJhfYYNcEiteo: 10 vacancy URLs, detail off, 0 detail events billed); if VDAB answers 404 for it the run finishes clean and says the vacancy id does not exist or has been taken offline, rather than reporting an empty search. Leave empty to use the filter fields below.

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

Free-text keywords, exactly as you would type them into VDAB's own search box (Dutch works best: 'verpleegkundige', 'boekhouder', 'heftruckchauffeur'). One crawl runs per keyword and the results are merged and de-duplicated.

## `location` (type: `string`):

Flemish/Brussels city name or postcode - 'Antwerpen', 'Gent', '9000'. Combine with the search radius below. MEASURED 2026-09-06: Antwerpen +5 km returns 12,719 vacancies, Antwerpen +10 km 18,227, Hasselt +5 km 4,210, postcode 9000 +10 km 16,247.

## `radiusKm` (type: `integer`):

Radius in kilometres around the location (or around the latitude/longitude below). Ignored when neither is set.

## `latitude` (type: `string`):

Latitude for a geo-radius search, e.g. 51.05 (Gent). Use together with Longitude and the radius. Overrides nothing — it is simply an alternative to the city field.

## `longitude` (type: `string`):

Longitude for a geo-radius search, e.g. 3.72 (Gent). MEASURED 2026-09-06: 51.05 / 3.72 at 25 km returns 33,914 vacancies.

## `regionCodes` (type: `array`):

VDAB employment-region codes, e.g. 'BE231A' (Regio Aalst - 2,952 vacancies measured 2026-09-06). The API takes one region per query, so several codes are run as several crawls and merged. Region codes appear on every detail row as workRegionCode, so one detail run tells you the codes you want.

## `jobDomains` (type: `array`):

VDAB's 24 top-level job domains. Live vacancy counts measured 2026-09-06: Techniek 75,352, Bouw 36,053, Productie 35,571, Logistiek en transport 29,878, Verkoop 22,844, Administratie 16,462, Management 11,866, Financieel 8,845, Gezondheid 6,308, ICT 3,604.

## `occupationIds` (type: `array`):

VDAB occupation UUIDs for a precise job title, e.g. cee9ee58-43a9-4300-b6c4-8bf1f7b59c28 = Verpleegkundige (2,498 vacancies measured 2026-09-06). All 30 shortage occupations and their UUIDs are listed in this Actor's README with the same-day counts.

## `contractTypes` (type: `array`):

VDAB contract categories. Live vacancy counts measured 2026-09-06: Tijdelijk met optie vast 112,178, Vaste jobs 84,226, Tijdelijke jobs 6,110, Zelfstandige activiteit 2,060, Flexijob 2,034, Studentenjobs 1,343, Dienstenchequebaan 995.

## `workingHours` (type: `array`):

Full-time or part-time. Measured 2026-09-06: Voltijds 195,928 vacancies, Deeltijds 12,055.

## `workSchedule` (type: `array`):

Shift pattern. Measured 2026-09-06: Dagwerk 186,744, 2-ploegen 14,978, Weekendwerk 7,405, Nachtwerk 3,682, 3-ploegen 2,547, Volcontinu 1,330, Onderbroken dienst 614.

## `educationLevels` (type: `array`):

Minimum diploma the vacancy asks for. Measured 2026-09-06: Geen 87,524, Secundair 67,127, Bachelor 43,697, Master 15,797, Graduaat/HBO5 10,547, Lager onderwijs 8,903.

## `specificDiplomaCode` (type: `string`):

VDAB study-programme code for an exact qualification, e.g. 600620 = Verpleegkunde (1,552 vacancies measured 2026-09-06). Study codes appear on every detail row as requiredStudies\[].code.

## `experienceLevels` (type: `array`):

Measured 2026-09-06: Beperkte ervaring 60,262, Minstens 2 jaar 58,969, Niet van belang 51,313, Geen ervaring 28,758, Minstens 5 jaar 10,350.

## `drivingLicences` (type: `array`):

Licence classes the vacancy requires. Measured 2026-09-06: B 50,688, CE 4,678, C 3,792, BE 574, D 269. 'Geen rijbewijs' (0) returns the 149,740 vacancies that need none.

## `international` (type: `array`):

Only vacancies flagged as international. Small but high-value - measured 2026-09-06: Europa 317, Grensregio 32, Buiten Europa 4.

## `postedWithin` (type: `string`):

Freshness filter, applied by VDAB server-side - the monitoring lever. MEASURED 2026-09-06: 'Past week' returns 46,969 vacancies; 'Today or yesterday' returned 3,070 at 05:07 UTC and climbs through the Belgian working day, so treat that one as a floor rather than a constant. Pair 'Today or yesterday' with Monitor mode and an Apify Schedule.

## `employerId` (type: `integer`):

Pull EVERY vacancy for one employer group. Take the employerGalleryId off any row this Actor returns and paste it here. MEASURED 2026-09-06: id 3446 (Lagom Engineering) returns 892 vacancies across all its entities. This is the key lead-gen lever - it turns one interesting row into that employer's whole hiring footprint.

## `sortBy` (type: `string`):

'Newest first' is what you want for monitors and fresh-lead runs. (VDAB's own site offers a 'relevance' sort, but its API rejects it with HTTP 400, so it is deliberately not offered here.)

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

Hard cap on rows delivered AND billed. MEASURED 2026-09-06: a run asking for 300 delivered exactly 300 rows, 300 unique ids, and billed 300. Defaults to 50 for a fast, cheap first run. Set 0 for an unlimited sweep - with adaptive slicing on, that can reach the whole 207,809-vacancy corpus.

## `pageSize` (type: `integer`):

VDAB accepts 1–200 per call (values above 200 are silently clamped). 100 is a good balance; 200 halves the number of calls on a big sweep.

## `startPage` (type: `integer`):

Skip the first N pages. VDAB's paging is 0-BASED, so page 0 is the first page. Setting this disables adaptive slicing for that query.

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

Safety stop on pages per query (or per slice when slicing is on). 0 = no page limit, bounded only by Max vacancies and VDAB's own ~20,000-row window.

## `autoSliceOversizedQueries` (type: `boolean`):

VDAB's search caps the reachable offset at ~20,000 rows: past it the same page replays forever. With this on, any query larger than that is split along VDAB's own live facet counts (experience > contract type > working hours > job domain) until every slice fits inside the window, then the slices are merged and de-duplicated. MEASURED 2026-09-06: the whole 207,807-vacancy corpus planned into 164 slices in 27 s, largest slice 16,836, none over the window. Turn it off only if you want exactly one raw query.

## `includeDetail` (type: `boolean`):

Adds one API call per vacancy and unlocks the lead-gen fields: employer website, named hiring contact, direct apply/ATS URL, street address, full description, requirements, offer, languages, licences, region code. Billed as a separate 'detail fetch' event - the ONLY thing that ever bills one, single-vacancy URLs included - so leave it off for cheap listing-only runs.

## `includeDescriptionHtml` (type: `boolean`):

Adds descriptionHtml / requirementsHtml / offerHtml alongside the plain text. Requires 'Fetch full detail'. Off by default because it roughly triples row size.

## `includeDescriptionMarkdown` (type: `boolean`):

Adds descriptionMarkdown / requirementsMarkdown / offerMarkdown. Requires 'Fetch full detail'. Handy for LLM pipelines.

## `includeDuplicates` (type: `boolean`):

7.1% of VDAB vacancies carry a 'dubbels' list of cross-posted twin listings, each with its own VDAB id (measured 2026-09-06 over 1,059 rows; 0-10.6% depending on the query). Off (default), the twins stay nested in the duplicates\[] field. On, they become their own rows - and are billed as rows. IMPORTANT: VDAB's 'dubbels' entry carries only the twin's id, title and employer name. With 'Fetch full detail' OFF a twin row is therefore a STUB - only jobId, url, title, employerName, isDuplicateOf, sourceQuery and scrapedAt are populated (duplicateCount is always 0 on a twin), and locationLabel, contractLine, firstPublishedAt and the rest arrive null. With 'Fetch full detail' ON the twin's own detail record is fetched and back-fills those listing columns, so the twin arrives as a full row.

## `includeRawJobPosting` (type: `boolean`):

Adds VDAB's own schema.org JobPosting object (title, description, hiringOrganization, jobLocation, employmentType, datePosted), parsed. Requires 'Fetch full detail'.

## `deduplicateById` (type: `boolean`):

Keep this ON. It is what stops you paying twice for the same vacancy: cross-posted twins, overlapping keyword crawls, overlapping slices and VDAB's own page-replay at the offset ceiling all resolve to one row per vacancy id. MEASURED 2026-09-06: the same keyword fed three times produced 369 raw rows that collapsed to 123 unique vacancies and billed 123; with this off, the identical input billed all 369.

## `monitorMode` (type: `boolean`):

Recurring watcher: remembers every vacancy id seen in earlier runs (in a NAMED key-value store that survives across runs) and outputs ONLY vacancies it has never seen. VERIFIED 2026-09-06: two identical back-to-back runs returned 25 and then the NEXT 25 unseen vacancies, with zero id overlap. Pair with an Apify Schedule and 'Posted within: Today or yesterday'. Billing note: a monitor run carries a flat $0.01 monitor-run fee plus $0.002 per new vacancy; if you set a maxTotalChargeUsd, that flat fee is reserved out of it before any row is delivered, so a cap below ~$0.013 delivers (and bills) nothing.

## `newOnlySince` (type: `string`):

ISO date or datetime, e.g. 2026-09-01 or 2026-09-01T00:00:00Z. Drops any vacancy whose first-published AND last-modified dates are both older. Works with or without Monitor mode. NOTE: unlike 'Posted within', VDAB has no server-side filter for this, so it is applied to every row VDAB returns - a cutoff nothing satisfies makes the run page the entire result set before it can report an empty answer (measured 2026-09-06: 175 search calls and 4 min to scan 3,391 rows and deliver 0). Nothing is charged for the rows it drops, and the run log and status message name both the count dropped and the cutoff that dropped them. For a cheap server-side freshness filter use 'Posted within' instead.

## `alertOnNewJob` (type: `boolean`):

In monitor mode, deliver an alert per newly seen vacancy through the channels below (up to 200 per run).

## `webhookUrl` (type: `string`):

POST endpoint for new-vacancy alerts (Make / Zapier / n8n / your own). One JSON body per alert.

## `slackWebhookUrl` (type: `string`):

Slack incoming-webhook URL for formatted new-vacancy cards.

## `emailRecipients` (type: `array`):

Email addresses for the new-vacancy digest, sent through apify/send-mail.

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

Apify datacenter proxy is enough - measured 2026-09-06 at 195/195 search calls and 745/746 detail calls HTTP 200 across 13 platform runs, and 40/40 on a concurrency-8 detail burst. RESIDENTIAL (BE or NL) also works if you ever need it, but it costs more for no measured gain.

## `maxConcurrency` (type: `integer`):

Parallel detail fetches. MEASURED 2026-09-06: 8 was clean (40/40 detail fetches HTTP 200 in 13.5 s); 5 is the polite default.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout. Measured 2026-09-06 through the Apify datacenter proxy: a 50-row search run finished cold-container-to-exit in 4.3 s, and no call in 195 searches or 746 detail fetches came near the 45 s default.

## `maxRequestRetries` (type: `integer`):

Retries on a transport error or 5xx, each on a fresh proxy exit. Nothing needed a retry on 2026-09-06 (195/195 searches, 745/746 detail fetches - the one miss a deliberate 404 probe of an id that does not exist), but detail fetches measured 19/20 first-try on an earlier day, so keep this at 3 or higher.

## Actor input object example

```json
{
  "radiusKm": 25,
  "postedWithin": "any",
  "sortBy": "DATUM",
  "maxItems": 50,
  "pageSize": 100,
  "startPage": 0,
  "maxPages": 0,
  "autoSliceOversizedQueries": true,
  "includeDetail": false,
  "includeDescriptionHtml": false,
  "includeDescriptionMarkdown": false,
  "includeDuplicates": false,
  "includeRawJobPosting": false,
  "deduplicateById": true,
  "monitorMode": false,
  "alertOnNewJob": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "maxConcurrency": 5,
  "requestTimeoutSecs": 45,
  "maxRequestRetries": 3
}
```

# Actor output Schema

## `items` (type: `string`):

The dataset of scraped VDAB vacancies — one Belgian/Flemish job vacancy per row, de-duplicated by VDAB vacancy id.

# 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 = {
    "sortBy": "DATUM",
    "maxItems": 50,
    "includeDetail": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/vdab-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 = {
    "sortBy": "DATUM",
    "maxItems": 50,
    "includeDetail": False,
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/vdab-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 '{
  "sortBy": "DATUM",
  "maxItems": 50,
  "includeDetail": false
}' |
apify call scrapersdelight/vdab-jobs-scraper --silent --output-dataset

```

## MCP server setup

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