NL Hiring Signal Tracker – Open Jobs per Company
Pricing
from $17.00 / 1,000 company resolveds
NL Hiring Signal Tracker – Open Jobs per Company
Hiring signal tracker for sales and revenue teams: open jobs per company straight from Greenhouse, Lever, Recruitee, Homerun, Teamtailor, Workable and Ashby. Department and location breakdowns, 30-day hiring velocity and what changed since the last run, CRM-ready. Vacancy data, no contact fields.
Pricing
from $17.00 / 1,000 company resolveds
Rating
0.0
(0)
Developer
Leyten Data
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a month ago
Last modified
Categories
Security
Limited permissionsShare
What does NL Hiring Signal Tracker do?
NL Hiring Signal Tracker is a hiring signal tool for sales and revenue teams: give it a list of company domains and get the open jobs (vacatures) straight from each company's own applicant tracking system (ATS). Per company, you get the number of open jobs, a breakdown by department and location, the newest and oldest posting, and the hiring velocity (new jobs in the last 30 days). Use it for growth signal enrichment in your CRM, account scoring and outbound timing. It reads job fields only, never recruiter or contact fields.
Why hiring is a buying signal
A company that is hiring is a company that is spending. Open roles can hint at where the budget is going; some examples of how to read them:
- 5 new engineering roles → a platform build is starting: dev tools, cloud, security and recruiting vendors
- First "Head of Finance" or "Controller" → finance software, audit and payroll providers
- New sales roles in a new country → expansion: local legal, office, payroll, sales tooling
- Customer support roles piling up → support tooling and outsourcing
- Hiring freeze (openings dropping to zero) → deprioritize, or offer cost savings
To keep the signal fresh and specific, this Actor reads the company's own ATS instead of aggregated job boards, and reports department, location and 30-day velocity instead of a single count.
Supported ATS and data sources
| ATS | Public source used | Posting date |
|---|---|---|
| Greenhouse | Job Board API (boards-api.greenhouse.io), jobs and departments | First published |
| Lever | Postings API (api.lever.co, EU instance api.eu.lever.co) | Created |
| Recruitee | Careers Site API ({company}.recruitee.com/api/offers/, also on custom domains) | Published |
| Workable | Public jobs widget API (apply.workable.com/api/v1/widget/accounts/{account}) | Published on |
| Ashby | Public Job Posting API (api.ashbyhq.com/posting-api/job-board/{name}) | Last published* |
| Teamtailor | Public career-site RSS feed (/jobs.rss, also on custom domains) | Published |
| Homerun | Public job feed (feed.homerun.co/{company}) | Not provided** |
* Ashby reports the date a job was last published, so a republished job counts as new. ** Homerun's feed has no posting date. For Homerun companies, velocity is based on when this Actor first saw each job: available from 30 days after the first run, and only while the job list is stored (see the FAQ).
For Teamtailor and Homerun, this Actor reads the public career-site feeds, which need no API key from the company.
Features
- Hiring signal per company from the company's own ATS: open jobs, jobs by department, jobs by location, newest and oldest posting, 30-day hiring velocity
- ATS detection from a domain (homepage, career pages and ATS-powered career domains such as
werkenbij.company.nl), or a job board URL directly - Official public job-board endpoints of Greenhouse, Lever, Recruitee, Homerun, Teamtailor, Workable and Ashby, not rendered pages
- Every input company reported: companies without a detectable ATS are listed with what was checked, and are not charged. A run that nears its timeout reports the companies it checked and says in its log and status message how many it left out
- Changes since the previous run:
+1 job (Frontend Engineer), −1 job (Account Executive Benelux) - CRM-ready dataset views and CSV export, with a HubSpot example below
- Vacancy data only: recruiter, contact and description fields are never read, and an e-mail address, phone number or link typed into a title, department or location is removed. A person's name that a company itself writes into a job title is kept as published
- Runs through the Apify API, on a weekly schedule, or from Make, Zapier and n8n
Use cases
Sales development: account scoring on hiring velocity
Score accounts on hiringVelocity.newJobsLast30Days and on departments that match your product (for example byDepartment.Engineering), and alert your reps on changeType: "changed" with new jobs. A weekly schedule suits account scoring; a daily one suits alerting on new roles at key accounts.
RevOps: hiring signals into the CRM
1. Download a CRM-ready CSV. The "Companies" view flattens the data into one row per company:
https://api.apify.com/v2/datasets/DATASET_ID/items?view=companies&format=csv&token=YOUR_API_TOKEN
Or pick exact columns yourself:
https://api.apify.com/v2/datasets/DATASET_ID/items?format=csv&flatten=payload,payload.hiringVelocity&fields=payload.input,payload.domain,payload.companyName,payload.ats,payload.totalJobs,payload.hiringVelocity.newJobsLast30Days,payload.topDepartments&token=YOUR_API_TOKEN
2. No-code: Make, Zapier or n8n. Trigger on "Actor run finished", read the dataset items, filter on payload.status = resolved, match the company on payload.input (the line you entered, so your own key) or on payload.domain in HubSpot, Salesforce or Pipedrive, and update fields such as Open jobs, New jobs last 30 days, Hiring departments and ATS.
3. Code: HubSpot example. Create the custom company properties open_jobs, new_jobs_30d and ats in HubSpot first, then match on the domain (companies you entered as a job board URL have none, so match those on payload.input):
const items = await fetch(`https://api.apify.com/v2/datasets/${DATASET_ID}/items?clean=true`, {headers: { Authorization: `Bearer ${APIFY_TOKEN}` },}).then((r) => r.json());for (const { payload } of items.filter((item) => item.payload.status === 'resolved' && item.payload.domain)) {const hubspot = { Authorization: `Bearer ${HUBSPOT_TOKEN}`, 'Content-Type': 'application/json' };const search = await fetch('https://api.hubapi.com/crm/v3/objects/companies/search', {method: 'POST',headers: hubspot,body: JSON.stringify({ filterGroups: [{ filters: [{ propertyName: 'domain', operator: 'EQ', value: payload.domain }] }] }),}).then((r) => r.json());const company = search.results?.[0];if (!company) continue;await fetch(`https://api.hubapi.com/crm/v3/objects/companies/${company.id}`, {method: 'PATCH',headers: hubspot,body: JSON.stringify({ properties: { open_jobs: payload.totalJobs, new_jobs_30d: payload.hiringVelocity?.newJobsLast30Days ?? '', ats: payload.ats } }),});}
Recruiting agencies and HR tech vendors
A weekly run over a target list shows which companies opened roles in your specialty and which stopped hiring.
How to use it
- Put your account list into Companies, one per line. A plain domain such as
sendcloud.comis enough: the Actor finds the careers page and the applicant tracking system behind it by itself. Paste a job board URL directly when you already know it, which skips the search and is the fastest path for a large list. - Run it once and read the
companiesview. Every company you submitted produces a record, including the ones with no detectable system and the ones that could not be reached, so a row never silently disappears from your list. The one exception is a run that nears its timeout: it stops checking companies so that it can still deliver the ones it checked, and its log and status message say how many it left out (see Large lists and the run's timeout below). Openjobswhen you want the individual openings behind the count. - Give it a State key and schedule it weekly. From the second run onward each record also carries what changed since the previous one. The 30-day hiring velocity is there from the first run for the six systems that publish posting dates, and for Homerun from 30 days after the first run while the job list is stored. Velocity is the number that actually signals something: a company that went from two openings to eleven is a different conversation than one that has had four all year.
Keep Include the job list on while you are exploring and turn it off for large lists once you only need the counts per company and the department and location breakdown. That keeps runs quick and the dataset small enough to push straight into a CRM.
Large lists and the run's timeout. Records are delivered once the companies of a run have been checked, so a run that the platform stopped at its timeout would deliver nothing. The Actor therefore stops checking companies while there is still time to deliver: it keeps a tenth of a second free for each record still to deliver, on top of the time every run keeps free before its timeout to save its state (a fifth of the time it has left when it starts, at most two minutes). A request to a website or ATS is sent while that leaves at least a second. With less than a request timeout (requestTimeoutSecs, 20 seconds by default) left, it gets the time left as its timeout, so a fast answer near the end still counts and its first attempt never delays the delivery. Its retries keep their usual pauses, a server's Retry-After included, and start only while at least 10 seconds, or one request timeout if that is shorter, are left; a retry that starts with less than a request timeout left can delay the delivery by at most the request timeout less 10 seconds, which comes out of the time kept free to save the state. A request sent with less than that left gets one attempt. A request whose retries were cut short fails like any other: the company is unreachable, free, with its last known record. A company whose request ran out of the time left, whose one attempt failed for a temporary reason with no time left to try again, or that was stopped before its next request, and the companies not started, are not in that run's dataset and are not charged, and what earlier runs stored about them stays as it was. An invalid entry sends no request, so it is still reported while its record can be delivered. The run still succeeds; the log, its status message and RUN_SUMMARY (outcome partial, with the count under leftOut) say how many companies it did not check. Every run checks, and lists, first the companies checked longest ago and those never checked, so the next run of the same list with the same State key starts with the companies this run did not check; when every company on the list was last checked by the same run, the order of your list stays. The default run timeout is 3 hours, set to fit the largest list, 10,000 companies, at the default concurrency and typical request times. A caller can set another timeout in the run options; to check every company of a long list with a shorter one, split the list over several runs. A run without a timeout checks every company.
This is vacancy data. Only job IDs, titles, departments, locations, job URLs and posting dates, and the company name, are collected from the company's own job board. Recruiter, contact and description fields are never read, and an e-mail address, phone number or link typed into a title, department or location is removed. A person's name that a company writes into a job title itself is not detected and is kept as published.
{"companies": ["sendcloud.com", "https://jobs.ashbyhq.com/ashby"],"includeJobList": true,"stateKey": "target-accounts"}
Input
| Field | What it does | Example |
|---|---|---|
companies | Company domains or public job board URLs, one per line. For domains, the ATS is detected automatically. | ["sendcloud.com", "https://jobs.ashbyhq.com/ashby"] |
includeJobList | Include every open job (title, department, location, URL, posting date). Off: only counts and breakdowns, and no job listings are charged. Changing it starts the history over. | true |
maxJobsPerCompany | Caps the job list (newest first) and the job-listing charges per company; totals and breakdowns always cover every open job. Changing it starts the history over. | 1000 |
maxCompaniesPerRun | Hard cap on charged companies in one run; the rest follows in the next run. One run takes at most the 10,000 companies companies accepts, so a higher value changes nothing. | 10000 |
maxPagesPerCompany | How many pages of a company website are loaded to find its ATS. | 5 |
maxConcurrency | Companies processed in parallel. Each one can hold one job board response of up to 20 MB, so the maximum of 20 in parallel can hold 400 MB of responses at once: when you raise it, run the Actor with 1 to 2 GB of memory instead of the default 512 MB. | 5 |
requestTimeoutSecs | Timeout for one request to a website or ATS. Near the run's timeout, a request is sent while at least a second is left beyond what delivering the records needs, with the time left as its timeout when that is shorter, and retried only while 10 seconds (or this timeout, if shorter) are left. | 20 |
stateKey | Separates independent account lists. Defaults to the task ID, or to default for a run started without a task; history is used for first-seen dates and the changes between runs. | enterprise-accounts |
proxyConfiguration | Not needed for ATS endpoints; only if company websites block data-centre traffic during detection. | { "useApifyProxy": false } |
Only companies is required, and for every field except stateKey the Example column is also the default:
{"companies": ["acme.nl", "https://jobs.lever.co/globex", "initech.nl", "werkenbij-hooli.nl"],"includeJobList": true,"maxJobsPerCompany": 200}
For CRM enrichment where you only need counts, set includeJobList to false: you still get totals and breakdowns, and no job listings are charged. Velocity keeps working on the six ATSs that publish posting dates; Homerun publishes none, so its velocity comes from the stored job list and stays empty without it.
Set these two before you schedule. includeJobList and maxJobsPerCompany decide what is stored, so changing either starts the history over: the next run reports every company as added with an empty changes, restarts trackingSince and the first-seen dates, and an unreachable company has no last known record left to deliver. Use a different stateKey per configuration if you want to keep both histories.
Output
Every run delivers one dataset item per company in the standard envelope, also when nothing changed. changeType tells you what changed since the previous run (added, changed, unchanged), summary says it in one line, and payload holds the hiring data:
| Field | Meaning |
|---|---|
payload.input, domain, companyName | The line you entered (your join key), its domain (empty when you entered a job board URL) and the company name as the ATS publishes it: Lever and Ashby publish none, Greenhouse and Recruitee publish it per job, so a board without openings has none |
payload.status | resolved (charged), no-ats-detected, unreachable (last known data kept, dataAsOf shows its date) or invalid-input |
payload.ats, boardUrl, boardId | Detected ATS and public job board |
payload.totalJobs | All open, published jobs |
payload.byDepartment, byLocation | Open jobs per department and location (Unspecified when the ATS has none) |
payload.topDepartments, topLocations | The five largest entries of those breakdowns as one line (Engineering (2); Data (1), then +N more): department and location names differ per company, so a table or CSV column can only show these |
payload.oldestPostedAt, newestPostedAt | Posting date range of open jobs |
payload.hiringVelocity | newJobsLast30Days and its basis: posted-date, first-seen or not-enough-history |
payload.jobs | Job list, newest first: title, department, location, url, postedAt, firstSeenAt |
payload.listedJobs, trackingSince, detectedVia | Jobs in the list (capped by maxJobsPerCompany), since when this company is tracked, and how its ATS was found |
payload.statusReason, checkedUrls | Why a company was not resolved and which pages were checked |
changes, summary | Jobs added and removed since the previous run |
{"envelopeVersion": 1,"id": "acme.example","scope": "acme.example","changeType": "changed","sourceUrl": "https://job-boards.greenhouse.io/acme","scrapedAt": "2026-10-20T06:00:00.000Z","firstSeenAt": "2026-09-15T06:00:00.000Z","hash": "eca6235db6f4071d8ce0d77d48f70030d3319f4e9a2d1e20c4af1507dea87f91","previousHash": "70918f1e9872e400ec4934a77ddd48529d828eee0a1d19c2cde3db6f5956de4d","changes": [{"path": "jobs[4002]","change": "removed","oldValue": {"department": "Sales","firstSeenAt": "2026-09-15T06:00:00.000Z","id": "4002","location": "Remote, Netherlands","postedAt": "2026-06-20T07:00:00.000Z","title": "Account Executive Benelux","url": "https://acme.example/careers?gh_jid=4002"}},{"path": "jobs[4004]","change": "added","newValue": {"department": "Engineering","firstSeenAt": "2026-10-20T06:00:00.000Z","id": "4004","location": "Utrecht, Netherlands","postedAt": "2026-10-10T07:00:00.000Z","title": "Frontend Engineer","url": "https://acme.example/careers?gh_jid=4004"}}],"summary": "Acme B.V.: +1 job (Frontend Engineer), −1 job (Account Executive Benelux), 3 open","diffText": "- jobs[4002]: {\"department\":\"Sales\",\"firstSeenAt\":\"2026-09-15T06:00:00.000Z\",\"id\":\"4002\",\"location\":\"Remote, Netherlands\",\"postedAt\":…\n+ jobs[4004]: {\"department\":\"Engineering\",\"firstSeenAt\":\"2026-10-20T06:00:00.000Z\",\"id\":\"4004\",\"location\":\"Utrecht, Netherlands\",\"pos…","payload": {"input": "acme.example","domain": "acme.example","companyName": "Acme B.V.","status": "resolved","statusReason": null,"ats": "greenhouse","boardUrl": "https://job-boards.greenhouse.io/acme","boardId": "greenhouse:acme","detectedVia": "previous-run","checkedUrls": [],"dataAsOf": "2026-10-20T06:00:00.000Z","totalJobs": 3,"listedJobs": 3,"byDepartment": {"Engineering": 2,"Data": 1},"byLocation": {"Amsterdam, Netherlands": 2,"Utrecht, Netherlands": 1},"topDepartments": "Engineering (2); Data (1)","topLocations": "Amsterdam, Netherlands (2); Utrecht, Netherlands (1)","oldestPostedAt": "2026-09-01T07:00:00.000Z","newestPostedAt": "2026-10-10T07:00:00.000Z","hiringVelocity": {"newJobsLast30Days": 1,"basis": "posted-date","windowDays": 30},"trackingSince": "2026-09-15T06:00:00.000Z","jobs": [{"id": "4004","title": "Frontend Engineer","department": "Engineering","location": "Utrecht, Netherlands","url": "https://acme.example/careers?gh_jid=4004","postedAt": "2026-10-10T07:00:00.000Z","firstSeenAt": "2026-10-20T06:00:00.000Z"},{"id": "4003","title": "Data Engineer","department": "Data","location": "Amsterdam, Netherlands","url": "https://acme.example/careers?gh_jid=4003","postedAt": "2026-09-12T07:00:00.000Z","firstSeenAt": "2026-09-15T06:00:00.000Z"},{"id": "4001","title": "Senior Backend Engineer","department": "Engineering","location": "Amsterdam, Netherlands","url": "https://acme.example/careers?gh_jid=4001","postedAt": "2026-09-01T07:00:00.000Z","firstSeenAt": "2026-09-15T06:00:00.000Z"}]}}
Pricing
Pay per event: you pay for resolved companies and for the jobs you receive. Platform compute and proxy are paid by us, not billed on top of these prices.
| Event | Name | What it means | Price |
|---|---|---|---|
apify-actor-start | Actor start | Charged by Apify when a run starts: once for a run of up to 1 GB of memory, and once more for every extra GB. It covers the first five seconds of compute. | $0.00005 per run up to 1 GB of memory ($0.05 per 1,000) |
company-resolved (primary) | Company resolved | A company whose ATS was found and whose open jobs were collected and aggregated (also when it has 0 openings). | $0.02 per company ($20.00 per 1,000) |
job-listing | Job listing | One open job included in the job list of a resolved company. | $0.0002 per listed job ($0.20 per 1,000) |
These are list prices, which the Free plan pays. Paid Apify plans pay less for every event except the Actor start: 5% on Apify's Bronze tier, 10% on Silver and 15% on Gold and higher.
Example: enriching 500 target accounts weekly, assuming 300 of them have a supported ATS with 12 open jobs on average, costs 300 × $0.02 + 3,600 × $0.0002 = $6.72 per run, plus $0.00005 for the Actor start, or $6.00 plus the start with includeJobList off.
Charged again every run. Every run delivers a complete record per company, also when nothing changed, so every resolved company is charged again. The example list run twice a week costs twice the amount above. includeJobList: false drops the job-listing events; maxJobsPerCompany caps them per company.
Not charged: companies without a detectable ATS, unreachable companies (their last known record is delivered again for free), invalid entries, companies you removed from the list, and companies a run left out because its timeout was near. A run that fails part-way charges only what it delivered before failing, and a run that delivers nothing costs no more than the Actor start event.
Ceilings. maxCompaniesPerRun is a hard cap on charged companies in one run. When that cap or your own Apify spending limit is reached, the Actor stops delivering, says so in the log, and the remaining companies are reported (and charged) in the next run instead.
The most a run can cost. At the default settings one company costs at most $0.22: $0.02 for the company plus at most 1,000 listed jobs (maxJobsPerCompany) at $0.0002 each, which is $0.20. One run takes at most 10,000 companies, so a run with the default settings costs at most 10,000 × $0.22 = $2,200, plus $0.00005 for the Actor start at the default 512 MB of memory ($0.0001 at 2 GB). With includeJobList: false the ceiling is 10,000 × $0.02 = $200, plus the start. Set a maximum cost per run in the run options in Apify Console if you want a lower hard ceiling: the Actor then stops at the first company whose charge, together with its job listings, no longer fits, and says so in the log, so no company arrives with only part of its job list.
FAQ
What is a hiring signal?
A change in a company's hiring (new roles, a new department or location, a spike or a freeze) that indicates a change in priorities and budget. Sales teams use hiring signals to find accounts that are growing and to time their outreach. The data comes from the public job board of the company's own ATS, the same source that feeds the company's careers page: no LinkedIn, no job aggregators, no rendered page scraping.
What if my company's ATS is not detected?
The company is listed with status: "no-ats-detected", the pages that were checked, and it is not charged. If you know the company's job board URL, add that URL instead of the domain. Detection works on plain HTML: career pages that only load links with JavaScript may not be detected. Detection is bounded per company: at most 8 job boards tried per page and 15 in total, at most 30 requests, website pages up to 5 MB and job feeds on a company's own career domain up to 10 MB, both loaded over https only, and job feeds with far more structure than a real board (such as over 200,000 tags and attributes, or tag or attribute names over 256 characters) are not parsed. Within these limits, the time to read a job feed grows with its size, not with how its tags are nested: the costliest feeds allowed took about 0.3 seconds of CPU time at the 10 MB limit, and about half a second at the 20 MB limit for feeds on the vendors' own hosts, on a development laptop, and the tests fail if one of them takes over ten seconds. statusReason says when a limit stopped detection or kept a page or job board from being read.
How is hiring velocity calculated?
The number of open jobs posted in the last 30 days, taken from the ATS posting date as soon as at least 80% of a company's open jobs carry one (basis: "posted-date"). Homerun publishes no posting dates, and a board where too few jobs carry one falls back to the same route: those companies are counted by when each job first appeared in your own runs (basis: "first-seen"), which needs 30 days of history. That fallback can only count jobs earlier runs stored, so it is used only when the complete job list was stored: with includeJobList off, or with more openings than maxJobsPerCompany, a job that was never stored would count as new in every run, and the Actor reports basis: "not-enough-history" with newJobsLast30Days: null instead of a number it cannot stand behind. The basis field always tells you which method was used.
Is hiring signal tracking GDPR-compliant?
The Actor collects job data only. For every ATS it reads only these job fields: ID, title, department, location, job URL and posting date; everything else in the response is discarded while parsing and never stored. Known personal fields (job descriptions, recruiter and hiring-manager fields, Atom author names, dc:creator, Recruitee's mailbox_email) are cut out before parsing; e-mail addresses, phone numbers and links typed into a job title, department or location are scrubbed; referral, recruiter and source parameters are stripped from job URLs; and the output schema is closed, so a record with any field outside the documented list fails validation. Names are not detected: a person's name that a company writes into a job title, department or location is kept as published. The test fixtures contain recruiter names, e-mail addresses, phone numbers, authors and mailboxes in every ATS format, and the tests check the output, the stored state and the logs for each of them. Company names and office locations are business data. How you use the data, for example for B2B prospecting, is your responsibility under the GDPR and your own legitimate interest assessment.
Does it work outside the Netherlands, and what are the limitations?
Yes: Lever's EU instance (api.eu.lever.co) is used automatically, and detection recognises Dutch, English, German and French career page wording. EU Greenhouse board URLs (boards.eu.greenhouse.io) are recognised, and their jobs are requested from the global Job Board API (boards-api.greenhouse.io). The limits: only the seven supported ATSs (companies using Workday, SAP SuccessFactors, Personio or their own careers system are reported as no-ats-detected); only published, public jobs are included, so drafts, internal and unlisted roles stay invisible; and these are public endpoints without a service guarantee. When one is down, slow or rate-limiting, that company comes back as unreachable with its last known record kept, the reason in statusReason and nothing charged, and the next run picks it up again. One failing company never fails the run. The job list holds the ID, title, department, location, job URL and dates of each job only: no job descriptions, salaries, seniority or contract type. Each company is reported from one job board: the homepage is checked first, then its career pages, and detection stops at the first page that has a board with open jobs, taking the board on it with the most open jobs. Jobs on a second board (a second ATS, or a separate board per country) are not counted. The Actor does not check that a linked board belongs to the company; boardUrl shows which board was used, and entering the company's board URL instead of its domain fixes a wrong match. A board that worked before is read first in later runs, and the website is only searched again when that board has no open jobs or cannot be read, so after a move to another ATS the old board is used while it still lists jobs.
Related Actors
- Benelux Insolvency Radar: the opposite signal, corporate insolvencies in NL and BE, matched by KvK or KBO number.
- Dutch Government Announcements Monitor: building permits and zoning plans as leads, per municipality and postcode area.
Support
Missing an ATS, or a company detected incorrectly? Open an issue with the domain and we will look into it.
Last live run against the job boards of sendcloud.com and one Ashby board: 21 September 2026, with the input the Console form opens with.