Multi-ATS Jobs: Greenhouse, Lever, Ashby, SmartRecruiters
Pricing
from $1.00 / 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. $1 per 1,000 jobs, descriptions included never billed for a job that did not reach your dataset.
Pricing
from $1.00 / 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
20 hours ago
Last modified
Categories
Share
Multi-ATS Job Scraper
Scrape job postings from Greenhouse, Lever, Ashby, and SmartRecruiters in one run, normalized into a single 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.
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.
deltaModemarks every jobnew,updated, orunchangedagainst the previous run, using each platform's own update timestamp where one exists and a content fingerprint where it doesn't (Lever and Ashby expose neither). 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. Ashby's API exposes a compensation band on most postings; this actor surfaces it as a structured
{ currency, min, max, period }object instead of dropping it or leaving it buried in the description text. - $1 per 1,000 jobs, descriptions included. Comparable actors in this category typically charge a flat $2–3 per 1,000 jobs regardless of whether you want descriptions. 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.
Why this instead of four separate scrapers
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 also uses each platform's public JSON 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.
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. Each 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 | 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 silently returns zero. 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.
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 is the expensive one. Its list endpoint omits the ad body, so each description needs its own request. Set includeDescription: false for SmartRecruiters-heavy runs if you only need titles and locations.
Pricing
Pay per event:
| Event | Price |
|---|---|
| Actor started | $0.00005 |
| Company board crawled | $0.002 |
| Job returned (no description) | $0.001 |
| Job returned (with description) | $0.001 |
That works out to $1 per 1,000 jobs, descriptions included. A 200-job Greenhouse board with full descriptions costs about $0.20. 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.
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