Public ATS Job Board Scraper (Greenhouse, Ashby, Lever)
Pricing
from $1.00 / 1,000 job results
Public ATS Job Board Scraper (Greenhouse, Ashby, Lever)
Full job descriptions from companies' public ATS APIs. No proxy, no browser - pay per job result, not per page load.
Pricing
from $1.00 / 1,000 job results
Rating
0.0
(0)
Developer
Hjerau Lee
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Security
Limited permissionsShare
ATS Jobs API Scraper — Greenhouse, Ashby, Lever Job Postings
Read a company's live job openings — including the full job description — straight from the applicant-tracking APIs those companies already publish. No headless browser, no proxy rotation, no login. One HTTP call usually returns an entire board, so you pay per job result rather than per page load.
Use it for sales intelligence (who is hiring, for what, where), recruiting-market research, candidate matching, internal mobility dashboards, or as the source of a job-alert product.
Highlights
- Cheap per result. A board with 700 openings costs one request, not 700 page renders. The measured transfer is ~4 MB of gzipped JSON per 1,000 jobs, no proxy involved — see How it works for why that matters to your bill.
- Full descriptions, structured. Every result carries the job text as both
contentHtmland tag-strippedcontentText, so keyword filters and embeddings work on the same record. - Nothing invented. A field the company does not publish stays
null. No guessed salaries, no fabricated dates, no silent "remote" assumption for ATSes that don't say. - Predictable cost ceiling. The Actor respects your run's
maxCost: once your limit is reached it stops storing and stops charging, and says so in the run log. - Fair coverage under a cap. With
maxItemsset, results are taken round-robin across the companies you asked for, instead of one big board consuming the whole quota.
How it works
Greenhouse, Ashby and Lever expose public, unauthenticated JSON endpoints so a company can embed its own career page:
| ATS | Endpoint the Actor calls | Jobs per request |
|---|---|---|
| Greenhouse | https://boards-api.greenhouse.io/v1/boards/{token}/jobs?content=true | whole board (typically 100–800) |
| Ashby | https://api.ashbyhq.com/posting-api/job-board/{token} | whole board |
| Lever | https://api.lever.co/v0/postings/{token}?mode=json | whole board |
That is the whole trick: the data is already published as an API, so this Actor reads it instead of rendering the website. Requests are sequential per board and modest — a 1,000-company run is ~1,000 requests, not 100,000.
What this Actor will not do (stated up front so you don't discover it later):
- It reads Greenhouse, Ashby and Lever only. LinkedIn, Indeed, Workday and company-built career
pages are out of scope. SmartRecruiters is out of scope for a different reason, and we corrected
ourselves here: an earlier note in this README claimed its public posting endpoint "returned zero
results". That was wrong — re-tested 2026-11-01,
api.smartrecruiters.com/v1/companies/<slug>/postingsreturned live listings for 2 of 5 company slugs we tried (e.g. 180 postings for one French retailer, 2 for SmartRecruiters itself); the other 3 slugs were simply not the right identifiers. We still do not support it, because its field mapping, pagination andapplyUrlshape are untested work we have not done - not because the endpoint is dead. Adding it would mean testing the mapping against real boards, which we have not done yet. - It cannot see closed, filled or unlisted openings — only what the board currently lists.
- Salary appears only when the board itself publishes a structured compensation field.
- It does not circumvent blocks, logins, CAPTCHAs or rate limits. If a board refuses, the run
records that company under
boardsFailedand moves on.
Input
{"companies": ["stripe","https://jobs.ashbyhq.com/linear","https://jobs.lever.co/spotify"],"keywords": ["engineer"],"excludeKeywords": ["staff", "principal"],"remoteOnly": false,"maxItems": 500,"includeJobContent": true}
| Field | Type | Default | Meaning |
|---|---|---|---|
companies | array of strings | required | ATS board tokens or careers URLs, one per entry. Tokens are probed against Greenhouse → Ashby → Lever; pin ats to skip the probing. |
ats | auto/greenhouse/ashby/lever | auto | Force the vendor when you know it. |
maxItems | integer | 500 | Hard cap on stored results (max 50,000). |
keywords / excludeKeywords | array | – | Case-insensitive substrings on title + location + department. |
keywordMatchContent | boolean | false | Also match inside the job description. |
locationKeywords | array | – | Substrings of the location label the company published. Locations are free text. |
remoteOnly | boolean | false | Only jobs the board itself marks remote. |
publishedWithinDays | integer | 0 | Filter on the board's own published date; jobs without one are dropped when this is on. |
includeJobContent | boolean | true | Store the description. Turn off for a much smaller, cheaper record. |
maxContentChars | integer | 20000 | Truncates stored text and sets contentTruncated: true. |
concurrency, requestTimeoutSecs | integer | 8 / 30 | Boards fetched in parallel, per-request timeout. |
Output
One dataset item per job:
{"source": "ashby","company": "linear","jobId": "d3bc1ced-3ce4-4086-a050-555055dbb1ff","title": "Senior / Staff Fullstack Engineer","url": "https://jobs.ashbyhq.com/linear/d3bc1ced-3ce4-4086-a050-555055dbb1ff","applyUrl": "https://jobs.ashbyhq.com/linear/d3bc1ced-.../application","locationText": "Europe","department": "Product","team": "Engineering","employmentType": "full-time","workplaceType": "remote","remote": true,"publishedAt": "2021-04-27T20:13:45+00:00","updatedAt": null,"contentHtml": "<p>At Linear, we're building ...","contentText": "At Linear, we're building ...","contentChars": 3000,"contentTruncated": true,"compensation": null,"extra": { "secondaryLocations": [], "postalAddress": { "addressCountry": "European Union" } },"collectedAt": "2026-10-28T13:07:53+00:00","sourceEndpoint": "https://api.ashbyhq.com/posting-api/job-board/linear"}
Field notes worth knowing before you build on them:
remoteis tri-state:true,false, ornullwhen the board never said. Greenhouse has no remote flag, so its jobs gettrueonly when the company's own location/office label literally says "remote"; otherwisenull.locationTextalways keeps the original wording.publishedAt/updatedAtare UTC ISO 8601 converted from the board's own timestamps (Greenhousefirst_published, AshbypublishedAt, LevercreatedAt).nullwhen absent.workplaceTypeis normalized toremote/hybrid/onsite/unknown.sourceEndpointis on every record so you can verify a result against the live board.- The run also writes a key-value record
RUN_SUMMARYlisting every board it read, how many jobs each board had, and every board that failed with the reason. Nothing is silently skipped.
Pricing
Charged through Apify's pay per event model: one event per stored job result
(job-result), plus the standard per-run start event. No platform-usage surcharge is added on
top — you pay for data, not for the plumbing. The live Pricing tab on this Actor's page is
the source of truth for current rates and tiers.
To estimate your own spend: results = jobs after filters, and the run's transfer volume is
roughly 4 MB per 1,000 job results with descriptions included (measured), so no proxy or browser
cost is folded into the price. For comparison inside this segment: on 2026-10-31 we read the live
event prices of the Actors Apify's store returns for these ATS searches - 41 of 63 publish a
per-result price, and the median is $2.00 per 1,000 results; this Actor is $1.00 per 1,000.
Tutorial
- Open the Run tab, put one company token or careers URL per line into Companies.
Take the token from the live careers URL, not from memory:
jobs.lever.co/<token>,boards.greenhouse.io/<token>orjobs.ashbyhq.com/<token>. Companies move ATS vendors, so a guessed token often 404s — that is the board being gone, not the Actor failing. A known-good starter set (verified 2026-10-29 against all three vendors):stripe,canonical,spotify,clickhouse. - Set
maxItemsto something small (say 20) for the first run and check the Dataset tab. - Add
keywords/remoteOnly/publishedWithinDaysto shape the set you actually want. - For recurring collection, use Schedules (e.g. hourly on a watchlist) and read the dataset through the Apify client or the dataset API. CSV, Excel and JSON exports are built into Console.
- Via API:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });const run = await client.actor('<ACTOR_ID>').call({companies: ['stripe', 'https://jobs.ashbyhq.com/linear'],keywords: ['engineer'],maxItems: 200,});const { items } = await client.dataset(run.defaultDatasetId).listItems();
Because results are ordinary dataset items, this Actor drops into Make, n8n, Zapier or an Airtable sync without any custom code.
FAQ
Is this allowed? It reads public, unauthenticated JSON endpoints the ATS vendors publish for embedding company career pages. There is no login, no bypass of access controls, no CAPTCHA defeat, and request volume is one per company rather than a crawl. Companies that do not want their board aggregated can disable the public API; this Actor respects whatever the endpoint returns.
A token returned almost nothing — is the Actor broken? Usually not: a bare token can exist
on more than one ATS (a stale legacy board plus the live one, or two unrelated companies that
happen to share a slug). The Actor never switches vendors on its own, because the bigger board
might belong to a different employer. Instead the run summary lists such tokens under
boardsAmbiguous with the alternative board and its job count, so you can re-run with
"ats": "ashby" (or greenhouse/lever) and get exactly the company you meant.
Why did a company return nothing? Either the token belongs to a different ATS (pin ats), the
board is empty, or it 404s — check RUN_SUMMARY.boardsFailed, which names the reason per company.
My publishedWithinDays filter returned almost nothing. That is usually the board's own data,
not a miss. Posting ages measured on 2026-10-30 (first 80 jobs per board): median age
38 days (lever/spotify), 41 (greenhouse/stripe), 91 (ashby/ramp), 210
(ashby/clickhouse); with a 7-day filter those boards returned 11, 8, 4 and 0 of 80. Start with
publishedWithinDays: 0 (off) or 30, and only tighten it once you have seen a board's cadence.
Which fields are often empty? compensation was null in 180 of 180 sampled records
(three boards, 60 each) — structured salary is rare in these APIs, and we publish it only when the
board itself provides it. remote is set on Ashby/Lever but on Greenhouse only when the board's own
location text literally says remote (6 of 60 sampled). employmentType came back empty for the
Greenhouse board while Ashby and Lever filled ~all of it. Those are declared nullable in the dataset
schema, so an empty field never becomes a guessed one.
Can you add LinkedIn/Indeed/Workday? Those require browser rendering, proxies and, for some, authenticated surfaces. This Actor deliberately does not do that, so we do not quote prices for those sources. What we did measure (live store prices, 2026-10-30 and 2026-10-31): inside the ATS segment the median is $2.00 per 1,000 results and this Actor is $1.00. If your need is a watchlist of big tech companies, the Greenhouse/Ashby/Lever coverage is usually enough.
Are the results deduplicated? Yes, by source + company + jobId. Re-runs of the same board
return the same IDs, so you can diff by jobId to detect newly posted and withdrawn openings.
What if my maxCost runs out mid-run? The Actor stops storing and charging at that moment and
logs buyer spending limit reached after N results; the run finishes with the results already
paid for, which is exactly the behaviour Apify's docs require.
How fresh is the data? Each run reads the board live; there is no cache. Set a schedule to keep a history.
Support
Open an issue or message the developer through the Actor page. When reporting a wrong or missing
field, include the company, the source, and a run ID — every record carries sourceEndpoint,
so we can compare against the live board.