ATS Jobs Openings Scraper – 10 ATS Platforms, One Schema
Pricing
from $0.70 / 1,000 job postings
ATS Jobs Openings Scraper – 10 ATS Platforms, One Schema
Live jobs from 10 ATS platforms — Greenhouse, Ashby, Lever, Recruitee, Workable, SmartRecruiters, Workday, Teamtailor, Personio and Breezy — in one schema. Department, team, city/region/country and salary parsed where the ATS exposes it. Filter before you pay.
Pricing
from $0.70 / 1,000 job postings
Rating
0.0
(0)
Developer
Fetch Smith
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
ATS Jobs Scraper – Greenhouse, Ashby, Lever, Recruitee, Workable, SmartRecruiters, Workday, Teamtailor, Personio & Breezy
Live job postings straight from any company's own career board on Greenhouse, Ashby, Lever, Recruitee, Workable, SmartRecruiters, Workday, Teamtailor, Personio or Breezy — 10 ATS platforms in one call, normalized into one schema no matter which ATS the company uses.
In short: a hiring page scraper and applicant tracking system API in one call — no browser, no login, just the postings each company's own ATS already exposes publicly.
What it does
- Fetches each company's public job-board API directly (no browser, no login) for Greenhouse, Ashby, Lever, Recruitee, Workable, SmartRecruiters, Workday, Teamtailor, Personio and Breezy — 10 ATS platforms combined reaching well over 10,000 career sites, including many large employers no other field in this list reaches.
- Don't know which ATS a company uses? Leave
atsoff (or set it to"auto") and give just the slug — the Actor checks all 9 non-Workday platforms for you and uses whichever one is real. See Don't know which ATS a company uses?. - Normalizes every posting into one shared schema (title, department, team, employment type, workplace type, remote flag, location, salary where the ATS exposes it, timestamps, apply URL, description).
- Client-side filters (title keyword + exclude, location keyword + exclude, remote-only, has-salary, posted-after date) are applied before charging, so you only pay for postings you actually want.
- Companies that have migrated off an ATS (very common for Lever) are skipped with a warning, not a failed run.
- Pay per result: you are charged only for job postings actually returned. No start fee.
Use cases
- Job-board aggregation — pull live openings from every company you track across 10 different ATSes into one feed, instead of maintaining 10 separate scrapers.
- Recruiting/sourcing intelligence — spot when a target company opens a new role in a specific team or location, with
department/team/locationalready normalized so you can filter without per-ATS cleanup. - Salary benchmarking —
salaryMin/salaryMax/salaryCurrency/salaryIntervalgive a normalized comparable across Ashby, Lever, Recruitee and Greenhouse postings that publish pay ranges. - Remote-work tracking —
remoteOnlyplus the normalizedworkplaceType/isRemotefields build a remote-jobs feed across every ATS at once, not just the ones with a "remote" search filter. - Skill/tech-stack sourcing —
descriptionKeywordsearches the full posting body, so you can pull every role at 50 companies that mentionsKubernetes,Rustorvisa sponsorship— requirements that almost never appear in the job title. - Hiring-trend research —
postedAfter/postedBeforepluspublishedAtlet you track how fast a company (or a whole market segment) is opening new roles over time. - Job alerts / new-posting watch — set
watchLabeland a scheduled run returns only the roles that opened since the last run, not the same open-roles list every time. You are charged for new postings only (see Watch mode).
Input
| Field | Type | Description |
|---|---|---|
companies | array | [{"ats": "auto|greenhouse|ashby|lever|recruitee|workable|smartrecruiters|workday|teamtailor|personio|breezy", "slug": "<company-slug>"}]. The slug is the company identifier in their career-board URL, e.g. jobs.ashbyhq.com/ramp → {"ats":"ashby","slug":"ramp"}. Omit ats or set it to "auto" to skip knowing which platform a company uses — the Actor tries all 9 non-Workday platforms for that slug and keeps whichever one actually has a board. Workday's slug is "<host>/<site>" (see below) since a Workday board has no single short identifier, and it always needs an explicit "ats":"workday" — it cannot be auto-detected. |
titleKeyword | string | Only keep postings whose title contains this text. |
titleExcludeKeyword | string | Drop postings whose title contains this text, e.g. "Senior" to filter out senior roles. |
locationKeyword | string | Only keep postings whose location/city/region/country contains this text. |
locationExcludeKeyword | string | Drop postings whose location/city/region/country contains this text, e.g. a country you don't hire in. |
employmentTypeKeyword | string | Only keep postings whose employmentType contains this text, matched case- and separator-insensitively (spaces/hyphens/underscores are ignored on both sides, so "full-time", "full time" and "fulltime" all match Ashby's "FullTime"), e.g. "full-time" or "part-time" — works on every ATS that publishes the field except Personio, whose closed vocabulary (permanent/intern/temporary/working_student) contains neither word — use "permanent" there instead. Each ATS uses its own vocabulary beyond that ("contract" matches nothing on Workday, Recruitee or Personio), so check the employment-type table below first. Postings where the ATS never exposes this field are dropped when set — on Greenhouse and Teamtailor boards that is every posting (see Employment type below). |
departmentKeyword | string | Only keep postings whose department, team or departmentPath contains this text, e.g. "Engineering". Postings where the ATS never exposes a department are dropped when set. |
descriptionKeyword | string | Only keep postings whose description text contains this text, e.g. "Kubernetes" or "visa sponsorship" — the requirements a job title never mentions. Matched against the plain text, not the HTML. |
descriptionExcludeKeyword | string | Drop postings whose description text contains this text, e.g. "security clearance". |
hasSalary | boolean | Only keep postings that come back with a salary — an ATS compensation field (Ashby, Lever, Recruitee) or a pay-transparency range printed in the posting body (Greenhouse). Default false. |
minSalary / maxSalary | integer | Only keep postings whose salary range overlaps this floor/ceiling, e.g. minSalary:150000. Compared against the number as posted — no currency conversion, no pay-interval normalization (check salaryCurrency/salaryInterval on the row, since ranges mix year/month/week/day/hour). Postings with no salary never match either. |
remoteOnly | boolean | Only keep postings the ATS marks as remote. Default false. |
postedAfter | string | YYYY-MM-DD (or full ISO datetime); only keep postings published on/after it. The named day is included. An unparseable date stops the run — see "Date bounds" below. |
postedBefore | string | YYYY-MM-DD (or full ISO datetime); only keep postings published on/before it — the named day is included in full. Pair with postedAfter for a date window (e.g. everything a company opened in one quarter). |
includeDescriptions | boolean | Include full HTML + plain-text description. Default true. The description filters still work with this off — the text is fetched, matched, then dropped from the rows. |
maxJobsPerCompany | integer | Cap on how many postings that pass your filters are kept per company — filters are applied first, then this cap. Default 50 — kept low so a run with no input at all (e.g. an API caller omitting the field) stays fast and cheap; pass a higher value explicitly for bulk pulls (up to 5000). |
maxResults | integer | Cap total postings returned across all companies. Default 300 — same reasoning as above; pass a higher value explicitly for bulk pulls (up to 100000). |
watchLabel | string | Optional. Name a saved job alert and return only postings not delivered under that label + filter set before. First run per label is a free baseline. See Watch mode. |
watchEvents | array | Optional. Restrict a watch to new and/or salaryAdded postings (empty = both). Ignored unless watchLabel is set. See Watch mode. |
webhookUrl | string | Optional. POST a small JSON completion summary (postings pushed, companies scanned/errored, dataset ID, watch new/skipped counts) here when the run finishes — see FAQ. |
proxyConfiguration | object | Apify Proxy config. Default: Apify Proxy on. |
Don't know which ATS a company uses? Use "ats": "auto"
If you only have a guess at the slug (e.g. the company's own name, lowercased) and not the platform, pass {"slug": "<slug>"} with no ats, or {"ats": "auto", "slug": "<slug>"}. The Actor tries Greenhouse, Ashby, Lever, Recruitee, Workable, SmartRecruiters, Teamtailor, Personio and Breezy for that exact slug in parallel and keeps whichever platform actually has a board there — atsSource on the returned rows shows what it found. If the same slug happens to be a real, populated board on more than one platform (rare, but generic slugs like demo can collide), it keeps the one with postings; if more than one has postings, it logs the ambiguity and picks by a fixed priority order. If no platform has that slug at all, the company is skipped and reported the same as a wrong explicit ats guess (see below) — nothing is charged for it. Auto-detect costs the same as specifying the platform directly: only the postings actually returned are charged. Workday is never auto-detected — its slug format is "<host>/<site>", not a bare company name, so there's nothing to guess; always pass "ats": "workday" explicitly for it.
Finding a company's slug
- Greenhouse:
boards.greenhouse.io/<slug>or thegh_jid-style URL on their careers page. - Ashby:
jobs.ashbyhq.com/<slug>. - Lever:
jobs.lever.co/<slug>. Many well-known companies have migrated to a different ATS — if the slug 404s, they're no longer on Lever and the Actor skips them without failing. Unlike Greenhouse, Lever's API honors real pagination, so the Actor only pulls roughly 3xmaxJobsPerCompanypostings off the board (not the whole thing) — on a 4,000+-posting board (e.g.jobgether) that is the difference between a ~1 MB request and a 50 MB one for a typicalmaxJobsPerCompany. - Recruitee:
<slug>.recruitee.com. - Workable:
apply.workable.com/<slug>. - SmartRecruiters:
jobs.smartrecruiters.com/<slug>(identifier is case-sensitive, e.g.BMWDealerCareers). - Workday: use
"<host>/<site>"from the board's own URL, e.g.okgov.wd1.myworkdayjobs.com/okgovjobs→{"ats":"workday","slug":"okgov.wd1.myworkdayjobs.com/okgovjobs"}. Thewd<N>host segment and the site name are both required and vary per company — copy them straight from the address bar on the company's own Workday career page (https://<host>/<site>). - Teamtailor:
<slug>.teamtailor.com, e.g.oatly.teamtailor.com→{"ats":"teamtailor","slug":"oatly"}. The board is a JSON Feed with an embedded schema.orgJobPostingper item, so location/date/organization come free — no per-job detail call. - Personio:
<slug>.jobs.personio.de, e.g.ottonova.jobs.personio.de→{"ats":"personio","slug":"ottonova"}. An empty board (no open roles) is a normal result, not an error. - Breezy:
<slug>.breezy.hr, e.g.the-san-francisco-standard.breezy.hr→{"ats":"breezy","slug":"the-san-francisco-standard"}. Breezy publishes structuredcountry/region/cityplus a realis_remoteflag, and a free-textsalaryfield is parsed intosalaryMin/salaryMax/salaryCurrency/salaryIntervalwhen the posting includes one.
Output
One row per job posting:
company, atsSource, jobId, title, department, team, departmentPath, employmentType, workplaceType, isRemote, location, secondaryLocations, country, region, city, salaryMin, salaryMax, salaryCurrency, salaryInterval, publishedAt, updatedAt, jobUrl, applyUrl, descriptionHtml, descriptionText, scrapedAt
descriptionHtml is the posting body as the ATS serves it, with one exception: inline base64 image payloads are dropped. Some Recruitee boards paste screenshots straight into the job description (one real posting measured 2,436,168 bytes, 99.6% of it a single PNG, against a 6,150-byte median) — the <img> tag is kept with the image type and byte count in place of the payload (src="data-uri-stripped:image/png:2426348"), so you still see that an image was there without paying storage for it. descriptionText is unaffected either way.
Sample row (Greenhouse, descriptionHtml/descriptionText truncated — the real fields carry the full posting body):
{"company": "airbnb","atsSource": "greenhouse","jobId": "8184174","title": "Account Manager","department": "2. Business","team": "Business Development","departmentPath": ["2. Business", "Sales & Partnerships", "Business Development"],"employmentType": null,"workplaceType": "Hybrid","isRemote": false,"location": "London, United Kingdom","secondaryLocations": [],"country": "United Kingdom","region": "England","city": "London","salaryMin": 46000,"salaryMax": 54000,"salaryCurrency": "GBP","salaryInterval": "year","publishedAt": "2026-09-09T04:35:19-04:00","updatedAt": "2026-09-29T20:00:54-04:00","jobUrl": "https://careers.airbnb.com/positions/8184174?gh_jid=8184174","applyUrl": "https://careers.airbnb.com/positions/8184174?gh_jid=8184174","descriptionHtml": "<div class=\"content-intro\"><p>Airbnb was born in 2007...</p></div>...","descriptionText": "Airbnb was born in 2007 when two hosts welcomed three guests...","scrapedAt": "2026-10-14T06:33:25.097Z"}
employmentType is null here because Greenhouse never publishes it (see Employment type below) — not a bug in this row. salaryMin/salaryMax come from the pay-transparency range printed in this posting's body (workplaceType: "Hybrid"), one of the sources listed under Department and team.
Department and team (changed in 0.1.36)
A Greenhouse job carries exactly one department, and it is the leaf of the board's own org tree — on a large board that is an internal cost-centre node, not a department anyone would group by. Measured over 1,909 live postings across 5 boards, 76% sit at depth ≥ 1 in the tree (up to depth 3 on Stripe: Tech > Security (Planning Org) > Security (Planning Group) > 8611 Security Analytics).
Since 0.1.36 this Actor also reads the board-level department endpoint — one extra request per company, not per job — rebuilds the chain, and reports:
| Field | Value |
|---|---|
department | the top-level department ("Tech") |
team | the leaf the posting actually sits in ("8611 Security Analytics"), null when the job is already top-level |
departmentPath | the whole chain root→leaf as an array, null on sources with no hierarchy |
The point is cross-company grouping: distinct department values on the same live boards collapse from 191 → 4 (Stripe), 44 → 13 (Databricks), 29 → 3 (Airbnb), 17 → 5 (Discord). team went from null on every Greenhouse row to filled on 76%.
departmentKeyword now matches against department, team and departmentPath together, so a filter you wrote against a leaf name before 0.1.36 still matches — the filter got strictly wider, never narrower. If the departments endpoint is unavailable for a board, department falls back to the leaf name exactly as before and team is null. Ashby and Lever expose a real team field of their own and are unchanged; Recruitee, Workable, SmartRecruiters and Workday have no team concept and return null.
Workday's board-listing API doesn't carry department, employmentType or an exact publishedAt date — those only come back from a per-job detail call, which this Actor makes automatically for every kept posting when includeDescriptions is true (the default) or when you set a filter that needs one of those fields (employmentTypeKeyword, departmentKeyword, postedAfter, postedBefore, descriptionKeyword, descriptionExcludeKeyword) — so those filters work on Workday boards even with includeDescriptions: false. With descriptions off and none of those filters set, the detail call is skipped for a faster, cheaper-in-wall-clock run and those three fields come back null for Workday postings only (every other ATS is unaffected either way). department is populated from the posting's hiring organization name (e.g. "131 DEPARTMENT OF CORRECTIONS"), which is what Workday boards actually expose in place of a dedicated department field.
Employment type
Greenhouse boards never publish an employment type, so employmentType is always null on Greenhouse rows and setting employmentTypeKeyword drops the entire board (measured: airbnb, 163 live postings → 0 rows, while the same "full" filter returns rows on Ashby and Lever). The run logs a warning naming the company when this combination is used. This is an upstream gap, not a fetch this Actor can defer: Greenhouse's public job-board API has no such field. Filter Greenhouse companies on titleKeyword ("intern", "contract") or descriptionKeyword instead. Ashby (FullTime), Lever (Regular Full Time (Salary)), Recruitee, Workable and SmartRecruiters all expose it from the listing; Workday exposes it via the per-job detail call described above.
employmentType is raw, not a shared vocabulary — each ATS spells "full-time" with a different (or no) separator: Ashby FullTime (no separator), Lever Full-time/Full-Time (hyphen, casing varies by company), Workable Full-time (hyphen), Recruitee fulltime_permanent/fulltime_fixed_term (underscore, lowercase), SmartRecruiters a plain label like Part-time/Intern. employmentTypeKeyword matching ignores spaces, hyphens and underscores on both sides, so "full-time" (the most natural way to type it) matches all of the above — measured live on ashby:ramp (155 postings, all FullTime/Intern/Temporary): before this fix "full-time" returned 0 rows while "fulltime" and "full" both returned the expected matches, since a bare .toLowerCase() never bridged the missing hyphen. If you need to distinguish a specific wording (e.g. permanent vs. fixed-term on Recruitee), filter on a longer, more specific substring instead ("fixed_term" or "fixedterm" both work).
What each ATS actually puts in employmentType (measured live 2026-10-10 — ~3,200 postings across 19 real boards; the "values seen" column is exactly what that sample contained, not a vendor-documented list). Separator style is already handled for you; vocabulary is not, so this is what you can filter on:
| ATS | Shape | Values seen live (sample) | "full-time" | "contract" |
|---|---|---|---|---|
| Ashby | closed, camelCase | FullTime, Contract, Intern, Temporary (1,112 postings, 3 boards) | ✅ | ✅ |
| SmartRecruiters | closed label | Full-time, Part-time, Intern (104 postings, 3 boards) | ✅ | not seen in sample |
| Workable | closed label | Full-time only (7 postings, 1 board — thin sample) | ✅ | not seen in sample |
| Recruitee | code, tenure baked in | fulltime_permanent, fulltime_fixed_term, parttime_fixed_term (13 postings, 1 board) | ✅ | ❌ — use "fixed-term" or "permanent" |
| Lever | free text the employer types | 18 distinct values on 8 boards (1,913 postings): Full Time Employee, Full-time, Full Time/Part Time, Seasonal, Part Time, Permanent, Internship, International EOR, Remote, Short Term, Full Time Contractor, … (2% null) | usually | rarely — varies by board |
| Workday | closed, two values only | Full time, Part time — nothing else | ✅ | ❌ never, on any board |
| Greenhouse | not published at all | always null | ❌ | ❌ |
| Breezy | closed label | Full-Time, Contract, Temporary, Other (9 postings, 2 boards) | ✅ | ✅ |
| Personio | closed, lowercase snake_case | permanent, intern, temporary, working_student (25 postings, 5 boards) | ❌ — use "permanent" | ❌ — use "temporary" |
| Teamtailor | schema.org field exists, never populated | always null (67 postings, 5 boards) | ❌ | ❌ |
Full write-up of this table, including why we normalise separators but deliberately do not normalise vocabulary: "full-time" is not a filter.
Only the Workday row is a claim about the whole vocabulary rather than about a sample: Workday publishes its own timeType facet, so the full value set is directly readable (see below). For the other ATSes, treat the table as "these definitely exist and definitely match" — a board may use a value this sample never hit.
- Personio's vocabulary doesn't contain the word "full" or "time" at all — a permanent, full-time role is just
permanent, soemploymentTypeKeyword: "full-time"returns 0 rows on every Personio board (measured:ottonova/clark/orderbird/exporto/personioown board, 25 postings, 20permanent). Filter on"permanent"instead;"contract"also matches nothing — the closest Personio gets to contingent work istemporaryorworking_student. - Teamtailor's
employmentTypeis a real schema.org field the Actor reads, but it wasnullon every one of 67 live postings sampled across 5 boards (oatly,career.teamtailor.com,schibsted,doconomy, plus a confirmed-empty board atvivino) — treat it as unavailable on this ATS and filter ontitleKeyword/descriptionKeywordinstead, same advice as Greenhouse.
Two of these are worth planning around:
- Lever's value is not a vocabulary. It is whatever the employer typed into Lever's "commitment" field, so it carries things that aren't employment types at all (
Remote,International Office Entity,International EOR) and splits the same concept four ways across boards (Full-time/Full Time/Full Time Employee/Permanent). Filter Lever on a short substring ("full","intern") rather than an exact phrase, or usetitleKeywordinstead. - Workday publishes the contract/intern dimension, but not per posting. Its
timeTypefield — the only employment type on the job detail payload — is a closedFull time/Part timepair (verified against Workday's owntimeTypefacet on three unrelated tenants: 443/81, 2674/2, 1521/14). The classification you probably want (Regular,Temporary,Seasonal,Intern,Contractor,Fixed Term,Apprentice) lives in a separateworkerSubTypefacet that Workday exposes only as a board-level facet — it is absent from both the listing rows and the per-job detail payload, so there is no per-posting value for this Actor to return. UsetitleKeyword("intern","contract") on Workday boards. The run logs a warning naming the board wheneveremploymentTypeKeywordcan't match it.
Location
See the full write-up for which platforms are structured vs free text and why offices[]/addressRegion are both traps, not shortcuts. location is always the board's own text. city, region and country are the normalised breakdown, and both locationKeyword and locationExcludeKeyword match against all four plus secondaryLocations — so locationExcludeKeyword: "United States" drops a Greenhouse posting whose text only reads "San Francisco, CA", and locationKeyword: "California" keeps it. Measured on the 163 live airbnb postings: locationKeyword: "California" returns 15 rows, only 1 of which says "California" in the board's own text.
Ashby, Lever, Recruitee, SmartRecruiters, Workable and Breezy publish structured geography, so it is passed straight through. Greenhouse publishes none — it has a single hand-typed string per posting, and the same board mixes "United States", "San Francisco, CA", "Bengaluru, India", "Remote - Texas" and "Northeast - United States". This Actor normalises that string against a country/state/city vocabulary: measured over 1,914 live postings (airbnb, databricks, figma, stripe, discord, September 2026) it fills country on 95%, city on 67% and region on 50%.
Breezy's city/region/country come from real structured fields (location.city/location.state.name/location.country.name), not text-parsed — measured on the 2 live boards found with postings (the-san-francisco-standard, breezy.breezy.hr's own demo tenant, 9 postings): all 9 filled country, region and city with no nulls. Teamtailor's city comes from its embedded schema.org jobLocation[].address.addressLocality, also structured, but region is deliberately left null — measured across 5 live boards (67 postings: oatly, career.teamtailor.com, schibsted, doconomy, plus confirmed-empty vivino), the underlying addressRegion field is missing on 49% (33 of 67, e.g. every schibsted/doconomy posting) and, on the 51% where it is populated, it's either a continent name ("North America", "Oceania", "Europe") or a repeat of the country itself ("United States", "Sweden", "Germany") — never a real state/province, so shipping it as region would mislabel it either way. Personio's only location field (office) is free text, same shape as Greenhouse, and reuses the same parseLocation normaliser: across the same 25-posting, 5-board sample used for employment type above, country and city each filled on 76% (19 of 25) and region on 0% (none of these boards' office strings carry a state/province token for the normaliser to match). The misses are real German place names the normaliser's vocabulary doesn't cover — "Singen am Hohentwiel", "Konstanz" and the field-sales label "Außendienst Köln" ("field sales Cologne," which doesn't match on the embedded city name either) all come back null rather than a guess, plus the literal placeholder "Remote".
Workday publishes no geography in its list payload either — just one text field per posting, written differently by every tenant ("US, CA, Santa Clara", "France - Paris", "Canada, Toronto"). The same normaliser runs on it, so Workday rows now carry city/region/country instead of the nulls they returned before. Measured over 800 live postings (nvidia, salesforce, adobe, okgov, September 2026): from the list payload alone country fills on 35%, city 22% and region 14%; when includeDescriptions is on, the per-job detail call adds Workday's one structured field and country reaches 100% (its spelling "United States of America" is normalised to "United States" so it groups with every other source). The detail call also resolves multi-site postings the list shows only as "3 Locations", which is where most of the extra city/region comes from. The practical effect on filtering: locationKeyword: "United States" matched 0 of those 800 Workday postings before and matches 89 now; "California" goes from 6 to 70.
Fixed in 0.1.39: on county-government boards where the list text has no place name at all (okgov's "Muskogee County", "Craig County", ...), locationKeyword/locationExcludeKeyword used to be checked before the per-job detail call that resolves country, silently dropping every one of those postings even though the resolved value would have matched. Measured live on okgov: locationKeyword: "United States" went from 0 of 10 to 10 of 10, with locationExcludeKeyword: "United States" now correctly complementary (0 kept). Billing unchanged (1 job event per returned row).
Re-measured after the fix (September 2026), and the "89 of 800" figure above is now a floor, not current: a fresh live pull of nvidia (list text already parseable, unaffected by the bug) and the full visible okgov board (60 of 60 postings, not just the earlier 10-row sample) matched locationKeyword: "United States" on 101–102 of 120 postings (≈84%), run twice a minute apart (live boards drift slightly). The county-government board alone went from the old code's 0 of 60 to the fixed code's 60 of 60 — the earlier 10-row proof generalizes to the whole board, not just the sample.
Anything the vocabulary does not recognise stays null rather than being guessed, because a wrong country is worse than a missing one. In practice that means: multi-country zones ("EMEA", "APAC"), placeholders ("N/A", "Remote"), internal abbreviations some boards use for their own offices ("SF", "CHI", "ATL"), and the bare token "Georgia", which is equally the US state and the country. Where a posting lists several places, the fields describe the first one and the rest stay in secondaryLocations. Greenhouse's offices[] array is deliberately not used to fill these fields: on some boards it contradicts the posting outright (a databricks posting located in "Finland" carries a "Denmark" office), so trusting it would invent wrong answers. The same restraint applies to Workday: its "3 Locations"/"8 Locations" placeholders, "Statewide", county names ("Tulsa County") and the street addresses some tenants put in the detail location ("Stringtown - 13001 N Highway 69") all stay null rather than becoming a bogus city.
Salary
Pay is returned when the board itself publishes it, and left null otherwise rather than guessed — Ashby, Lever and Recruitee expose structured pay ranges in their APIs, and Greenhouse boards with pay transparency switched on render the range as a structured block in the posting itself, which this Actor reads. Workable, SmartRecruiters and Workday carry no compensation data through any standard field, so those stay null — SmartRecruiters' one documented exception is a freeform, per-company customField that genuinely carries pay text on one real board checked, not parsed here since the field's label and format are admin-configured and not part of the API's contract (see the SmartRecruiters deep-dive).
Coverage measured on live boards (September 2026): Ashby ramp 141/148 postings, Greenhouse airbnb 139/166, databricks 472/874, figma 99/152. Greenhouse boards that never turned pay transparency on (stripe, discord) return null — there is nothing in their payload to read.
Two Greenhouse specifics worth knowing. The currency comes from the ISO code the board prints, not from the symbol — 17 of airbnb's ranges pay in CAD and still show $, so reading the symbol would mislabel them USD; where a board prints a bare $ with no code, salaryCurrency stays null instead of guessing. A salary printed in prose is not parsed — only Greenhouse's structured pay block is read, so a sentence like "the base salary range for this position is $196,000 to $220,500" buried in the description is left alone rather than risk picking up some other number from the text.
salaryMin/salaryMax are also filled on Greenhouse when includeDescriptions is false — the pay block is read out of the payload either way, so switching descriptions off to save bandwidth does not cost you the salary.
salaryInterval says what the number actually means — year, month, week, day or hour. Each ATS spells its period differently (1 YEAR, monthly, per-year-salary); they are normalized to one vocabulary so an hourly rate and an annual salary are never silently compared.
Watch mode (only new postings since last run)
Set watchLabel to any name you like ("backend-remote-eu") and the run stops returning the whole open-roles list every time and starts returning only the postings that appeared since the previous run under that label. This is the job-alert shape: schedule it hourly or daily and each run's dataset is your diff.
- The first run for a label is a free baseline. It records which postings are currently open (up to 5,000), returns zero rows and charges nothing. Run it again later to get what's new.
- Already-delivered postings are dropped before any charge, so a run with nothing new costs you nothing.
- The baseline lives in your own Apify account — a named key-value store
fetchsmith-ats-watch, keywatch-<label>-<fingerprint>. Nothing is kept on our side. - The fingerprint covers the company list and every filter (
titleKeyword,titleExcludeKeyword,locationKeyword,locationExcludeKeyword,employmentTypeKeyword,hasSalary,remoteOnly,postedAfter,includeDescriptions, plusdepartmentKeyword,descriptionKeyword,descriptionExcludeKeyword,postedBefore,minSalaryandmaxSalarywhen you set them). Change any of them and you get a fresh baseline instead of a dump of postings the old filters had excluded.maxJobsPerCompany/maxResultsare not in the fingerprint — they are cost caps, not criteria. maxJobsPerCompanycaps what is delivered, not what is checked. In watch mode the run scans the whole match set for each company (so a new role that sorts 40th is still found) but still delivers at mostmaxJobsPerCompanynew postings per company per run; the rest arrive on the following run.- Baseline size cap: the recorded baseline holds at most 20,000 posting ids per label; once a label crosses that, the oldest ids are dropped to make room. A dropped id is re-delivered — and re-charged — as "new" on a future run. This only bites a very broad, unfiltered watch on a long company list; narrowing
titleKeyword/locationKeywordor trimmingcompanieskeeps a label well under the cap. A run that actually drops ids logs a warning and says so in its status message. watchEventspicks which kinds of change get delivered. By default (empty) a watch reports both a brand-new posting and an already-delivered posting that gained a salary since you last saw it (watchEvent: "new"/"salaryAdded", withpreviousHasSalaryon the changed row). Greenhouse, Ashby, Lever and Recruitee postings commonly publish without a salary and add one later under the same posting id — a pay-transparency add-on or a plain edit — and without this event that edit was invisible to a watch forever; SmartRecruiters/Workable/Workday never carry a salary, sosalaryAddedsimply never fires on those. Pick just["salaryAdded"]to be alerted only on that and never pay for a brand-new posting.salaryAddedonly starts firing on the run after a baseline recorded before this feature existed — the run that upgrades an old baseline has nothing to compare a salary against yet.- Never pin a watch to
postedBefore. A newly-published posting'spublishedAtis its own post time, so once today's real date passes a fixedpostedBefore, nothing new can ever fall on or before that cutoff again — the watch goes permanently quiet while still reporting a healthy "nothing new" every run. A run in that state logs two warnings and setswatchDeadDateBound: truein thewebhookUrlpayload.postedAfteralone is safe to keep in a watched query; it only trims old postings, it never excludes new ones.
Pricing
Pay per result: $0.001 per job posting on the free plan, dropping to $0.0007 on Gold and above (Bronze $0.00085, Silver $0.00075), with no Actor-start fee. 1,000 postings costs $1.00 on the free plan, $0.70 on Gold. You are charged only for postings that pass your filters and actually reach your dataset.
Correction (verified live 2026-10-04, cycle 1242): the price was cut 2026-09-26 and this whole section still described the old, higher ladder ($0.0015 FREE → $0.001 Gold+) for over a week — every crossover number below was computed against that stale price and is now rewritten against the live one. meta.json and the live Actor were never out of sync with each other, only with this README's prose; bin/check-pricing doesn't read competitor-comparison sentences, only the structured tier table, so it couldn't catch this.
Honest comparison against the real 7-ATS rivals (re-verified live 2026-10-04): most multi-ATS scrapers in the Store only cover 5 platforms (Greenhouse, Lever, Ashby, SmartRecruiters, Workday) — missing both Recruitee and Workable — or swap Workable for Personio. softyways/greenhouse-lever-ashby-workday-job-scraper (3 users) genuinely matches our exact 7-platform set and also ships new/updated/removed-posting change tracking, but at its flat $0.0015/job + $0.00005 start fee we're now cheaper at every single tier, including FREE (not just from Bronze up as before the cut).
One correction that reverses a prior finding entirely: fetch_cat/ats-jobs-scraper raised its price roughly 24x since we last checked it — it used to be our single biggest undercutter (tiered $0.000115/job FREE down to $0.000028 DIAMOND) and is now tiered $0.002829 (FREE) down to $0.001476 (GOLD+), plus a $0.0020211 start fee — dearer than us at every tier even before its start fee is added. Retracted, not re-scoped: it is no longer a threat in any form.
openclawai/career-site-ats-jobs-scraper (44 users, re-verified live 2026-10-19) still auto-detects the company from a name or URL across 60+ ATSes — including all 7 of ours plus systems like SuccessFactors, iCIMS and Taleo — at a flat $0.0005/job with no start fee: half our own FREE rate and about 30% below our GOLD+ rate, at every volume, though it's a generic any-ATS tool with no department/location field normalisation and no salary-aware watch mode.
wickfeed/ats-job-aggregator (30 users, re-verified live 2026-10-16) covers 5 of our 7 platforms (missing SmartRecruiters and Workday) at a flat $0.001/job with no start fee — that now exactly ties our FREE tier rather than beating it, and it's dearer than every paid tier of ours (our Bronze/Silver/Gold+ are all below its flat $0.001). No longer a genuine undercutter past the FREE tier.
webdata_labs/greenhouse-lever-ashby-jobs-scraper (69 users, Personio instead of Workable) is tiered $0.001 (FREE/BRONZE) → $0.0008 (SILVER) → $0.0006 (GOLD+): ties our FREE tier, is dearer than our BRONZE/SILVER, and only beats us (by ~14%) from GOLD up.
automation-lab/multi-ats-jobs-scraper (164 users, no Recruitee or Workable) charges a $0.005 start fee + tiered $0.00115 (FREE) → $0.001 (BRONZE) → $0.00078 (SILVER) → $0.0006 (GOLD) → $0.0004 (PLATINUM) → $0.00028 (DIAMOND). Its own per-job rate is now higher than ours on FREE/BRONZE/SILVER, so the start fee makes it dearer than us at every volume on those three tiers; it only turns cheaper on GOLD and above, and only past real volume — crossover is ~50 jobs/run at GOLD, ~17 at PLATINUM, ~12 at DIAMOND. At DIAMOND scale it's still the cheapest large-volume option found (1,000 jobs: $0.285 vs our $0.70).
blackfalcondata/greenhouse-scraper (48 users, Personio instead of Workable) charges a flat $0.00095/job + a $0.005 start fee: its per-job rate just barely beats our FREE tier ($0.00095 vs $0.001), so it only turns cheaper than us past ~100 jobs/run on FREE, and it's dearer than us at every volume on Bronze/Silver/Gold+ (its flat $0.00095 is above all three).
What we ship that the cheaper ones don't: real Recruitee/Workable coverage, Greenhouse/Workday department-hierarchy normalisation (department/team/departmentPath, cross-company grouping), location normalisation to city/region/country on Greenhouse and Workday's free-text fields, and watch mode with a salaryAdded re-delivery event on top of new-posting detection.
Full-cohort sweep, re-verified live 2026-10-05 (cycle 1281, user counts re-checked cycle 1282): 44 previously-unnamed listings with >=3 users live-priced (not a top-N cut), 5 genuine new findings. davidbenittah/career-page-job-change-monitor is the cheapest rival found in this niche to date: a flat $0.00005 per job posting, no start fee — 20x under our FREE tier — covering Lever, Greenhouse, Ashby, Workable, Personio and Recruitee (5 of our 7, plus Personio, which we don't support; missing Workday and SmartRecruiters). datahamster/ats-jobs undercuts us at every tier with no start fee: tiered $0.0005 (FREE/BRONZE) → $0.00045 (SILVER) → $0.0004 (GOLD and above), on Greenhouse/Lever/Ashby/Workday (4 of 7; missing Recruitee, Workable, SmartRecruiters), plus a separate monitor-mode charge we don't have a direct equivalent for. cirkit/ats-job-boards-scraper charges a flat $0.0007/job + a $0.00005 start fee on Greenhouse/Ashby/Lever only (3 of 7) — undercuts our FREE/BRONZE/SILVER tiers and ties our GOLD-and-above rate exactly. fetch_cat/career-page-job-postings-scraper (a different Actor from the already-named fetch_cat/ats-jobs-scraper above) tiers $0.000575 (FREE) down to $0.00014 (DIAMOND) plus a $0.005 one-time start fee — its per-job rate beats ours at every tier, but the start fee means it only turns cheaper than our FREE tier past roughly 12 jobs in a single run. andok/ats-jobs-scraper (Greenhouse/Lever/Ashby/Workable) has the glitchbound shape from our court-records-scraper audit: both its per-job rate ($0.002 FREE → $0.00028 DIAMOND) and its Actor-start fee ($0.02 FREE → $0.0028 DIAMOND) are tiered, so it's dearer than us at every volume on FREE/BRONZE/SILVER/GOLD, but crosses under us on PLATINUM past ~45 jobs/run and on DIAMOND past ~7 jobs/run. The other 39 of the 44 are dearer at every tier, narrower in ATS scope (single-platform specialists on Lever/Workday/Workable alone), a different shape entirely (per-company "hiring signal"/change-feed products, not per-posting exports), or carry a start fee large enough ($0.05–$0.20) to erase any per-job saving at realistic volumes.
Resweep (cycle 1327, verified live 2026-10-06): the >=3-user unnamed cohort moved to 39 listings (composition changed; same methodology) and came back genuinely clean — every one of the 39 is dearer than our GOLD+ rate at every tier, and the two closest ties (vnx0/lever-ats-job-scraper, 8 users, and chilly_damask/company-careers-job-scraper, 10 users (corrected this cycle from 8, found stale by bin/check-claim-coverage), both flat $0.001/job, Lever-only and Greenhouse/Lever/Ashby respectively) only match our FREE tier and are dearer than our BRONZE/SILVER/GOLD+. No new undercutter. illehius/ats-jobs-scraper grew 1→4 users and remains impossible to disclose either way: its Store listing marks a $0.00001 apify-default-dataset-item as the primary display event alongside an unused-looking $0.001 job-scraped event, and confirmed this cycle that our plan cannot run public Actors at all (403 public-actor-disabled) to see which one the code actually calls — this is a permanent limitation of our current Apify plan, not a one-off gap to revisit. All headline-named rivals above (davidbenittah, datahamster, cirkit, both fetch_cat listings, andok, softyways, blackfalcondata, bovi, scrapesage, wickfeed, webdata_labs, automation-lab, jobo.world, memo23, enosgb, get_anything, k1ra, i-scraper, deadwood_data_solutions, both dalleyne listings) re-verified live, full tiered price maps byte-for-byte unchanged — only openclawai/career-site-ats-jobs-scraper's user count had drifted (19→22, corrected above).
Resweep (cycle 1363, verified live 2026-10-07): the >=3-user unnamed cohort grew to 41 listings (composition changed again; same methodology, full cohort live-priced, no top-N cut). One genuine new partial undercutter: ninhothedev/ats-jobs-scraper (3 users, Greenhouse + Lever only — 2 of our 7) charges a flat $0.0008/job + a $0.00005 start fee — under our FREE ($0.001) and BRONZE ($0.00085) tiers, but dearer than our SILVER ($0.00075) and GOLD+ ($0.0007), plus the start fee it has and we don't. The other 40 of the 41 are dearer than us at every tier, narrower in ATS scope (single-platform specialists), a different shape (per-company hiring-signal/change-feed monitors, not per-posting exports — scrapersdelight/gtm-trigger-feed, sapph1re/public-ats-job-change-feed, qualifyops/ats-hiring-signal-finder, tribloc/company-hiring-signals), or price well above ours even on their cheapest tier (nexgendata/tech-hiring-signals-ats-jobs $0.04 flat, qualifyops/ats-hiring-signal-finder $0.01 flat, freshactors/greenhouse-lever-jobs-scraper $0.02→$0.006). dstyx/ats-job-feed-actor is the closest shape-match found — its tiered ladder ($0.001 FREE → $0.0009 BRONZE → $0.0008 SILVER → $0.0007 GOLD+) ties our FREE and GOLD+ rates exactly but runs dearer on BRONZE/SILVER, so it is a near-mirror of our own ladder, not an undercut.
This niche was never promoted into our internal multi-term sweep until cycle 1401 (verified live 2026-10-08) — and that was the gap, not the per-cycle resweeps above. Every resweep since cycle 1281 searched Store listings for the literal phrase "ats jobs", but this niche's own listings almost never write that phrase: they name the specific platforms instead ("Greenhouse Jobs Scraper", "Workday Job Scraper"). Widening the match to the platform names themselves surfaced the single biggest listing in this entire niche, previously invisible end to end: fantastic-jobs/career-site-job-listing-api (8,067 users — 16x bigger than bovi below, the previous largest). It and its four sibling listings from the same vendor are all dearer than us at every tier despite the scale: fantastic-jobs/career-site-job-listing-api ($0.012 FREE → $0.004 GOLD+, plus a $0.0001 start fee, 58 ATS platforms + AI/LinkedIn enrichment), fantastic-jobs/career-site-job-listing-feed (1,461 users, $0.0025 → $0.0008 plus a $0.001 start fee), fantastic-jobs/greenhouse-jobs-api (923 users, Greenhouse only, $0.002 → $0.0012), fantastic-jobs/ashby-jobs-api (491 users, Ashby only, same ladder), and fantastic-jobs/jobs-scraper (129 users, $0.0022 → $0.001). piotrv1001/company-career-page-scraper (453 users) covers 8 platforms including Oracle HCM — one more than our 7 — at $0.003 → $0.00225 plus a $0.00005 start fee, dearer than us throughout. Two real but narrow undercutters: shahidirfan/Workday-Job-Scraper (421 users, Workday only) ties our FREE per-row rate ($0.00099 vs $0.001) but its $0.0005 start fee means it only turns cheaper than our FREE tier past ~50 jobs/run, and it's dearer than our BRONZE/SILVER/GOLD+ regardless of volume; automation-lab/greenhouse-jobs-scraper (217 users, Greenhouse only — a different Actor from the already-named automation-lab/multi-ats-jobs-scraper) has a $0.01 start fee that erases its cheaper per-row rate ($0.0004 DIAMOND vs our $0.0007) until ~34 jobs/run, and never catches up on FREE/BRONZE/SILVER/GOLD. A dozen more single-platform Workday/Greenhouse/Lever specialists surfaced the same way (miladamirzadeh, crawlerbros, glitchbound, benthepythondev's Greenhouse/Workday/Lever trio, santamaria-automations, fabro.dev, shahidirfan's Workable listing) — all dearer than us at every tier, most with their own start fee on top. The wider match (~800 Store listings, ~45 read closely so far) is far bigger than any prior sweep reached; the remaining long tail is still unread and queued for the next resweep.
The niche's biggest listings still don't undercut us — and the price cut has widened that lead, including over the single biggest listing in the niche (verified live 2026-10-04): bovi/greenhouse-lever-ashby-job-scraper (490 users — bigger than every other rival named above combined) makes the same Workable-for-Personio swap webdata_labs does and its near-flat tiered job-result price ($0.0015 FREE down to only $0.001425 DIAMOND) used to beat us at FREE; we're now cheaper than it at every single tier, including FREE. scrapesage/multi-ats-job-scraper (59 users) used to undercut us at DIAMOND ($0.00075 vs our old $0.001) — we're now cheaper than it at every tier there too (its own ladder runs $0.003 FREE down to $0.00075 DIAMOND, and our DIAMOND of $0.0007 beats even that). jobo.world/ats-jobs-api (769 users) and memo23/career-site-ats-jobs-api (235 users, re-verified live 2026-10-16) are general-purpose ATS scrapers spanning 75+ and 116 platforms respectively, far broader than our 7 — but both price well above us at every tier ($0.004 down to $0.0013, and $0.004 down to $0.0025 plus a $0.03 start fee) and neither does ATS-specific field normalisation. enosgb/ats-job-scraper (129 users, Rippling instead of Recruitee or Workable) also prices above us at every tier (tiered $0.003 FREE down to $0.0012 Gold+, plus a $0.00005 start fee). get_anything/ats-jobs-scraper (50 users), k1ra/ats-jobs-scraper (47 users) and i-scraper/ats-jobs-scraper (42 users) charge flat $0.0015-$0.002/job plus a start fee with no plan discount at all, dearer than us at every tier by an even wider margin than before the cut. The closest anyone comes to matching our exact 7-platform scope besides softyways above is deadwood_data_solutions/greenhouse-lever-ashby-workable-jobs-api — its job-record ladder ($0.0015 FREE down to $0.00135 DIAMOND) is now clearly dearer than ours at every tier (not just "comparable" as before the cut), on top of a flat $0.01-$0.009 per-run query charge we don't have. Two single-ATS specialists are also bigger by users than most of the multi-ATS rivals above: dalleyne/greenhouse-job-scraper (166 users, Greenhouse only) and dalleyne/ashby-job-scraper (52 users, Ashby only) both charge a tiered $0.002 (FREE/BRONZE) down to $0.0013 (GOLD+) per job plus a $0.00005 start fee — dearer than our own $0.001→$0.0007 at every tier by a wider margin than before, and each covers one platform where we cover seven.
Resweep (cycle 1433, verified live 2026-10-08): closed the long tail cycle 1401 left unread. The platform-name sweep now matches 814 listings (up from ~800); the full >=3-user unnamed cohort — 197 listings, not a top-N cut — was live-priced. Two genuine broad-scope undercutters, each an any-company multi-ATS scraper like ours rather than a single-platform specialist: eiv/company-jobs-scraper (22 users, re-verified live 2026-10-16, Greenhouse/Lever/Ashby/SmartRecruiters/Workable/Recruitee plus Personio and Breezy — all 7 of ours and more) was recorded here as a flat $0.0008/job, no start fee, beating us at every tier — corrected cycle 1470 (verified live 2026-10-09): that was never quite right and has since drifted further. Its $0.0008/job rate alone already sits above our SILVER ($0.00075) and GOLD+ ($0.0007), so it could never have beaten us at every tier; it has also since gained a $0.005 one-time Actor-start fee and a $0.004 per-company "company-resolved" fee (charged once per company read, even ones with zero open roles) that cycle 1433 didn't carry. Folding both in, it only turns cheaper than our FREE tier past ~45 jobs for a single company (or ~180 jobs/company to beat BRONZE) and never catches SILVER/GOLD+/PLATINUM/DIAMOND at any volume; shahidirfan/Career-Site-Job-Listing-API (19 users, any ATS via URL) charges a flat $0.00099/job, no start fee — ties our FREE rate almost exactly but undercuts BRONZE/SILVER/GOLD+. A large swarm of single-platform spinoffs from vendors already named above or elsewhere in this niche also undercut us for their one platform, though none matches our 7-platform breadth: five memo23 listings split out of its already-named career-site-ats-jobs-api (memo23/workday-jobs-api 36u $0.0005 flat, memo23/smartrecruiters-scraper 26u $0.0009 flat, memo23/lever-jobs-scraper 24u $0.00099 flat, memo23/greenhouse-jobs-scraper 23u $0.00079 flat, memo23/ashby-jobs-scraper 22u $0.0009 flat); four fetch_cat listings distinct from its already-named/already-retracted ones (fetch_cat/workday-jobs-scraper 9u (re-verified 2026-10-18) and fetch_cat/greenhouse-jobs-scraper/fetch_cat/lever-jobs-scraper/fetch_cat/workable-jobs-scraper 3u each, all tiered down near $0.00001-$0.0003); three getascraper "monitor"-framed listings (greenhouse-requisition-monitor, workday-tenant-jobs-monitor, getascraper/smartrecruiters-jobs-monitor, 3 users each, tiered $0.0007-$0.0009 FREE down to $0.0005-$0.0007); johnvc/ashby-job-board-scraper (19u, tiered $0.0005→$0.0004); shahidirfan/ashby-jobs-api (16u, $0.0009 flat); apt_marble/greenhouse-jobs-scraper (3u, $0.0008 flat); and ninhothedev/smartrecruiters-jobs-scraper (3u, $0.0005 flat, from the vendor already named above for Greenhouse+Lever). None of these ships our department/location normalisation or salary-aware watch mode, and none covers more than one of our 7 platforms. Three more are priced near-zero but are not real substitutes on inspection: alizarin_refrigerator-owner/unified-ats-api-ashby-breezy-hr-workable (8u, $0.00001 flat) covers only Ashby/BreezyHR/Workable (2 of our 7, missing Greenhouse/Lever/Recruitee/SmartRecruiters/Workday); nomad-agent/company-careers-bundle (6u, re-verified live 2026-10-16, $0.0002 flat) and nomad-agent/lever-jobs-scraper ($0.0001 flat) both cover a fixed, named list of employers (16 companies, or one — DNB — respectively), not arbitrary companies, so neither substitutes for a general per-company scraper; starbright_overlap/ats-database (7u, FREE) is a company→ATS lookup tool, not a job-postings feed, out of scope entirely. The other roughly 172 of the 197 priced are dearer than us at every tier, narrower, or a different shape (hiring-signal/change-feed monitors), consistent with every prior sweep of this niche.
Resweep (cycle 1470, verified live 2026-10-09): grown tail, two new every-tier undercutters, both narrower than our 7-platform scope. niche-size now matches 819 listings (up from 814 at 1433); the full >=3-user unnamed cohort — 182 listings, not a top-N cut — was live-priced fresh (composition churn from 1433's 197, not a real drop). Two genuine new undercutters beat us at every tier, same shape as eiv/shahidirfan above: bujhmml/ats-jobs-scraper (28 users, Greenhouse/Lever/Ashby only — 3 of our 7) charges a flat $0.0004/job + a $0.00005 start fee — under even our GOLD+ rate; dami_studio/career-site-jobs-scraper (3 users) auto-detects a company's ATS from just a URL, domain or name across 10 unnamed hiring systems and charges a flat ~$0.00055/job + a $0.001 start fee — also under our GOLD+ at any real volume. Two more are real but only cross under us at higher tiers/volume: vamsi-krishna/workday-jobs-scraper (27 users, Workday only) mirrors our exact ladder shape — ties FREE/BRONZE, then undercuts SILVER ($0.0007 vs ours $0.00075) and GOLD+ ($0.00055 vs ours $0.0007); steadydata/company-career-site-jobs covers 6 of our 7 platforms (everything but SmartRecruiters) plus 9 more we don't (SuccessFactors, iCIMS, UKG, Oracle, Dayforce, JazzHR, BambooHR, Paylocity, Teamtailor), up to 200 boards/run, no start fee — ties our FREE, dearer on BRONZE/SILVER, undercuts GOLD+ ($0.00065). Three smaller, lower-priority finds: arman-bd/ashby-jobs-scraper (3 users) and maydit/ashby-jobs-scraper (1 user, Ashby only) only cross under us at GOLD+/PLATINUM/DIAMOND volumes; parsebird/workable-job-scraper (3 users, Workable only) ties our FREE rate almost exactly ($0.00099) and is dearer everywhere else. emastra/hiring-signal-tracker (6 users) is the same hiring-signal/growth-metrics shape already excluded elsewhere in this niche — headcount and role-velocity trends, not a job-postings feed — not a substitute. The other roughly 174 of the 182 priced are dearer than us at every tier, single-platform and narrower, or a different shape, consistent with every prior sweep. Also re-verified the niche's biggest previously-named rivals live (fantastic-jobs/career-site-job-listing-api 8,150 users, bovi 507, jobo.world 796, memo23 222, piotrv1001 454, dalleyne/greenhouse-job-scraper 167) — zero price drift beyond the eiv correction above.
Resweep (cycle 1787, verified live 2026-10-16): top of the tail by users, clean. niche-size now matches 1,455 listings (hand-curated 15-term sweep, re-run under the cycle-1780 limit=1000 fix); the full unnamed cohort is 1,386, of which 251 have >=3 users. Live-priced the top 30 of those 251 by user count (not a random cut — the highest-user listings are the ones most likely to have reached real adoption) rather than the full 251 in one sitting; the remaining ~221, all under the 30th listing's user count, are queued for a follow-up resweep. Zero new undercutters in this batch. The two closest: code-node-tools/job-listings-scraper (57 users, 180+ job boards/ATS platforms) and igolaizola/greenhouse-companies (33 users) both run the exact same ladder shape as ours ($0.001 FREE → $0.0007 GOLD+ with the same Bronze/Silver steps) — a tie at FREE and GOLD+, dearer on Bronze/Silver, not an undercut anywhere. gooyer.co/myworkdayjobs (253 users) is genuinely free (no pricing record at all) but Workday-only (1 of our 7 platforms) with a generic templated description showing no evidence of field normalisation, salary parsing or watch mode — not a substitute for a paying buyer who needs more than raw Workday postings. Every other listing in the top 30 (hirebase/job-search 315u, samstorm/hiring-intent-lead-scraper 106u, conserving_celerytop/live-career-page-jobs-api 43u, and 25 more) prices well above us at every tier, is a different shape (hiring-intent/lead-scoring, not a postings export), or is narrower in ATS scope — consistent with every prior resweep of this niche since cycle 1281.
Resweep (cycle 1805, verified live 2026-10-16): closes the ~221-listing tail cycle 1787 left unread — the full cohort, not a top-N cut. niche-unnamed now matches 1,479 listings (up from 1,455 at 1787, composition churn); the full unnamed cohort is 1,404, of which 256 have >=3 users — all 256 live-priced in one sweep via bin/_batch_price_ats3.py, 0 unresolvable. One genuine new broad-scope partial undercutter: wildorigins/company-jobs (5 users) covers all 7 of our platforms plus Teamtailor and Personio, by company domain — ties our FREE/BRONZE tiers exactly, then undercuts SILVER ($0.0007 vs ours $0.00075), GOLD ($0.0006 vs $0.0007), PLATINUM ($0.0005) and DIAMOND ($0.0004) — the same broad-scope-undercutter shape as eiv/shahidirfan/steadydata above, now one more entry in that set. Three narrower every/most-tier undercutters, each missing most of our platform breadth: ax_dev/greenhouse-lever-ashby-job-scraper (1 user, re-verified 2026-10-18, Greenhouse/Lever/Ashby/SmartRecruiters — 4 of our 7) flat $0.00009/job, under even our GOLD+ rate; hipersoft/ashby-scraper (4 users, Ashby only) ties FREE then undercuts BRONZE through DIAMOND ($0.0008 → $0.0005); clearpath/workday-jobs-scraper (3 users, Workday only) undercuts FREE and GOLD/PLATINUM/DIAMOND but is dearer on BRONZE/SILVER (a non-monotonic ladder, not a clean undercut). The rest of the 256 are not real substitutes on inspection: netorb/netorb-web-scraper (5u) is restricted to Canadian employers only; hiraware/greenhouse-jobs (4u) is genuinely free but Greenhouse-only with a generic templated description (same gooyer.co shape as cycle 1787); pranayjsathish/job-market-intel (14u, free) and fantastic-jobs/expired-jobs-api-for-career-site-job-listing-api (35u, a companion feed to the already-named fantastic-jobs/career-site-job-listing-api that serves only already-expired postings once a day) are a different shape, not a live-postings export; scrapebench/ats-career-page-jobs-scraper (5u) charges a flat per-run fee with no per-row event, break-even against our GOLD+ rate at just 2 jobs/run — dearer than us beyond that for the rest of the run; two nomad-agent listings (greenhouse-jobs-scraper, ashby-jobs-scraper) are each hard-coded to one or two named employers (Stripe/Figma, or Ashby's own board), the same fixed-employer-list exclusion already applied to this vendor's other listings above. This closes the backlog item left open since cycle 1787 — the full >=3-user unnamed cohort is now live-priced, not just its top 30.
FAQ
How do titleKeyword, locationKeyword, descriptionKeyword, remoteOnly and the date bounds combine? All of them must pass (AND) — set only the ones you need, leave the rest empty/false.
Can I filter on the description without paying for description columns? Yes. descriptionKeyword/descriptionExcludeKeyword work with includeDescriptions: false: the text is fetched, matched against your keyword, then stripped from the row, so you get lean rows filtered on their full body text.
What if companies has the same {ats, slug} entry twice? Deduped automatically — an exact repeat is only fetched (and charged) once per run.
A watch already delivered this posting once — why is it back? Because its salary changed. Watch mode diffs an already-delivered posting's salary state against what was recorded last time, not just its id — a posting that publishes without a salary and later adds one (common on Greenhouse/Ashby/Lever/Recruitee under pay-transparency rules) comes back once as watchEvent: "salaryAdded", with previousHasSalary: false on the row, and is charged like any other delivered posting. Set watchEvents: ["new"] if you only ever want brand-new postings and never a salary-change re-delivery.
My watch says "nothing new" every single run — is it broken? Check whether you set postedBefore. A watch combined with a fixed upper date bound is a dead end: every brand-new posting is dated "now", so once the calendar passes your cutoff no new posting can ever satisfy it again, and the run honestly reports zero new rows forever. Such a run now logs two warnings and sets watchDeadDateBound: true in the webhookUrl payload so a scheduled watch can alert on it instead of trusting the quiet. Fix it by removing postedBefore from the watched query — keep postedAfter if you want a floor — or drop watchLabel if you genuinely wanted a one-off historical window rather than an ongoing watch. (Changing the filter starts a fresh free baseline, since postedBefore is part of the watch fingerprint.)
What happens if a board has a transient connection blip mid-run? Every ATS request (Greenhouse, Ashby, Lever, Recruitee, Workable, SmartRecruiters and Workday, including Workday's paged listing calls) is retried up to 3 times on a connection-level failure — measured at roughly 1 fresh request in 4 for HTTP/2 faults, 2026-09-21 — before that company is marked errored in the completion summary. A single blip no longer costs you that company's whole board.
Does maxJobsPerCompany cap before or after my filters run? After. Filters are applied first, then up to maxJobsPerCompany of the matching postings are kept per company — so {"titleKeyword": "engineer", "maxJobsPerCompany": 3} returns 3 postings that actually contain "engineer", not the first 3 raw postings off the board (verified live: a real Greenhouse board run with those exact inputs returned 3/3 titles containing "Engineer"/"Engineering").
Date bounds: both ends inclusive, and a bad date stops the run. postedAfter starts at 00:00:00.000 UTC on the day you name and postedBefore ends at 23:59:59.999 UTC on the day you name, so postedAfter: "2026-08-01" + postedBefore: "2026-08-31" is exactly August — no posting is trimmed off either end. Both accept YYYY-MM-DD or a full ISO datetime (2026-08-01T12:00:00Z) and nothing else: 06/15/2026, 2026-8-1, last week and calendar-impossible dates like 2026-02-30 fail the run with an error naming the input. That is deliberate. This Actor charges per posting, and the obvious alternative — parse what we can, ignore the rest — means an unreadable bound silently deletes your date filter and hands you (and bills you for) the entire unfiltered board. A filter that can't be honoured should stop the run, not quietly widen it.
Will postedAfter/postedBefore filter out a posting that has no date? No — if an ATS doesn't return a publishedAt for a posting, that posting is kept regardless of the date bounds. On Workday the date comes only from the per-job detail call, and setting either bound now triggers that call automatically, so the window applies to Workday boards too.
Which ATSes expose salary? Ashby, Lever and Recruitee publish a structured pay range in their API, and Greenhouse boards with pay transparency on print one in the posting body, which this Actor parses (139/166 postings on a live airbnb board, 472/874 on databricks). Workable, SmartRecruiters and Workday carry no compensation data through any standard field, so those come back null rather than guessed — see the SmartRecruiters deep-dive for the one real, narrow exception found on a single board's freeform custom field.
Does minSalary/maxSalary compare against an annual salary? Not necessarily — it compares the raw number as the ATS posted it, in whatever salaryInterval that posting uses (year, month, week, day or hour all appear in the wild). Mixing companies on very different intervals in one run means the filter isn't apples-to-apples; check salaryInterval/salaryCurrency on each row if that matters to you.
A company I need isn't on any of these 7 ATSes — can it still be scraped? Only if it uses one of the 7 (or a client-side board no ATS API backs, which this Actor can't reach). Message support@fetchsmith.com with the company's careers URL if you're unsure which ATS it runs on.
I set watchLabel and got zero results — is it broken? Usually not: either it was the baseline run (the log says so explicitly, and you were charged nothing), or nothing new has been posted on those boards since your last run. For a watch, zero is the normal, expected, free result most of the time. Read the run's status message to tell the two apart from a real problem — a misspelled board slug or a filter that matches nothing produces a zero-result run that looks exactly the same every time, so the status message names those causes explicitly ("Why you got nothing: ...") instead of calling it normal. If it says nothing is wrong, nothing is wrong.
How do I reset a watch baseline? Delete its record from the fetchsmith-ats-watch key-value store in your Apify account, or just use a new watchLabel. Changing any filter also starts a fresh baseline automatically.
One of my boards failed during the baseline run — what happens? That board contributes nothing to the baseline, so its currently-open postings will come back as "new" (and billable) on the next run. The baseline run logs a loud warning naming any board that failed, so you can re-seed before scheduling it.
Why did a company I listed return zero postings? Either it's genuinely down to 0 open roles, or it has migrated off that ATS — the run logs (not a failure) list any company that 404s so you can find its new slug/ATS instead of getting a silently empty result.
How is webhookUrl different from Apify's own platform webhooks?
Apify's platform webhooks are configured separately per Task/Actor via the Console or the Webhooks API — useful if you already live in the Apify Console, but extra setup if you're calling this Actor's API directly and just want a completion ping. webhookUrl is a plain input field: set it on the run itself and it POSTs a JSON body (actorRunId, defaultDatasetId, finishedAt, pushed, companiesScanned, companiesErrored, and — if watchLabel is set — watchSeeding/watchNewCount/watchSkippedCount/watchEvents/watchChangedCount/watchEventsFilteredCount/baselineTruncated/baselineTruncatedTotal) once the run finishes and every posting is already pushed and charged. Especially useful with watchLabel on a scheduled job alert: your endpoint gets told how many new postings landed without polling the dataset, and companiesErrored warns you when a board failed to answer. It's best-effort — a slow or failing webhook only logs a warning, it never fails the run, changes the result set, or affects billing.
Notes
Only public, no-login job-board data is collected — the same postings anyone can see on the company's own careers page. Job descriptions occasionally include a named recruiter contact the company itself chose to publish; this Actor does not extract or highlight individual contact data as a feature. Issues or feature requests: support@fetchsmith.com. Also available as a hosted API at https://fetchsmith.com
Related guides
- Six ATS job-board JSON APIs, six different ideas of what a job posting is
- Workday's public career-site API has no auth, a hard page-size cap, and no department field
- Greenhouse's job board API has no auth and no pagination — a big board is one 10 MB response
- Lever's job board API actually paginates — the one ATS that does
- Ashby's job board API: no pagination, no size cap, and pay bands baked into one number
- Workable's job board API: no pagination, and the description flag is the one that's actually worth gating
- Recruitee's job board API: one real filter hiding among ignored ones, and a 42 MB board that's 97% pasted screenshots
- SmartRecruiters' job board API: real pagination, a real per-posting cost, and one board that quietly leaks a salary nobody else publishes
- Teamtailor, Personio and Breezy — three more ATS job-board APIs, three more shapes
- "full-time" is not a filter: employmentType across ten ATS job-board APIs — why
employmentTypeKeywordreturns rows on seven platforms and zero on three, and which value to use instead on each. - Location is not one field either: normalizing ten ATS job-board APIs down to city/region/country — which platforms are structured vs free text, why Teamtailor's
addressRegionisn't usable as a region, and the Workday county-board bug thatlocationKeywordused to miss. - A Try-for-free click shouldn't cost a first-time buyer $1.26 and five minutes
- Eight ways an "only new since last run" watch mode silently stops working — the general failure modes
watchLabelis built to avoid. - We nearly charged our own buyers twice for rows they'd already paid for — a capped watch-mode baseline can silently evict old-but-current ids on a high-volume run, re-delivering (and re-billing) rows already paid for. Reproduced on this Actor, closed with truncation tracking.
- https://fetchsmith.com/tools
- Source code: https://github.com/Fetchsmith/fetchsmith/tree/main/actors/ats-jobs-scraper