VDAB Job Scraper - Belgium Flanders Vacancies & Employers
Pricing
from $0.50 / 1,000 per job returneds
VDAB Job Scraper - Belgium Flanders Vacancies & Employers
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.
Pricing
from $0.50 / 1,000 per job returneds
Rating
0.0
(0)
Developer
Scrapers Delight
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
21 hours ago
Last modified
Categories
Share
🇧🇪 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:
- 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.
- 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.
- 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: 015 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 —
applyUrlwas 100% on a single agency's postings and 37% on a general Antwerpen sweep.drivingLicencesis 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 whenincludeDetailis 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 (600620Verpleegkunde → 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 theemployerGalleryIdoff 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—STANDAARDorDATUM(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.
maxItemsis a hard cap on rows delivered and billed — a run asking for 300 delivers exactly 300 and bills exactly 300 (measured 2026-09-06, runsyWYgKnZKyDUUnHL5: 300 rows, 300 unique ids,job-scraped: 300), or every row the query has if that is fewer.
includeDuplicatesis the one flag whose extra rows can be stubs. VDAB'sdubbelsentry carries only the twin's id, title and employer name. Listing-only (includeDetailoff) a twin row therefore arrives with 8 of 24 columns filled andlocationLabel,contractLine,firstPublishedAtand the rest null — and it is still billed as a row (measured 2026-09-06, runJcNg7xKYYRmad6sIm: 24 twin rows of 250, 8 of 24 columns filled on every one). WithincludeDetailon, the twin's own detail record is fetched and back-fills those listing columns, so the twin arrives as a full row: runXQhlubnOkQj4SVyrc(2026-09-06) returned 24 twins at 50–60 of 64 columns filled, mean 57.6, withlocationLabel,contractLine,firstPublishedAt,lastModifiedAt,openings,closed,employerKindandworkScheduleLabelspopulated on 24 of 24. OnlyexperienceLabelstays null — VDAB's detail record has no experience field at all. Off by default either way.
Monitor & alerts
monitorMode · newOnlySince · alertOnNewJob · webhookUrl · slackWebhookUrl ·
emailRecipients.
newOnlySinceis 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 (runMc821vFibQKRNahpD):newOnlySince: 2027-01-01on 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 usepostedWithininstead.
Performance
proxyConfiguration · maxConcurrency (1–8) · requestTimeoutSecs · maxRequestRetries.
📌 Example inputs
Nursing leads around Ghent, with employer contact details
{"searchTerms": ["verpleegkundige"],"location": "Gent","radiusKm": 25,"includeDetail": true,"maxItems": 500}
Every truck-driver vacancy that needs a CE licence, newest first
{"drivingLicences": ["CE"],"sortBy": "DATUM","includeDetail": true,"maxItems": 2000}
One employer's entire hiring footprint
{ "employerId": 3446, "includeDetail": true, "maxItems": 0 }
The whole corpus (adaptive slicing does the work)
{ "maxItems": 0, "pageSize": 200, "autoSliceOversizedQueries": true }
Daily new-vacancy monitor for ICT roles, to Slack
{"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) crawledWhy 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
maxTotalChargeUsddid 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, keywordverpleegkundigeunder 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
startPagexpageSizealready 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
newOnlySincecutoff 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.txtcontains noDisallowrule matching/rest/, the path this Actor uses. It does containDisallow: /api/vindeenjob/,Disallow: /include/vacature/andDisallow: /vac/— this Actor calls none of those. VDAB also publishesSitemap: 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
includeDetailon, 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. TurnincludeDetailoff 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.