ATS Jobs Hiring Radar: Greenhouse, Lever, Ashby, Time-to-Fill avatar

ATS Jobs Hiring Radar: Greenhouse, Lever, Ashby, Time-to-Fill

Pricing

from $20.00 / 1,000 company hiring snapshots

Go to Apify Store
ATS Jobs Hiring Radar: Greenhouse, Lever, Ashby, Time-to-Fill

ATS Jobs Hiring Radar: Greenhouse, Lever, Ashby, Time-to-Fill

Scrape ATS jobs from Greenhouse, Lever, Ashby, SmartRecruiters, Recruitee and Personio via their official public APIs, then monitor hiring: new and closed roles with days-to-fill, stale roles, hiring surges and your own keyword signals. No API keys, no proxies. 100 free job rows per run.

Pricing

from $20.00 / 1,000 company hiring snapshots

Rating

0.0

(0)

Developer

Raylable Data

Raylable Data

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

14 hours ago

Last modified

Share

Scrape ATS jobs from Greenhouse, Lever, Ashby, SmartRecruiters, Recruitee and Personio through their official public job-board APIs, then monitor what changes: new roles, closed roles with days to fill, stale roles, hiring surges and your own keyword signals. No API keys, no proxies, no browser.

Try it free: the first 100 job rows of every run cost nothing, and the default input (GitLab on Greenhouse, Notion on Ashby, Palantir on Lever) runs in under 10 seconds.

What does ATS Hiring Radar do?

  • Reads six applicant tracking systems (ATS) through the public job-board endpoints each vendor documents for careers pages. Nothing is scraped from HTML job pages, LinkedIn or job aggregators.
  • Returns every job in one normalised schema: stable uid, the employer's own postedAt and updatedAt, department, location, ISO-2 countryCode, remoteType, seniority (7 levels), function (9 classes), normalizedTitle, apply URL and plain-text description.
  • Tags compensation with its provenance: structured pay ranges from the ATS (source: "ats") or a range parsed from the description (source: "description-regex"), never a guess from bare numbers.
  • Monitor mode keeps per-company history in a named key-value store on your account and, after the first run, returns only new jobs plus closures, 7/30/90-day deltas, surge and contraction flags.
  • Computes time-to-fill from observed closures: daysToClose per closed role, medianDaysToFill90d, p75DaysToFill90d, staleShare and hardToFillFunctions per company.
  • Evaluates your own regex Signal rules against title, description, department or location and emits a typed signal row with an evidence snippet for every match.
  • Pushes fired signals to your HTTPS endpoint during the run, signed with HMAC-SHA256, on top of the normal Apify integrations.

Who is it for?

  • B2B sales and RevOps teams who want to know which target accounts started hiring for a tool they sell, opened leadership roles, or grew open roles by 25% or more in a month.
  • Recruiting agencies hunting mandates: which companies have roles open longer than 60 days in your specialty, and what their median time to fill looks like.
  • Job boards and talent-intelligence products that need a clean multi-ATS feed with employer timestamps, stable ids and compensation provenance instead of re-scraped HTML.
  • Investors and analysts tracking headcount intent per company over months rather than per run.
  • AI agents on the Apify MCP server: one call, a descriptive input schema with titles for every option, pay-per-event pricing and limited permissions.

What you get

Every run writes three kinds of rows to the default dataset, each carrying recordType and runAt, and the dataset has a matching view for each: Jobs, Company snapshots and Signals.

Job rows (recordType: "job")

One row per open role. Example (Ashby, Notion, read live on 2026-09-17):

{
"recordType": "job",
"runAt": "2026-09-17T08:00:00.000Z",
"uid": "ashby:notion:1fc309c8-da20-4ff2-84c7-8b863ece2b0a",
"ats": "ashby",
"companyKey": "ashby-notion",
"company": "notion",
"companySlug": "notion",
"companyDomain": null,
"title": "Software Engineer, Developer Platform",
"normalizedTitle": "software engineer, developer platform",
"department": "Engineering",
"team": "Engineering",
"location": "San Francisco, California",
"city": "San Francisco",
"countryCode": "US",
"secondaryLocations": [],
"remoteType": "hybrid",
"employmentType": "FullTime",
"seniority": "mid",
"function": "engineering",
"postedAt": "2026-08-24T14:44:49.699Z",
"updatedAt": null,
"firstSeenAt": "2026-09-17T08:00:00.000Z",
"daysOpen": 24,
"isStale": false,
"isNew": true,
"compensation": null,
"url": "https://jobs.ashbyhq.com/notion/1fc309c8-da20-4ff2-84c7-8b863ece2b0a",
"applyUrl": "https://jobs.ashbyhq.com/notion/1fc309c8-da20-4ff2-84c7-8b863ece2b0a/application",
"descriptionText": "Notion is building the connected workspace ...",
"language": null,
"source": {
"endpoint": "https://api.ashbyhq.com/posting-api/job-board/notion?includeCompensation=true",
"fetchedAt": "2026-09-17T08:00:01.000Z"
}
}

When the ATS publishes a pay range, compensation is structured and tagged. GitLab's Greenhouse board exposed 86 such ranges out of 217 jobs on 2026-09-17:

"compensation": { "min": 139200, "max": 235200, "currency": "USD", "period": "year", "source": "ats", "raw": "United States Salary Range" }

Company snapshot rows (recordType: "company_snapshot")

One row per company per run. This is where the monitoring value lives:

{
"recordType": "company_snapshot",
"runAt": "2026-09-17T08:00:00.000Z",
"companyKey": "greenhouse-gitlab",
"ats": "greenhouse",
"company": "GitLab",
"companySlug": "gitlab",
"companyDomain": "gitlab.com",
"isBaselineRun": false,
"scanComplete": true,
"capped": false,
"openRoles": 217,
"newThisRun": 6,
"closedThisRun": 4,
"openRolesDelta7d": 2,
"openRolesDelta30d": 31,
"openRolesDelta90d": 40,
"openRolesPct30d": 0.168,
"surge": false,
"contraction": false,
"staleRoles": 57,
"staleShare": 0.264,
"medianDaysToFill90d": 45,
"p75DaysToFill90d": 71,
"closedRolesTracked90d": 9,
"hardToFillFunctions": [
{ "function": "engineering", "staleRoles": 31 },
{ "function": "sales", "staleRoles": 14 },
{ "function": "customer", "staleRoles": 6 }
],
"byFunction": { "engineering": 121, "sales": 44, "marketing": 12, "customer": 15, "operations": 9, "people": 5, "finance_legal": 6, "product_design": 4 },
"bySeniority": { "mid": 74, "senior": 96, "staff": 21, "director": 14, "executive": 3, "junior": 6, "intern": 2 },
"byRemote": { "remote": 212, "hybrid": 3, "onsite": 1 },
"byCountry": { "US": 96, "GB": 18, "DE": 9, "CA": 7 },
"leadershipOpen": 17,
"newFunctions": [],
"newCountries": ["PL"],
"historyRuns": 12,
"firstTrackedAt": "2026-06-01T08:00:00.000Z"
}

The counts above are illustrative for a company tracked for three months; a first (baseline) run has isBaselineRun: true and every delta, closure and time-to-fill field is null. Every extract-mode snapshot has that baseline shape too, with historyRuns: 0 and firstTrackedAt: null, because extract mode stores nothing. Snapshots always describe the whole board: Job filters narrow the job rows and the job-level signals, never openRoles, byFunction or the stale figures.

Signal rows (recordType: "signal")

One row per fired signal, only when something fires:

{
"recordType": "signal",
"runAt": "2026-09-17T08:00:00.000Z",
"signalType": "rule_match",
"companyKey": "greenhouse-gitlab",
"company": "GitLab",
"ats": "greenhouse",
"uid": "greenhouse:gitlab:8638232002",
"title": "AI Transformation Owner, CRO",
"url": "https://job-boards.greenhouse.io/gitlab/jobs/8638232002",
"ruleId": "salesforce-in-jd",
"field": "description",
"evidence": "...hands-on experience with Salesforce, Gainsight and the wider revenue tooling stack...",
"daysToClose": null,
"openedAt": null,
"closedAt": null,
"daysOpen": null,
"openRolesDelta30d": null,
"openRolesPct30d": null,
"function": null,
"countryCode": null,
"seniority": null,
"dedupeKey": "rule_match:greenhouse-gitlab:greenhouse:gitlab:8638232002:salesforce-in-jd"
}

A job_closed signal carries daysToClose, openedAt and closedAt; stale_role carries daysOpen; hiring_surge and hiring_contraction carry openRolesDelta30d and openRolesPct30d; new_function carries function; new_country carries countryCode; leadership_hire carries seniority.

Three more record types are never billed: company_error (input, reason, httpStatus, message), rule_error (ruleId, message) and one run_summary per run, which is also saved to the default key-value store under the key OUTPUT.

Which ATS platforms does it support?

Every endpoint below is the vendor's documented public job-board API, read with plain GET requests and no credentials. Counts were observed live on 2026-09-17.

ATSAccepted input formsEndpoint readCompensation sourceVerified example
Greenhousegreenhouse:gitlab, https://boards.greenhouse.io/gitlab, https://job-boards.greenhouse.io/gitlabboards-api.greenhouse.io/v1/boards/{token}/jobs?content=true&pay_transparency=truepay_input_ranges (ats), else description regexgitlab: 217 jobs, 86 with structured pay ranges, 0.3 s
Leverlever:palantir, https://jobs.lever.co/palantirapi.lever.co/v0/postings/{site}?mode=json (EU boards: api.eu.lever.co)salaryRange (ats), else description regexpalantir: 313 postings, 6 MB, 2 s
Ashbyashby:notion, https://jobs.ashbyhq.com/notionapi.ashbyhq.com/posting-api/job-board/{name}?includeCompensation=truecompensation summary (ats), else description regexnotion: 128 jobs
SmartRecruiterssmartrecruiters:BoschGroup, https://jobs.smartrecruiters.com/BoschGroupapi.smartrecruiters.com/v1/companies/{id}/postings?limit=100&offset=N (paginated)description regexBoschGroup: 4,820 postings (capped by Max jobs per company); smartrecruiters: 1
Recruiteerecruitee:tellent, https://tellent.recruitee.com{company}.recruitee.com/api/offers/salary (ats), else description regextellent: valid empty board
Personiopersonio:personio, https://personio.jobs.personio.de{company}.jobs.personio.de/xml?language=endescription regexpersonio: 1 position

Workable (workable:slug, https://apply.workable.com/slug) is recognised by the input parser but disabled in this version: its documented account endpoint answered, but no board with published jobs could be verified yet, so it returns company_error with reason unsupported_ats instead of silently returning nothing. It will be enabled in a patch release once a populated board has been confirmed.

Not supported: Workday, iCIMS, Taleo, SuccessFactors, Teamtailor, BambooHR, JazzHR and Breezy. These either require tenant tokens or have no documented public feed, and this Actor does not scrape HTML careers pages. A company on one of them returns a company_error row with reason detection_failed or unsupported_ats and is never billed.

How this differs from other ATS job scrapers

Several Store Actors read the same public ATS APIs and return job rows, and they do that job well. ATS Hiring Radar prices job rows at the same market level and adds an analytics layer that none of them ship, based on a review of their READMEs and input schemas on 2026-09-16/17:

CapabilityTypical multi-ATS job scrapersATS Hiring Radar
Job rows from Greenhouse, Lever, Ashby and more, with auto-detected ATSYesYes, six ATS, ats:slug, board URL or plain domain
New-since-last-run flagYes (compares to the previous run only)Yes (isNew), plus closures and deltas against dated history up to 400 snapshots per company
Closed rolesSome flag is_closed per jobjob_closed signal with daysToClose, only from complete scans, feeding medianDaysToFill90d and p75DaysToFill90d
Time-to-fill and stale-role analyticsNot offereddaysOpen, isStale, staleShare, hardToFillFunctions, stale_role signals
Trend horizonsPrevious run only, or stateless7-, 30- and 90-day deltas, openRolesPct30d, surge / contraction
Custom keyword alertsFixed signal types, or filters that drop rowsYour own regex Signal rules on title, description, department or location, each match a typed signal row with ruleId and evidence
CompensationSalary string or parsed numbermin, max, currency, period plus source telling you whether the ATS or a regex produced it
BillingPer job rowJob rows at $0.002 (100 free per run); snapshots and fired signals billed only when produced, so an unchanged watchlist pays only snapshots
Signal deliveryRun-finished webhooksPer-signal batches during the run, HMAC-SHA256 signed, with a retry outbox

How much does it cost to scrape ATS jobs?

ATS Hiring Radar uses pay-per-event pricing. You pay for what a run produces, on top of normal Apify platform usage.

EventEvent name in run_summary.chargedPriceWhen it is charged
Actor startapify-actor-start$0.00005 per run (Apify default, per GB of memory)Once at the start of every run
Job recordjob-record$0.002 per job row ($2 per 1,000)For every job row beyond the first 100 in a run; the first 100 job rows of every run are free
Company hiring snapshotcompany-snapshot$0.02 per company per runOnce per company that returned a board, even when the board is empty; never for companies that returned an error
Hiring signalsignal$0.03 per fired signalOnly when a Signal rule matches or a built-in signal fires

Worked examples:

  • One-company trial (a board with 80 open roles, monitor mode): 80 job rows are inside the free 100, one snapshot: about $0.02.
  • 5,000-job extract across 25 companies in extract mode: 4,900 billable job rows x $0.002 = $9.80, plus 25 snapshots x $0.02 = $0.50: about $10.30. Turn Emit job rows off if you only want the snapshots: $0.50.
  • 100-company weekly watchlist in monitor mode: 100 snapshots x $0.02 x 4.3 runs per month = $8.60, plus new job rows beyond the free 100 per run (typically a few dollars), plus roughly 100-150 fired signals x $0.03 = $3-4.50: about $12-15 per month.

Set a spending limit per run in the Apify Console or API (maxTotalChargeUsd). When the limit is reached, the Actor delivers what still fits for the company being processed, in this order: its snapshot first, then its signals, then its job rows (the free 100 first). The rest of that company's rows and signals are withheld, the remaining companies are skipped, the run_summary gets stoppedReason: "budget" and the run exits SUCCEEDED. Nothing withheld is charged. In monitor mode the withheld job rows are not marked as seen and the withheld signals wait in the state store (up to 1,000 per company), so the next run delivers both, each exactly once; in extract mode nothing is remembered and the next run simply produces everything again. company_error, rule_error and run_summary rows are always free.

How to scrape Greenhouse, Lever and Ashby jobs

  1. Open the Actor on Apify and paste your companies into Companies, one per line: greenhouse:gitlab, https://jobs.lever.co/palantir, notion.so or any mix.
  2. Pick a Mode: Extract all open jobs for a one-off dataset, Monitor changes between runs for a watchlist you will run on a schedule. Extract mode is stateless: it reads nothing from and writes nothing to the state store, returns every open job on every run, one snapshot per company computed from that run alone (isBaselineRun: true; deltas, closures and time-to-fill null) and only rule_match and leadership_hire signals. Monitor mode keeps per-company history and is the mode for closures, trends and the other signals.
  3. Optionally narrow the output with Job filters and add Signal rules.
  4. Click Start. Open the Jobs, Company snapshots or Signals view in the Output tab, or export as JSON, CSV, Excel or via the API.

How to scrape Greenhouse jobs

Input greenhouse:{board token} or the board URL (https://boards.greenhouse.io/{token} and https://job-boards.greenhouse.io/{token} both work). Rows include first_published as postedAt, updated_at as updatedAt, the first department, the primary office, and structured pay ranges where the employer publishes them.

How to scrape Lever jobs

Input lever:{site} or https://jobs.lever.co/{site}. Rows include createdAt as postedAt, categories.department (falling back to categories.team) as department, the Lever country code, workplaceType and salaryRange when present. Requests to Lever are serialised at one per second, so a watchlist of 60 Lever boards takes at least a minute.

How to scrape Ashby jobs

Input ashby:{job board name} or https://jobs.ashbyhq.com/{name}. Rows include publishedAt, department, team, isRemote and workplaceType mapped to remoteType, secondaryLocations, the country from the postal address, and Ashby's compensation summary when the company publishes it. The company must have enabled the public posting API; otherwise the board returns company_error with reason not_found.

How to scrape SmartRecruiters jobs

Input smartrecruiters:{company identifier} or https://jobs.smartrecruiters.com/{identifier}. Identifiers are case-sensitive: BoschGroup returns 4,820 postings while bosch returns zero. Large boards are paginated 100 at a time and capped by Max jobs per company; a capped board has scanComplete: false and capped: true, a partially fetched one scanComplete: false plus a company_error row, and neither produces closures or trends.

How to scrape Recruitee jobs

Input recruitee:{subdomain} or https://{subdomain}.recruitee.com. Rows include published_at, updated_at, the remote and hybrid flags, locations[] with country codes and the structured salary object. Recruiter mailbox addresses present in the feed are dropped.

How to scrape Personio jobs

Input personio:{subdomain} or https://{subdomain}.jobs.personio.de. The XML feed provides createdAt, office, department, employmentType and the job description sections, which are joined into descriptionText.

How does monitor mode work?

  1. Baseline run. The first time a company is seen in the chosen State store name, every job is returned, the snapshot has isBaselineRun: true, deltas and time-to-fill are null, and only rule_match and leadership_hire signals may fire.
  2. Later runs. Only jobs with isNew: true are returned as job rows. Jobs that were in the store but are missing from a complete scan become closures: closedThisRun on the snapshot and a job_closed signal with daysToClose measured from the employer's postedAt (or from firstSeenAt when the ATS gives no date).
  3. Complete scans only. A closure is recorded only when every page was fetched and the board did not hit Max jobs per company. A failed page yields company_error with reason partial_scan and leaves the stored jobs untouched; a capped board has capped: true. Neither writes a usable count into the trend history, so a truncated scan never produces false deltas, surges or contractions, false closures or a broken time-to-fill median.
  4. Mass disappearances are confirmed first. When a complete scan answers zero postings for a board that had roles, or at least five roles making up 80% or more of the board vanish at once (a switched-off board, an ATS incident, an empty payload), nothing is closed yet: the snapshot reports scanComplete: false, a company_error row with reason partial_scan explains the hold, and the roles close on the next complete scan that still misses them, dated at the run in which they first went missing. A genuinely emptied board therefore closes its roles one run later.
  5. Stale roles. isStale is computed on every run, but the stale_role signal alerts when a role becomes stale while you watch it: a role that is already past Stale after (days) when it is first seen (the whole baseline run, or a role that appears later with an old posting date) is counted as stale and never fires, and every role fires at most once.
  6. History. Each run appends a dated snapshot (up to 400 per company; entries from partial, capped or held scans are kept but skipped by the trend calculations). Deltas compare the current count with the latest complete snapshot at least 7, 30 or 90 days old, so a daily and a weekly schedule produce the same 30-day figures.
  7. State lives on your account in a named key-value store, one record per company. Adding or removing companies never resets the others. Use a different State store name per client or per schedule; two concurrent runs on the same store are serialised by a 10-minute lease. A role that leaves a capped board's window is kept for max(2 x Stale after (days), 120) days and then dropped without a closure, and a record never holds more than max(3 x Max jobs per company, 5,000) roles (12,000 at most).
  8. Reset state deletes the listed companies' records so the run becomes a fresh baseline for them.
  9. Schedule it with Apify Schedules (daily for signals, weekly for trends).

Extract mode, by contrast, is stateless: it never reads or writes the state store, so every run returns every open job, a snapshot computed from that run alone and only rule_match / leadership_hire signals; a failed signal webhook batch is reported in run_summary.webhookDeliveries but cannot be retried.

What are hiring signals and time-to-fill?

Definitions used by every snapshot field and signal type:

  • daysOpen = round((now - (postedAt or firstSeenAt)) / 1 day). isStale = daysOpen >= Stale after (days) (default 60).
  • daysToClose = days between openedAt (= postedAt or firstSeenAt) and closedAt (the run in which the job first disappeared from a complete scan; a held mass disappearance keeps that first date).
  • medianDaysToFill90d and p75DaysToFill90d = median and 75th percentile of daysToClose over roles closed in the last 90 days; closedRolesTracked90d = their count.
  • staleShare = staleRoles / openRoles; hardToFillFunctions = the three functions with the most stale roles.
  • openRolesDelta30d = openRoles - open count in the latest complete snapshot at least 30 days old (partial, capped and held scans are skipped, and produce null trends themselves); openRolesPct30d = that delta divided by the older count. surge = openRolesPct30d >= Surge / contraction threshold (%) / 100; contraction = <= the negative of it.
  • newFunctions / newCountries = functions or countries hired for now that never appeared in this company's history.
  • leadershipOpen = open roles with seniority director or executive.

Built-in signal types (choose them under Built-in signal types):

Signal typeFires whenPayload
rule_matchA Signal rule matches a job (new jobs only after the baseline)ruleId, field, evidence, uid, title, url
job_closedA stored job disappears from a complete scandaysToClose, openedAt, closedAt, uid, title, url
stale_roleA tracked role crosses Stale after (days) while you watch it (once per role; a role already stale when first seen, including the whole baseline run, never fires)daysOpen, uid, title, url
hiring_surgesurge is true (once per company per day)openRolesDelta30d, openRolesPct30d
hiring_contractioncontraction is true (once per company per day)openRolesDelta30d, openRolesPct30d
new_functionA function appears for the first timefunction
new_countryA country appears for the first timecountryCode
leadership_hireA new job has seniority director or executiveseniority, uid, title, url

Every signal has a dedupeKey ({signalType}:{companyKey}:{uid or function or countryCode or run date}; rule_match keys end in :{ruleId}, so two rules can match the same job) and, in monitor mode, is emitted and billed at most once per key per state store. Extract mode keeps no state, so identical runs repeat the same rule_match and leadership_hire keys.

Custom signal rules

Signal rules is a JSON array. Each rule has an id, a field (title, description, department or location), a JavaScript regex pattern and optional flags (i, u):

[
{ "id": "salesforce-in-jd", "field": "description", "pattern": "salesforce", "flags": "i" },
{ "id": "leadership-title", "field": "title", "pattern": "\\b(vp|vice president|head of|chief|director)\\b", "flags": "i" },
{ "id": "berlin-sales", "field": "location", "pattern": "berlin", "flags": "i" },
{ "id": "kubernetes-and-terraform", "field": "description", "pattern": "kubernetes[\\s\\S]{0,400}terraform", "flags": "i" }
]

Escaping note: the pattern is a JSON string, so every backslash must be doubled. Write \\b for a word boundary and \\d for a digit. A single \b inside JSON is the backspace character, which the Actor rejects as a control character. Patterns longer than 200 characters, patterns with control characters and patterns that do not compile produce a rule_error row with the reason and are skipped; the run still succeeds. Matching runs on the first 20,000 characters of the field, and evidence is a snippet of at most 160 characters around the first match. In monitor mode rules run on new jobs only after the baseline, so a rule added later fires for roles that appear from then on.

Input

FieldTypeDefaultMeaning
Companiesarray of stringsgreenhouse:gitlab, ashby:notion, lever:palantirats:slug, board URL or plain company domain, one per entry
Modeextract or monitormonitorextract: stateless, every open job every run, snapshot from that run only, only rule_match / leadership_hire; monitor: baseline, then changes from stored history
Emit job rowsbooleantrueWrite job rows; off = snapshots and signals only
Include description textbooleantrueInclude descriptionText in job rows
Max description charactersinteger 200-200004000Truncate descriptionText
Max jobs per companyinteger 1-100001000Cap per company; a capped board has capped: true, no closures and no trends
Max companiesinteger 1-2000500Cap on companies per run
Job filtersobjectall emptytitleKeywords, excludeTitleKeywords, functions, seniorities, countries, remoteTypes, postedWithinDays; applied to job rows and job-level signals before billing (snapshots always describe the whole board)
Stale after (days)integer 7-36560isStale threshold
Surge / contraction threshold (%)integer 5-5002530-day change that raises hiring_surge / hiring_contraction
Signal rulesarray of objectstwo example rulesRegex rules producing rule_match signals
Built-in signal typesmulti-selectall eightWhich built-in signals may fire
State store namestringats-hiring-radar-stateNamed key-value store holding history
Reset statebooleanfalseDelete stored history for the listed companies first
Signal webhook URLHTTPS URLnoneReceives signal batches during the run
Webhook secretsecret stringnoneHMAC-SHA256 key for x-hiring-radar-signature
Concurrencyinteger 1-208Companies processed in parallel (Lever always 1 request/s)
Debug loggingbooleanfalseLog every HTTP request

Minimal API input:

{ "companies": ["greenhouse:gitlab", "ashby:notion", "lever:palantir"], "mode": "monitor" }

Output

  • Dataset rows with recordType in job, company_snapshot, signal, company_error, rule_error, run_summary (fields above).
  • Dataset views Jobs, Company snapshots and Signals in the Output tab; each view lists the columns relevant to one record type. When exporting a single record type, filter on recordType (for example ?fields=...&filter in the API or a column filter in Excel).
  • Key-value store record OUTPUT with the run_summary: mode, companiesRequested, companiesProcessed, companiesFailed, companiesSkipped, jobRowsEmitted, freeJobRowsUsed, snapshots, signalsBySignalType, charged, stoppedReason, durationMs, webhookDeliveries.
  • The named state store you chose (monitor mode only), with one JSON record per company ({ats}-{slug}) and detection_{domain} records for resolved domains (the colon of the logical key detection:{domain} is stored as an underscore, because store keys allow no colon).

Export any view as JSON, CSV, Excel, XML or RSS from the Output tab or through the Apify API dataset endpoints.

Integrations

  • Apify Schedules run the watchlist daily or weekly; monitor mode makes unchanged runs cheap.
  • Signal webhook URL: fired signals are POSTed during the run as JSON arrays of up to 100 signals with headers content-type: application/json, x-hiring-radar-run-id and, when Webhook secret is set, x-hiring-radar-signature: sha256=<hex HMAC-SHA256 of the raw body>. Failed batches are kept in an outbox in the state store and retried first thing on the next run.
  • Apify integrations (Slack, Make, Zapier, n8n, Google Sheets, Airbyte and the run-finished webhooks) work on the dataset as with any Actor; use the Signals view for alerting flows and the Company snapshots view for dashboards.

Use with AI agents (MCP)

The Actor is designed to be called by agents through the Apify MCP server (mcp.apify.com): every input option has a title, description and, where applicable, enumTitles; pricing is pay-per-event only, so an agent's spending limit bounds the cost; and the Actor runs with limited permissions. Example prompt for an agent with the Apify MCP tools: "Use call-actor on ats-hiring-radar with companies greenhouse:gitlab and ashby:notion in extract mode, then list the roles with seniority director or executive and their postedAt."

Use cases

  1. Hiring-intent account list. Companies = your target accounts; Signal rules = the tools or skills you sell; run daily; route rule_match signals to Slack.
  2. Stale-role mandate radar for agencies. Companies = employers in your specialty; Stale after (days) = 45; read stale_role signals and hardToFillFunctions, and pitch on medianDaysToFill90d.
  3. Competitor headcount tracking. Companies = competitors; weekly schedule; chart openRoles, openRolesDelta30d, byFunction and byCountry from the Company snapshots view.
  4. Salary transparency dataset. Extract mode on Greenhouse boards with pay ranges; filter compensation.source = "ats" for employer-published ranges only.
  5. Job board backfill. Extract mode with Include description text on, Max description characters 20000; join on uid across runs and use postedAt / updatedAt for freshness.
  • The Actor reads only the public job-board APIs that Greenhouse, Lever, Ashby, SmartRecruiters, Recruitee and Personio document for embedding their customers' job listings on careers pages. No login, token, proxy or browser is used, and no HTML job page is scraped. The only page the Actor ever fetches is a company's home or careers page when you give a plain domain, and only to find which ATS it links to.
  • Job postings are published by employers for the public. The Actor never touches candidate data, applications or recruiter inboxes; recruiter contact addresses present in some feeds are dropped before output.
  • GDPR note. Output contains company and job information, not personal data. If you enrich the results with personal data downstream, you become the controller for that processing; keep a lawful basis and honour data-subject rights. The state store on your account holds job ids, titles and dates only.
  • Respect the source: keep attribution to the employer's posting URL in anything you republish, and do not repost descriptions wholesale.
  • Rate limits are respected by design: Lever is read at one request per second per its robots.txt, other hosts at most four concurrent requests, with exponential back-off on 429 and 5xx responses.
  • This section is general information, not legal advice. ATS Hiring Radar is an independent tool and is not affiliated with, endorsed by or sponsored by Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee or Personio.

Limits stated plainly

  • A company must have enabled the public posting API on its ATS (always on for Greenhouse and Lever boards, opt-in for Ashby). Boards that are not exposed return company_error with reason not_found.
  • Empty boards are valid: one snapshot with openRoles: 0 and no job rows.
  • SmartRecruiters identifiers are case-sensitive; a wrong-case identifier returns company_error with reason not_found on the first run.
  • Workable is disabled in this version; Workday, iCIMS, Taleo, SuccessFactors, Teamtailor, BambooHR, JazzHR and Breezy are not covered.
  • Closure detection needs at least two runs on the same State store name, and only complete scans produce closures.
  • Boards larger than Max jobs per company are truncated (capped: true): they never produce closures or trends, their stale and time-to-fill figures cover the returned postings only, and roles that leave the truncated window are dropped from the state after max(2 x Stale after (days), 120) days without a closure.
  • A mass disappearance (zero postings for a board that had roles, or at least five roles and 80% of the board gone at once) is held for one run and closes only when the next complete scan confirms it.
  • Domain detection scans the home page and /careers, /jobs, /join-us for known ATS hosts; single-page careers sites that load their jobs via JavaScript may not be detected. Give the ats:slug form instead.
  • The free quota is 100 job rows per run, not per account.

FAQ

How do I find a company's slug?

Open the company's careers page and look at the job-board URL: boards.greenhouse.io/{slug}, jobs.lever.co/{slug}, jobs.ashbyhq.com/{slug}, jobs.smartrecruiters.com/{slug}, {slug}.recruitee.com or {slug}.jobs.personio.de. Paste that URL directly into Companies; the Actor extracts the slug.

Can I just paste a domain?

Yes. gitlab.com is resolved by scanning the home page and the usual careers paths for ATS links, then verifying the candidate board against the ATS API. The result is cached in your state store so later runs skip the scan. If detection fails you get a company_error with reason detection_failed and no charge.

Why did a company return company_error?

Check reason: not_found (board does not exist, is not public, or the identifier has the wrong case), blocked (the ATS answered 401/403), timeout, unsupported_ats (Workable or an ATS outside the list), detection_failed (no ATS link found on the domain) or partial_scan (a page failed, or a mass disappearance is being held for confirmation on the next complete scan; stored jobs left untouched). Error rows are never billed.

How fresh is the data?

Every run reads the live endpoints; nothing is cached between runs except detection results and your history. The employer's own postedAt and updatedAt are passed through unchanged.

Does it need a proxy?

No. The endpoints are public APIs intended for careers pages, so the Actor uses plain requests with a descriptive user agent and no proxy.

How do I get Slack alerts?

Either connect the Apify Slack integration to the run-finished event and read the Signals view, or set Signal webhook URL to a Make, Zapier or n8n webhook that posts to Slack; each signal row already contains company, title, url and evidence.

Can I filter by salary?

Not as an input filter in this version. Export the Jobs view and filter on compensation.min, compensation.max, compensation.currency and compensation.source; rows without a stated range have compensation: null.

How do I reset history?

Turn on Reset state for one run: the listed companies' records are deleted and that run is their new baseline. Other companies in the same store are untouched. To start completely fresh, use a new State store name.

Can I run it per client?

Yes. Give each client its own State store name (for example radar-client-acme) and its own schedule. History, dedupe keys and webhook outboxes are then fully separate.

What happens at my spending limit?

The Actor delivers what still fits for the company it is processing (snapshot first, then signals, then job rows), withholds the rest of that company's output, skips the remaining companies, writes the run_summary with stoppedReason: "budget" and exits with status SUCCEEDED. Withheld rows and signals are not charged. In monitor mode the next run delivers them: withheld job rows are not marked as seen in the state store and withheld signals wait in a per-company queue (up to 1,000). In extract mode the next run produces everything again anyway.

Why does my regex rule produce a rule_error?

Most often because of JSON escaping: \b in JSON is a backspace, so the pattern must read \\b. The message field states the exact problem (control character, too long, or the JavaScript regex compile error).

Do you store my data?

Your dataset and your state store live in your own Apify account. The Actor keeps no copy; the developer sees only aggregate run statistics that Apify shows for every Actor.

Changelog

See CHANGELOG.md for the full history.

  • 0.1.0 (2026-09-17): initial release with six ATS, monitor mode, time-to-fill analytics, eight signal types, custom Signal rules, signed signal webhook and pay-per-event pricing with 100 free job rows per run.
  • 0.1.0 release-candidate fixes (2026-09-21): stale_role fires only for roles that become stale while tracked; extract mode is stateless; partial, capped and held scans no longer feed the trend history; mass disappearances are confirmed before closing; the state record is bounded; signals and rows withheld by a spending limit are delivered by the next monitor run; new capped snapshot field.

Support

Report problems or request an ATS through the Issues tab of this Actor; issues are answered within 14 days, usually much sooner. Include the run id and the company_error rows when a board misbehaves.

If ATS Hiring Radar saves you time, please leave a review on its Store page; reviews help other users find it and tell us what to build next.