Multi-ATS Jobs: Greenhouse, Lever, Ashby, SmartRecruiters avatar

Multi-ATS Jobs: Greenhouse, Lever, Ashby, SmartRecruiters

Pricing

from $0.90 / 1,000 job returned with descriptions

Go to Apify Store
Multi-ATS Jobs: Greenhouse, Lever, Ashby, SmartRecruiters

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

Donovan

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Features

Limited permissions

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 deltaMode on, each job is classified new, updated, or unchanged against the previous run, and the dataset includes only new and updated rows. Greenhouse provides updated_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_limit in 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
}
FieldTypeDefaultWhat it does
companiesarrayrequiredBoards 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.
deltaModebooleanfalseReturn only new or changed postings since the last run.
includeDescriptionbooleantrueInclude the full job description.
filterKeywordsstring[][]Keep only jobs matching at least one keyword (case-insensitive).
filterScopeenumtitleMatch against title, or title_and_description.
maxJobsPerCompanyintegerunlimitedCap jobs per board. Useful for testing and cost control.

Finding a company's slug

It's in the company's careers URL:

ATSCareers URLSlug
Greenhouseboards.greenhouse.io/stripe or job-boards.greenhouse.io/stripestripe
Leverjobs.lever.co/roro
Ashbyjobs.ashbyhq.com/rampramp
SmartRecruitersjobs.smartrecruiters.com/BoschGroupBoschGroup

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:

GreenhouseLeverAshbySmartRecruiters
DescriptionsInline, freeInline, freeInline, freeExtra request per job
CompensationNot exposedNot exposedYesNot exposed
updated_atYesNoNoNo
Requests per company1111 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:

EventPrice
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 requests content=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 of jobs.ashbyhq.com/{name}. This actor sends includeCompensation=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 needs GET 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 yourselfThis actor
CostFree, plus your time and hostingPay per event. See the pricing table above. Free Apify plans: up to 4 boards and 25 jobs per run
CoverageOne API per platform. Workday and any other ATS need a different integrationGreenhouse, Lever, Ashby, and SmartRecruiters only
SchemaFour different JSON shapesOne row per job, exportable as JSON, CSV, or Excel
What is new since the last runYou build the diffdeltaMode, with the fingerprint, first-run, and 90-day limits above
SalaryOnly 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 SmartRecruitersSame 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 429 and 5xx, honoring Retry-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_SUMMARY record 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 install
bun run typecheck
bun run src/main.ts # reads storage/key_value_stores/default/INPUT.json