Multi-ATS Jobs: Greenhouse, Lever, Ashby, SmartRecruiters
Pricing
from $0.90 / 1,000 job returned with descriptions
Multi-ATS Jobs: Greenhouse, Lever, Ashby, SmartRecruiters
Greenhouse, Lever, Ashby and SmartRecruiters in one normalized schema, with real delta tracking: each job is marked new, updated, or unchanged since your last run. Includes Ashby salary bands. $0.90 per 1,000 jobs, descriptions included, never billed for a job that did not reach your dataset.
Pricing
from $0.90 / 1,000 job returned with descriptions
Rating
0.0
(0)
Developer
Donovan
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Features
Share
Multi-ATS Job Scraper
An ATS job scraper that queries the Greenhouse API, Lever API, Ashby API, and SmartRecruiters API directly and normalizes every result into one schema. Point it at a list of company boards and get back consistent job records regardless of which platform each company uses.
Built for hiring-signal feeds, recruiting tools, job aggregators, and sales teams targeting companies that are actively hiring.
Free-plan sample limit
Free Apify plans receive up to 4 company boards and 25 jobs per run. A four-board sample is distributed across the boards so you can verify Greenhouse, Lever, Ashby, and SmartRecruiters in one run. Paid Apify plans receive the full requested boards and output. For a finished recruiter hiring-intel report rather than a raw scrape, see https://dorkjobs.com/recruiters. The one-time report is free. The page also prices an optional weekly brief at $49/month; as of 2026-11-11 the button records interest only.
What sets this apart
There are several Multi-ATS scrapers on the store covering the same four platforms. Where this one is different:
- Real change tracking, not just a fresh dump. With
deltaModeon, each job is classifiednew,updated, orunchangedagainst the previous run, and the dataset includes onlynewandupdatedrows. Greenhouse providesupdated_at. Lever, Ashby, and SmartRecruiters do not, so those three use a content fingerprint. Schedule it daily and you get a stream of what actually changed, not the same thousand postings re-processed every morning. - Salary data where it exists. When an Ashby posting includes a compensation band, this actor surfaces it as a structured
{ currency, min, max, period }object instead of dropping it or leaving it buried in the description text. - $0.90 per 1,000 jobs, descriptions included. Here, descriptions cost the same as titles-only, and the per-board fee is separate so delta-mode reruns stay cheap.
- You're not billed for data you didn't get. If a run hits your spending cap mid-scrape, it stops cleanly instead of continuing to scrape jobs it can no longer charge for or deliver. Pushes and charges are computed from the same number, and
stopped_at_charge_limitin the run summary tells you when a result set is partial. In delta mode, anything not delivered stays unmarked so it comes back on your next run instead of vanishing.
One schema across the Greenhouse, Lever, Ashby, and SmartRecruiters APIs
Every ATS returns a different shape. Greenhouse nests locations under offices, Lever puts them in categories.allLocations, Ashby uses secondaryLocations, SmartRecruiters splits city/region/country. This actor flattens all of that into one job record, so your downstream code never branches on which platform a posting came from.
It queries the Greenhouse API, Lever API, Ashby API, and SmartRecruiters API directly. No headless browsers, no proxies, no anti-bot workarounds, which keeps runs fast and cheap.
Delta mode: turn it into a hiring-signals feed
Set deltaMode: true and the actor returns only jobs that are new or changed since your last run. State persists between runs in a key-value store keyed by job ID. Entries that have not been seen for 90 days are dropped, so a still-open job can come back as new after that.
Schedule it daily and you get a clean stream of "who started hiring for what today" instead of re-processing the same thousands of postings every morning. Every returned record carries a delta_status of new or updated.
Input
{"companies": [{ "ats": "greenhouse", "slug": "stripe", "name": "Stripe" },{ "ats": "lever", "slug": "ro", "name": "Ro" },{ "ats": "ashby", "slug": "ramp", "name": "Ramp" },{ "ats": "smartrecruiters", "slug": "BoschGroup", "name": "Bosch" }],"deltaMode": true,"includeDescription": true,"filterKeywords": ["engineer", "security"],"filterScope": "title","maxJobsPerCompany": 500}
| Field | Type | Default | What it does |
|---|---|---|---|
companies | array | required | Boards to scrape. Free plans receive up to 4 boards and 25 jobs total, distributed across selected boards; paid plans receive full requested output. Each entry needs ats and slug; name is optional labelling. |
deltaMode | boolean | false | Return only new or changed postings since the last run. |
includeDescription | boolean | true | Include the full job description. |
filterKeywords | string[] | [] | Keep only jobs matching at least one keyword (case-insensitive). |
filterScope | enum | title | Match against title, or title_and_description. |
maxJobsPerCompany | integer | unlimited | Cap jobs per board. Useful for testing and cost control. |
Finding a company's slug
It's in the company's careers URL:
| ATS | Careers URL | Slug |
|---|---|---|
| Greenhouse | boards.greenhouse.io/stripe or job-boards.greenhouse.io/stripe | stripe |
| Lever | jobs.lever.co/ro | ro |
| Ashby | jobs.ashbyhq.com/ramp | ramp |
| SmartRecruiters | jobs.smartrecruiters.com/BoschGroup | BoschGroup |
SmartRecruiters identifiers are case-sensitive and must match exactly. BoschGroup returns results; bosch returns zero postings. If a SmartRecruiters board comes back empty, check capitalization first.
Output
Every record, from every platform, looks like this:
{"id": "ashby:ramp:34413f8d-26bf-4bbc-8ade-eb309a0e2245","ats": "ashby","company": { "slug": "ramp", "name": "Ramp" },"title": "Security Engineer, Cloud","department": "Engineering","team": "Backend","locations": ["New York, NY (HQ)", "Remote (US)", "Remote (Canada)", "Miami, FL"],"employment_type": "FullTime","remote": true,"posted_at": "2026-04-07T17:12:35.753Z","updated_at": null,"url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245","description_html": "<div>...</div>","description_text": "About Ramp\n\nRamp is a financial operations platform...","compensation": {"currency": "USD","min": 211400,"max": 290600,"period": "1 YEAR"},"raw_source": "ashby","scraped_at": "2026-08-01T22:14:03.118Z","delta_status": "new"}
Fields a platform doesn't provide come back as null rather than being omitted, so the schema is stable across sources. delta_status is the exception: it is set only when deltaMode is on, and saved rows are new or updated. Jobs classified unchanged are not written to the dataset.
Platform coverage and limitations
Being upfront about what each source actually gives you:
| Greenhouse | Lever | Ashby | SmartRecruiters | |
|---|---|---|---|---|
| Descriptions | Inline, free | Inline, free | Inline, free | Extra request per job |
| Compensation | Not exposed | Not exposed | Yes | Not exposed |
updated_at | Yes | No | No | No |
| Requests per company | 1 | 1 | 1 | 1 per 100 jobs, plus 1 per description |
Two things worth knowing:
Only Greenhouse exposes a real update timestamp. For the other three, delta mode detects changes using a content fingerprint (title, locations, department, employment type, compensation, description length). That reliably catches meaningful edits, but a change confined to description wording of identical length won't register.
SmartRecruiters descriptions take extra requests. Its list endpoint omits the ad body, so each description needs its own request. The posting URL comes from that detail request, so url is null on SmartRecruiters rows when includeDescription is false. Set includeDescription: false for SmartRecruiters-heavy runs if you only need titles and locations. Those extra requests are not a separate charge. The job event price below is the same with or without a description.
Pricing
Pay per event:
| Event | Price |
|---|---|
| Actor started | $0.00005 |
| Company board crawled | $0.0005 |
| Job returned (no description) | $0.0009 |
| Job returned (with description) | $0.0009 |
That works out to $0.90 per 1,000 jobs, descriptions included. A 200-job Greenhouse board with full descriptions costs about $0.18. In delta mode after the first run you pay the per-board fee plus only the postings that actually changed, so a daily feed over a dozen boards usually lands in the low cents.
Jobs are charged only after they're written to the dataset, so you're never billed for a record you can't see.
If you set a maximum cost for the run, the actor stops cleanly once it's reached rather than scraping boards whose results would be discarded. The run summary reports stopped_at_charge_limit: true so you know the result set is partial, and in delta mode the jobs it couldn't deliver stay unrecorded and come back on the next run instead of being silently skipped.
How to pull Greenhouse, Lever, Ashby, and SmartRecruiters jobs into one spreadsheet
This section answers the question: how do I pull all open jobs from a list of company career sites on Greenhouse, Lever, Ashby, and SmartRecruiters into one spreadsheet, and see which roles are new since my last run?
The official job board APIs are free
Each of these four platforms publishes a public job board API. You do not need a Greenhouse Harvest key, a Lever API key, an Ashby customer key, or a SmartRecruiters API key to read public postings. If you only have a few companies, and you are comfortable writing a short script or a spreadsheet import, calling these APIs yourself is enough. There is no reason to pay an actor for a one-off look at a single Greenhouse board.
- Greenhouse Job Board API: docs.greenhouse.io/job-board.html. List jobs with
GET https://boards-api.greenhouse.io/v1/boards/{board_token}/jobs?content=true. The docs say job board data is publicly available and authentication is not required for GET endpoints. This actor always requestscontent=true. - Lever Postings API: github.com/lever/postings-api. Public postings:
GET https://api.lever.co/v0/postings/{site}?mode=json. Published postings are publicly viewable. Lever's API key is for creating postings, not for this read. - Ashby Job Postings API: developers.ashbyhq.com/docs/public-job-posting-api.
GET https://api.ashbyhq.com/posting-api/job-board/{name}?includeCompensation=true. The name is the last segment ofjobs.ashbyhq.com/{name}. This actor sendsincludeCompensation=true. - SmartRecruiters Posting API: developers.smartrecruiters.com/docs/endpoints.
GET https://api.smartrecruiters.com/v1/companies/{identifier}/postings. SmartRecruiters lists the Posting API under APIs that do not require authentication (authentication). The list omits the job ad, so a description needsGET https://api.smartrecruiters.com/v1/companies/{identifier}/postings/{postingId}. This actor pages the list at 100 postings per request.
The four responses are different shapes. None of them, on its own, gives you one spreadsheet with a new-since-last-run flag across all four.
What this actor adds
This actor calls those same official endpoints with plain HTTP requests. It does not use a headless browser. Each public posting becomes one dataset row, and Apify can export that dataset as JSON, CSV, or Excel.
The shared fields are a stable id ({ats}:{slug}:{source id}), title, locations, department, team, employment_type, url, description_html, description_text, compensation, posted_at, and updated_at. String and object fields the source does not provide are null, so the columns stay put. The id includes the slug text you sent, so a different slug string for the same board is a different set of jobs. Greenhouse rows have employment_type: null; this actor does not map an employment type from that list payload. Ashby jobs marked isListed: false are omitted.
deltaMode classifies every scraped job as new, updated, or unchanged against the previous run. The dataset receives new and updated only. The first run has no saved state, so those jobs come back as new. Greenhouse is compared by its updated_at timestamp. Lever, Ashby, and SmartRecruiters do not provide one, so they are compared with a content fingerprint of title, locations, department, employment type, compensation minimum and maximum, and description length. A description edit that does not change that length is not marked updated. Seen jobs are kept for 90 days.
Ashby compensation is a { currency, min, max, period } object when the posting includes a salary component. An equity-only component is not used as that band. compensation is null on Greenhouse, Lever, and SmartRecruiters rows.
On a free Apify plan the run keeps the first 4 valid boards in companies and at most 25 jobs, spread across those boards. A paid Apify plan returns the full request. Leave filterKeywords empty when you want every posting the run is allowed to return, and leave maxJobsPerCompany empty for no per-board cap. A job is billed only after it is written to the dataset. Use the event table above for the prices. Workday is not covered. If the list is mostly Workday, this actor will not help.
For a finished emailed roster of the same four hosts, use the free recruiter report at dorkjobs.com/recruiters. As of 2026-11-11 that page prices an optional weekly brief at $49/month, and the button records interest only.
Official APIs vs this actor
| Official job board APIs, called yourself | This actor | |
|---|---|---|
| Cost | Free, plus your time and hosting | Pay per event. See the pricing table above. Free Apify plans: up to 4 boards and 25 jobs per run |
| Coverage | One API per platform. Workday and any other ATS need a different integration | Greenhouse, Lever, Ashby, and SmartRecruiters only |
| Schema | Four different JSON shapes | One row per job, exportable as JSON, CSV, or Excel |
| What is new since the last run | You build the diff | deltaMode, with the fingerprint, first-run, and 90-day limits above |
| Salary | Only where that API exposes it. Ashby can include a band when you pass includeCompensation=true. This actor does not map a structured band from Greenhouse, Lever, or SmartRecruiters | Same source data. Ashby bands are parsed into { currency, min, max, period }. The other three rows have compensation: null |
FAQ
How do I pull all open jobs from a list of company career sites on Greenhouse, Lever, Ashby, and SmartRecruiters into one spreadsheet, and see which roles are new since my last run?
Call each official job board API above and merge the results yourself, or run this actor with that company list, deltaMode set to true, and filterKeywords left empty. Download the dataset as CSV or Excel. The first run sets delta_status to new on the returned jobs. Later runs return only rows whose delta_status is new or updated. You can schedule the actor daily for that diff. Workday is not included. On a free Apify plan, the run returns at most the first 4 valid boards and 25 jobs.
How do I find a company's slug?
It is the last path segment of the careers URL: boards.greenhouse.io/{slug} or job-boards.greenhouse.io/{slug}, jobs.lever.co/{slug}, jobs.ashbyhq.com/{slug}, and jobs.smartrecruiters.com/{slug}. SmartRecruiters slugs are case-sensitive. BoschGroup returns results; bosch returns zero postings.
Why did a SmartRecruiters board come back empty?
Check capitalization first. A mismatched SmartRecruiters identifier returns zero postings and a warning, and the other boards in the run still continue. An HTTP 404 while loading a board fails that board only. A failed SmartRecruiters description fetch still keeps the job, without the description. The run fails outright only when every board failed.
Does this replace DorkJobs?
No. This actor is a raw dataset you can schedule. DorkJobs /recruiters emails a finished hiring report for the same four hosts. The one-time report is free. The page prices an optional weekly brief at $49/month, and as of 2026-11-11 that button records interest only. The DorkJobs home search is a separate Google site: query. It can include boards this actor does not scrape, including Workday.
Checked against this actor's code, the official job board API docs, and the DorkJobs recruiter page on 2026-11-11.
Reliability
- Exponential backoff with jitter on
429and5xx, honoringRetry-After. - A bad company slug fails that board only, with a clear error naming the slug. Other boards in the same run continue.
- Individual SmartRecruiters description fetches that fail emit the job without a description instead of dropping it.
- Every run writes a
RUN_SUMMARYrecord to the key-value store with counts, per-board failures, and warnings. - The run only fails outright if every board failed, so a partial result is never silently reported as success.
Local development
bun installbun run typecheckbun run src/main.ts # reads storage/key_value_stores/default/INPUT.json