ATS Jobs Hiring Radar: Greenhouse, Lever, Ashby, Time-to-Fill
Pricing
from $20.00 / 1,000 company hiring snapshots
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
14 hours ago
Last modified
Categories
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 ownpostedAtandupdatedAt,department,location, ISO-2countryCode,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:
daysToCloseper closed role,medianDaysToFill90d,p75DaysToFill90d,staleShareandhardToFillFunctionsper 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.
| ATS | Accepted input forms | Endpoint read | Compensation source | Verified example |
|---|---|---|---|---|
| Greenhouse | greenhouse:gitlab, https://boards.greenhouse.io/gitlab, https://job-boards.greenhouse.io/gitlab | boards-api.greenhouse.io/v1/boards/{token}/jobs?content=true&pay_transparency=true | pay_input_ranges (ats), else description regex | gitlab: 217 jobs, 86 with structured pay ranges, 0.3 s |
| Lever | lever:palantir, https://jobs.lever.co/palantir | api.lever.co/v0/postings/{site}?mode=json (EU boards: api.eu.lever.co) | salaryRange (ats), else description regex | palantir: 313 postings, 6 MB, 2 s |
| Ashby | ashby:notion, https://jobs.ashbyhq.com/notion | api.ashbyhq.com/posting-api/job-board/{name}?includeCompensation=true | compensation summary (ats), else description regex | notion: 128 jobs |
| SmartRecruiters | smartrecruiters:BoschGroup, https://jobs.smartrecruiters.com/BoschGroup | api.smartrecruiters.com/v1/companies/{id}/postings?limit=100&offset=N (paginated) | description regex | BoschGroup: 4,820 postings (capped by Max jobs per company); smartrecruiters: 1 |
| Recruitee | recruitee:tellent, https://tellent.recruitee.com | {company}.recruitee.com/api/offers/ | salary (ats), else description regex | tellent: valid empty board |
| Personio | personio:personio, https://personio.jobs.personio.de | {company}.jobs.personio.de/xml?language=en | description regex | personio: 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:
| Capability | Typical multi-ATS job scrapers | ATS Hiring Radar |
|---|---|---|
| Job rows from Greenhouse, Lever, Ashby and more, with auto-detected ATS | Yes | Yes, six ATS, ats:slug, board URL or plain domain |
| New-since-last-run flag | Yes (compares to the previous run only) | Yes (isNew), plus closures and deltas against dated history up to 400 snapshots per company |
| Closed roles | Some flag is_closed per job | job_closed signal with daysToClose, only from complete scans, feeding medianDaysToFill90d and p75DaysToFill90d |
| Time-to-fill and stale-role analytics | Not offered | daysOpen, isStale, staleShare, hardToFillFunctions, stale_role signals |
| Trend horizons | Previous run only, or stateless | 7-, 30- and 90-day deltas, openRolesPct30d, surge / contraction |
| Custom keyword alerts | Fixed signal types, or filters that drop rows | Your own regex Signal rules on title, description, department or location, each match a typed signal row with ruleId and evidence |
| Compensation | Salary string or parsed number | min, max, currency, period plus source telling you whether the ATS or a regex produced it |
| Billing | Per job row | Job rows at $0.002 (100 free per run); snapshots and fired signals billed only when produced, so an unchanged watchlist pays only snapshots |
| Signal delivery | Run-finished webhooks | Per-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.
| Event | Event name in run_summary.charged | Price | When it is charged |
|---|---|---|---|
| Actor start | apify-actor-start | $0.00005 per run (Apify default, per GB of memory) | Once at the start of every run |
| Job record | job-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 snapshot | company-snapshot | $0.02 per company per run | Once per company that returned a board, even when the board is empty; never for companies that returned an error |
| Hiring signal | signal | $0.03 per fired signal | Only 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
- Open the Actor on Apify and paste your companies into Companies, one per line:
greenhouse:gitlab,https://jobs.lever.co/palantir,notion.soor any mix. - 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-fillnull) and onlyrule_matchandleadership_hiresignals. Monitor mode keeps per-company history and is the mode for closures, trends and the other signals. - Optionally narrow the output with Job filters and add Signal rules.
- 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?
- 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 arenull, and onlyrule_matchandleadership_hiresignals may fire. - Later runs. Only jobs with
isNew: trueare returned as job rows. Jobs that were in the store but are missing from a complete scan become closures:closedThisRunon the snapshot and ajob_closedsignal withdaysToClosemeasured from the employer'spostedAt(or fromfirstSeenAtwhen the ATS gives no date). - 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_errorwith reasonpartial_scanand leaves the stored jobs untouched; a capped board hascapped: 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. - 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, acompany_errorrow with reasonpartial_scanexplains 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. - Stale roles.
isStaleis computed on every run, but thestale_rolesignal 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. - 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.
- 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).
- Reset state deletes the listed companies' records so the run becomes a fresh baseline for them.
- 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 - (postedAtorfirstSeenAt)) / 1 day).isStale=daysOpen>= Stale after (days) (default 60).daysToClose= days betweenopenedAt(=postedAtorfirstSeenAt) andclosedAt(the run in which the job first disappeared from a complete scan; a held mass disappearance keeps that first date).medianDaysToFill90dandp75DaysToFill90d= median and 75th percentile ofdaysToCloseover 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 producenulltrends 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 withsenioritydirector or executive.
Built-in signal types (choose them under Built-in signal types):
| Signal type | Fires when | Payload |
|---|---|---|
rule_match | A Signal rule matches a job (new jobs only after the baseline) | ruleId, field, evidence, uid, title, url |
job_closed | A stored job disappears from a complete scan | daysToClose, openedAt, closedAt, uid, title, url |
stale_role | A 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_surge | surge is true (once per company per day) | openRolesDelta30d, openRolesPct30d |
hiring_contraction | contraction is true (once per company per day) | openRolesDelta30d, openRolesPct30d |
new_function | A function appears for the first time | function |
new_country | A country appears for the first time | countryCode |
leadership_hire | A new job has seniority director or executive | seniority, 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
| Field | Type | Default | Meaning |
|---|---|---|---|
| Companies | array of strings | greenhouse:gitlab, ashby:notion, lever:palantir | ats:slug, board URL or plain company domain, one per entry |
| Mode | extract or monitor | monitor | extract: 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 rows | boolean | true | Write job rows; off = snapshots and signals only |
| Include description text | boolean | true | Include descriptionText in job rows |
| Max description characters | integer 200-20000 | 4000 | Truncate descriptionText |
| Max jobs per company | integer 1-10000 | 1000 | Cap per company; a capped board has capped: true, no closures and no trends |
| Max companies | integer 1-2000 | 500 | Cap on companies per run |
| Job filters | object | all empty | titleKeywords, 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-365 | 60 | isStale threshold |
| Surge / contraction threshold (%) | integer 5-500 | 25 | 30-day change that raises hiring_surge / hiring_contraction |
| Signal rules | array of objects | two example rules | Regex rules producing rule_match signals |
| Built-in signal types | multi-select | all eight | Which built-in signals may fire |
| State store name | string | ats-hiring-radar-state | Named key-value store holding history |
| Reset state | boolean | false | Delete stored history for the listed companies first |
| Signal webhook URL | HTTPS URL | none | Receives signal batches during the run |
| Webhook secret | secret string | none | HMAC-SHA256 key for x-hiring-radar-signature |
| Concurrency | integer 1-20 | 8 | Companies processed in parallel (Lever always 1 request/s) |
| Debug logging | boolean | false | Log every HTTP request |
Minimal API input:
{ "companies": ["greenhouse:gitlab", "ashby:notion", "lever:palantir"], "mode": "monitor" }
Output
- Dataset rows with
recordTypeinjob,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=...&filterin the API or a column filter in Excel). - Key-value store record
OUTPUTwith therun_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}) anddetection_{domain}records for resolved domains (the colon of the logical keydetection:{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-idand, 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
- Hiring-intent account list. Companies = your target accounts; Signal rules = the tools or skills you sell; run daily; route
rule_matchsignals to Slack. - Stale-role mandate radar for agencies. Companies = employers in your specialty; Stale after (days) = 45; read
stale_rolesignals andhardToFillFunctions, and pitch onmedianDaysToFill90d. - Competitor headcount tracking. Companies = competitors; weekly schedule; chart
openRoles,openRolesDelta30d,byFunctionandbyCountryfrom the Company snapshots view. - Salary transparency dataset. Extract mode on Greenhouse boards with pay ranges; filter
compensation.source = "ats"for employer-published ranges only. - Job board backfill. Extract mode with Include description text on, Max description characters 20000; join on
uidacross runs and usepostedAt/updatedAtfor freshness.
Is it legal to scrape ATS job boards?
- 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_errorwith reasonnot_found. - Empty boards are valid: one snapshot with
openRoles: 0and no job rows. - SmartRecruiters identifiers are case-sensitive; a wrong-case identifier returns
company_errorwith reasonnot_foundon 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-usfor known ATS hosts; single-page careers sites that load their jobs via JavaScript may not be detected. Give theats:slugform 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_rolefires 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; newcappedsnapshot 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.