# NL Hiring Signal Tracker – Open Jobs per Company (`lwsdjfls/nl-hiring-signal-tracker`) Actor

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.

- **URL**: https://apify.com/lwsdjfls/nl-hiring-signal-tracker.md
- **Developed by:** [Leyten Data](https://apify.com/lwsdjfls) (community)
- **Categories:** Business, Lead generation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $17.00 / 1,000 company resolveds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### 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:

```text
https://api.apify.com/v2/datasets/DATASET_ID/items?view=companies&format=csv&token=YOUR_API_TOKEN
```

Or pick exact columns yourself:

```text
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`):

```js
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

1. Put your account list into **Companies**, one per line. A plain domain such as `sendcloud.com` is 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.
2. Run it once and read the `companies` view. 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). Open `jobs` when you want the individual openings behind the count.
3. 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.

```json
{
  "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:

```json
{
  "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 |

```json
{
  "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](https://apify.com/lwsdjfls/benelux-insolvency-radar), matched by KvK or KBO number.
- [Dutch Government Announcements Monitor: building permits and zoning plans as leads](https://apify.com/lwsdjfls/nl-govt-announcements-monitor), 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.

# Changelog

This Actor's version history is a separate document: https://apify.com/lwsdjfls/nl-hiring-signal-tracker/changelog.md

# Actor input Schema

## `companies` (type: `array`):

Company domains (acme.nl) or public job board URLs (https://jobs.lever.co/acme, https://boards.greenhouse.io/acme, https://acme.recruitee.com, https://acme.homerun.co, https://acme.teamtailor.com, https://apply.workable.com/acme, https://jobs.ashbyhq.com/acme). For domains, the ATS is detected automatically; companies without a detectable ATS are listed in the output and not charged.

## `includeJobList` (type: `boolean`):

Include every open job (title, department, location, URL, posting date). Turn off to receive only counts and breakdowns; job listings are then not charged. Velocity keeps working on the six systems that publish posting dates; Homerun has none and needs the stored job list, so its velocity stays empty. Changing this setting starts the history over: the next run reports every company as added, without changes.

## `maxJobsPerCompany` (type: `integer`):

Caps the job list (newest first) and the job-listing charges per company. Totals and breakdowns always cover every open job; only the list and the charge are capped. Changing this setting starts the history over: the next run reports every company as added, without changes.

## `maxCompaniesPerRun` (type: `integer`):

Hard cap on charged companies in one run. One run takes at most the 10,000 companies the input accepts, so a value above that changes nothing. Companies above the cap are delivered in the next run.

## `maxPagesPerCompany` (type: `integer`):

How many pages of a company website are loaded to find its ATS (homepage and career pages).

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

Companies processed in parallel. Each one can hold one job board response of up to 20 MB in memory, so above 8 run the Actor with 1 to 2 GB instead of the default 512 MB.

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

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.

## `stateKey` (type: `string`):

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 for the changes between runs. Stored encrypted, because it often names a customer.

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

Not needed for ATS endpoints. Use it only if company websites block data center traffic during detection.

## Actor input object example

```json
{
  "companies": [
    "sendcloud.com",
    "https://jobs.ashbyhq.com/ashby"
  ],
  "includeJobList": true,
  "maxJobsPerCompany": 1000,
  "maxCompaniesPerRun": 10000,
  "maxPagesPerCompany": 5,
  "maxConcurrency": 5,
  "requestTimeoutSecs": 20,
  "stateKey": "enterprise-accounts",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `companies` (type: `string`):

One row per company: status, ATS, open jobs, new jobs in the last 30 days and how that number was established, the largest departments and locations, and the job board URL.

## `jobs` (type: `string`):

One row per open job: company, title, department, location, posting date and URL.

## `runSummary` (type: `string`):

Companies seen, added and changed, the events charged per type, the estimated cost of the run and any failures.

# 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 = {
    "companies": [
        "sendcloud.com",
        "https://jobs.ashbyhq.com/ashby"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lwsdjfls/nl-hiring-signal-tracker").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 = { "companies": [
        "sendcloud.com",
        "https://jobs.ashbyhq.com/ashby",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("lwsdjfls/nl-hiring-signal-tracker").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 '{
  "companies": [
    "sendcloud.com",
    "https://jobs.ashbyhq.com/ashby"
  ]
}' |
apify call lwsdjfls/nl-hiring-signal-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lwsdjfls/nl-hiring-signal-tracker"
        }
    }
}
```

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/VYspFhUejMiz05YVs/builds/GzdbY5EEB6hI2DNaT/openapi.json
