ATS Jobs API with Salary: Greenhouse, Lever, Ashby & more
Pricing
$1.20 / 1,000 jobs
ATS Jobs API with Salary: Greenhouse, Lever, Ashby & more
Every open job from any company on Greenhouse, Lever, Ashby, Workable, Recruitee or Personio: salary as numbers, and what changed since your last run. $1.20 per 1,000 jobs. Fields: salaryMin, salaryMax, salaryCurrency, salaryInterval, salarySource, changeStatus (new, changed, closed).
Pricing
$1.20 / 1,000 jobs
Rating
0.0
(0)
Developer
Christopher Nowlan
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Security
Limited permissionsShare
ATS Jobs API with Salary: Greenhouse, Lever, Ashby, Workable, Recruitee & Personio
Read the open jobs of any company that hires through Greenhouse, Lever, Ashby, Workable, Recruitee or Personio in one clean format. Every row carries three things a plain job list does not:
- The salary as numbers, even when the board has no pay field. The Actor uses the range the employer filled in on
the board (
salarySource: "structured") and, when there is none, reads the range written in the job description, such as$150,000 - $200,000or$75/hr - $90/hr(salarySource: "text"). It never estimates: a posting that states no pay returnsnull. - What changed since your last run. Each job is flagged
new,changedorunchangedagainst the previous run of the same board, and withonlyChangesyou also get a freeclosedrow for every job that left the board. - A price you can work out in advance. $1.20 per 1,000 jobs delivered: filtered-out jobs, closed rows and
company-name lookups are free, and with
onlyChangesso are unchanged jobs. A board of 200 jobs costs about $0.24.
Give a company name, a careers link or a ready-made preset; no board tokens needed. No login, no proxy, no browser: the Actor calls the public job-board endpoints that these vendors provide for exactly this purpose, so runs are fast, cheap and stable.
What you get
- One schema for all six platforms: title, department, team, location, remote flag, workplace type, employment type, posted date, job and apply URLs.
- Salary as numbers, not text:
salaryMin,salaryMax,salaryCurrency,salaryInterval(year,month,week,day,hour), plus every pay zone the employer listed insalaryRanges.salarySourcesays where the number came from:structured(the field the employer filled in on the board) ortext(a range such as$150,000 - $200,000written in the job description, as US pay-transparency laws require). Greenhouse pay ranges (which come in cents) are converted to normal currency units. - Change tracking: each row says whether the job is
new,changedorunchangedsince the last run of the same board. Turn ononlyChangesto be charged only for new and changed jobs, which suits a daily schedule, and to get a freeclosedrow for every job that left the board (see below). - No tokens to look up: type
airbnb, pastehttps://careers.airbnb.comor a board link, or pick a preset. The Actor finds which of the six vendors hosts the company and tells you in the run'sSUMMARY. - Filters: title keywords, locations, remote only, only jobs with a published salary.
- Polite by design: robots.txt is read and obeyed on every run, requests are spaced (at least 0.5 s apart, longer if a site's Crawl-delay says so) and failed requests are retried with backoff.
Use from AI agents (Apify MCP)
Add the Actor to an MCP client with https://mcp.apify.com/?actors=flipvo/ats-jobs-scraper. It appears as one tool,
flipvo--ats-jobs-scraper, whose arguments are the fields below. Nothing is required: a call with no arguments reads
the three example boards. Pass boards (a list) for real work. The result names the run's dataset; fetch the rows
with the dataset tool and ask for the fields you need, for example
company,title,salaryMin,salaryMax,salaryCurrency, salaryInterval,salarySource,changeStatus,urlInput
| Field | What it does | Default |
|---|---|---|
boards | Companies to read: a name (airbnb), a careers URL, a board URL, or greenhouse:stripe / lever:spotify / ashby:ashby / workable:hirefellows / recruitee:channable / personio:lanch. Up to 200, presets included. | boards or presets; none at all runs the example boards |
presets | Ready-made company lists added to boards: ai-labs, devtools (see below) | none |
keywords | Keep jobs whose title or department contains any of these words | all jobs |
locations | Keep jobs whose location contains any of these texts | all locations |
remoteOnly | Only jobs the employer marks or labels as remote | off |
onlyWithSalary | Only jobs with a published salary, structured or written in the description | off |
includeDescription | Add the plain-text description to each job (the salary is read from it either way) | off |
onlyChanges | Deliver only new and changed jobs since your previous run, plus a free closed row per job that left | off |
maxJobsPerBoard / maxJobsTotal | Caps (per board, counted after onlyChanges); maxJobsTotal is your budget cap | 1000 / 1000 |
requestDelayMs | Minimum gap between requests to one host (200 to 10000) | 500 |
maxRetries | Retries with backoff for 429, 5xx and network errors (0 to 6) | 3 |
Example:
{"boards": ["airbnb", "https://careers.zoox.com", "ashby:ashby"],"presets": ["devtools"],"keywords": ["engineer"],"onlyWithSalary": true,"maxJobsTotal": 500}
Company names, careers links and board links
Each entry of boards is read like this:
- A board link or
platform:token(https://boards.greenhouse.io/stripe,https://job-boards.greenhouse.io/stripe,https://jobs.lever.co/spotify,https://jobs.ashbyhq.com/ashby,https://apply.workable.com/hirefellows,https://channable.recruitee.com,https://lanch.jobs.personio.de,greenhouse:stripe,workable:hirefellows,recruitee:channable,personio:lanch): used as given. Nothing is looked up. - A company name (
airbnb,Scale AI) or a careers link (https://careers.airbnb.com): the Actor builds the likely board token from the name (scaleai, thenscale-ai; for a link, from the domain), asks the public API of Greenhouse, then Lever, then Ashby, then Workable, then Recruitee, then Personio, and uses the first board that has open jobs. Every request goes through the same robots.txt check and request spacing as the rest of the run. It does not open or scrape the careers page. Where a company's board token differs from its name, or one name exists on two vendors, give the explicit form. A bare name matches a board token, not a company: a token can belong to a different company with the same or a similar name (ashbyreads Ashby's own careers board). CheckatsandboardinSUMMARY, or paste the company's careers or board link when you need that exact company. - Links to other job-board vendors (SmartRecruiters, Workday and so on) are refused with a message naming the vendor.
The SUMMARY record lists every input with input (what you typed), ats (greenhouse, lever, ashby, workable, recruitee, personio, or null
when no board was found), board (the board that was read), source (explicit, url, name, careers-url or
preset:<name>) and, for names and careers links, tried (every board asked). A name that matches nothing is reported
there with the reason and does not stop the other boards.
Presets
presets adds a curated list of boards to the run. Each list holds only boards that returned open jobs from their
public API when it was last checked (2026-11-04 20:03 UTC); a board that has since moved or closed is skipped and
reported in SUMMARY.
| Preset | Boards |
|---|---|
ai-labs | Anthropic, OpenAI, xAI, Cohere, Perplexity, Scale AI, ElevenLabs, Together AI (8) |
devtools | Vercel, GitLab, Datadog, MongoDB, LaunchDarkly, Grafana Labs, Supabase, Linear, PostHog, Sentry, Render, ClickHouse (12) |
The exact boards are in src/presets.json.
Output
One dataset row per job. A sample from a real run is in docs/sample-output.json; a row looks like this:
{"platform": "lever","board": "zoox","company": "zoox","jobId": "7980e9a3-99f3-4988-ae6d-355c9199d8fb","title": "Body Structures Engineer","department": "Vehicle Development","team": "Body, Closures and Chassis","location": "Foster City, CA","isRemote": false,"workplaceType": "onsite","employmentType": "Full-time","postedAt": "2026-09-10T20:41:07.958Z","url": "https://jobs.lever.co/zoox/7980e9a3-99f3-4988-ae6d-355c9199d8fb","salaryMin": 170000,"salaryMax": 204000,"salaryCurrency": "USD","salaryInterval": "year","salarySource": "structured","changeStatus": "new","scrapedAt": "2026-11-04T19:47:27.000Z"}
Fields
| Field | Meaning |
|---|---|
platform, board, company, jobId | Where the job lives and its id on that board |
title, department, team, location | As the board states them (team is Lever and Ashby only; the other vendors have none) |
isRemote | true when the board marks the job remote or the location or title says Remote; otherwise null (never false from text) |
workplaceType, employmentType | remote / hybrid / onsite and the employment type, where the board states them; otherwise null |
postedAt, updatedAt | ISO 8601 dates; updatedAt is Greenhouse and Recruitee only; Workable and Personio give only a publish date |
url, applyUrl | Job page and application link |
salaryMin, salaryMax, salaryCurrency, salaryInterval | First salary range, in normal currency units; null when there is none |
salarySource | structured, text or null (see Salary below) |
salaryRanges | Every range found, one per pay zone: label, min, max, currency, interval |
descriptionText | Plain-text description, only with includeDescription |
changeStatus, scrapedAt | new, changed, unchanged (or closed) since your previous run, and when the row was read |
Salary
- Structured first. If the employer filled in the board's pay fields (Greenhouse pay transparency, Lever
salaryRange, Ashby compensation, Recruiteesalary), that range is used andsalarySourceisstructured. It always wins over text. - Otherwise the description text. Many employers, Stripe among them, state the pay only in the posting prose. The
Actor reads ranges written as
$150,000 - $200,000,$150K–$200K USD,$75/hr - $90/hr,$120,000 to $150,000 per year,Salary: $193,232 - 288,000/yr,Rs 4000000 - Rs 6000000,80.000 - 120.000 EUR,15,50 € und 19,50 € pro Stundeand similar, with an interval from the words around the range (/yr,per hour,annually, "the hourly rate is") or, with none, from an annual-sized number. These rows havesalarySource: "text". - Strict, so a wrong salary is rare. A range needs a currency symbol and two amounts. Single figures,
$5M funding,$100B market,$20k-$100k+ ARR,401(k), and ranges next to "bonus", "sign-on", "equity" or "relocation" are ignored; a range with no interval and a small number ($20 - $30) is ignored. If nothing qualifies the salary isnullandsalarySourceisnull.
The description is read even when includeDescription is off, but it only appears in the row when you turn it on.
Accuracy, measured
Measured 2026-11-04 on a sample of 145 jobs, drawn at random (seed 41) from 5,538 jobs the Actor delivered from 34 public
boards between 22:46Z and 22:49Z UTC. The sample covers all six vendors and every salarySource (structured, text, none).
Each job's salary and remote status were checked by hand against the posting's own pay text. Each vendor and
salarySource group was sampled on its own, so the population-weighted rates scale each group by its size.
| Measure | Result |
|---|---|
salarySource: structured: exact (min, max, currency and interval all right) | 35 of 36 (97.2%) |
salarySource: structured: per field (min / max / currency / interval) | 97.2% / 100.0% / 100.0% / 100.0% |
salarySource: text: exact (min, max, currency and interval all right) | 41 of 41 (100.0%) |
salarySource: text: per field (min / max / currency / interval) | 100.0% / 100.0% / 100.0% / 100.0% |
| Postings that state pay where we deliver none | 7 of 84 sampled (6 state one value, not a range); 9.1% population-weighted |
| Delivered salaries exact, population-weighted | 99.7% |
isRemote: true right | 17 of 17 (100.0%) |
isRemote: false right | 61 of 62 (98.4%) |
isRemote: null (no remote signal in the vendor's fields, location or title) | 66 of 145 |
| Before this release's fixes | text exact 94.3%, missed 15 of 84, isRemote: true right 51.5% |
- The misses are mostly single figures ("$30 per hour", "USD 280 per approved task", "450 EUR"). The Actor reads only ranges, on purpose: a lone number in a posting is too often a bonus, a budget or a funding round. One more is an internship whose pay is called a "stipend", a word the Actor skips because it usually means a relocation or learning allowance.
- The one structured error: a Recruitee employer typed one range in the pay fields and a different one in the text (EUR 5,500 vs 5,800 a month). We deliver what the pay fields say.
- The fixes this check found: rupee ranges (
Rs,₹,INR), ranges with the currency after the amounts (German and Dutch postings), a customerARRband read as pay, and Ashby'sisRemoteflag, which is set on hybrid jobs too (522 of OpenAI's 811 postings).isRemotenow follows Ashby's stated workplace type.
Every sampled job, with its posting link, pay text, our answer and the hand-checked answer, is kept with the Actor's
source (docs/salary-accuracy-2026-11.json), next to the scripts that draw, re-run and score the sample. A test fails
the build if this table stops matching that file. Ask in the Issues tab for a copy.
Export as JSON, CSV, Excel or via the API. A summary of every input (which vendor matched, jobs on the board, matched,
delivered, closed, error) is saved to the SUMMARY record of the run's key-value store.
Closed jobs
With onlyChanges on, a job that was on the board at your previous run and is gone now comes back as a short row:
{"changeStatus": "closed","platform": "greenhouse","board": "airbnb","company": "Airbnb","jobId": "7123456","title": "Senior Backend Engineer","url": "https://boards.greenhouse.io/airbnb/jobs/7123456","closedAt": "2026-11-05T06:00:03.120Z","scrapedAt": "2026-11-05T06:00:03.120Z"}
closedAt is the time your run noticed the job gone (the vendors do not publish a close time). Each closure is
reported once. Closed rows follow your keywords, locations, remoteOnly and onlyWithSalary filters, judged on the
details stored when the job was last seen, and they do not count against maxJobsPerBoard or maxJobsTotal. If a
board answers with an empty list while it had jobs at the last run, the Actor treats that as a failed answer, reports
no closures and keeps its state, so a vendor hiccup does not close a whole board. State saved by earlier versions is
read as before; a job from that old state that has left the board is reported with title and url as null (and, because its details were not stored, only when no keyword, location, remote or salary filter is set).
Pricing
Pay per event: $1.20 per 1,000 jobs delivered ($0.0012 per job), nothing for jobs you filter out, and with
onlyChanges nothing for unchanged jobs. Closed-job rows are free: they are written without the job event.
Each one reports a job you already paid for when it was delivered, so a monitoring run costs only the new and changed
jobs. Looking up a company name or careers link is free too. Apify platform usage is not charged on top. A board of 200 jobs costs about
$0.24 per run.
Good to know
- The salary comes from the structured fields the employer filled in and, failing that, from a range written in the
job description (
salarySourcetells you which). The Actor never estimates a salary: a posting that states none returnsnull. - Reading the text costs requests on Greenhouse, whose job list carries no description. After the other filters
(keywords, locations, remote), jobs with no structured salary are read: up to 5 of them one by one, more than that
with one extra request for the whole board. Lever, Ashby, Workable, Recruitee and Personio already send the text in their list, so they
cost nothing extra. If that read fails, a run with
onlyWithSalaryreports the board as an error inSUMMARY(instead of silently returning fewer jobs); a run without it still delivers the board with structured salaries only and puts awarningon that board inSUMMARY. - Workable publishes no structured pay, so its salaries always come from a range written in the description
(
salarySource: "text"); a posting that states none returnsnull. Workable's public widget endpoint returns the whole board, descriptions included, in one request per board (no paging), so it costs one request however many jobs the board has. It has no update date or team: change detection compares the title, location, department, employment type and description.isRemoteistruefor a job the account marks as telecommuting (workplaceType: "remote") or whose location or title says Remote. A board is its account name:hirefellowsforapply.workable.com/hirefellows(orhirefellows.workable.com);companyis the account's display name. - Recruitee boards are read from the company's public careers API,
https://<company>.recruitee.com/api/offers/, in one request per board, descriptions included. Companies that set a salary on the offer return it as numbers withsalarySource: "structured"(a pay of 0 or an empty one counts as none); otherwise a range in the text is read. Recruitee does not document thesalaryfield of this API, so a change on their side would show asnullsalaries rather than errors.workplaceTypecomes from the offer's remote, hybrid and on-site flags;employmentTypeis Recruitee's code (for examplefulltime_permanent). A board is the company's<company>.recruitee.comsubdomain, even when its careers page runs on its own domain (jobs.channable.comisrecruitee:channable). - Personio boards are read from the company's public XML feed,
https://<company>.jobs.personio.de/xml(the.jobs.personio.comdomain is tried when the company is not on.de), in one request per board, descriptions included, in the language the company set as its default. The feed carries no pay, no update date, no apply link and no company name, sosalarySourceistextornull,urlandapplyUrlare the job page, andcompanyis the board name.locationlists the main office and any additional ones, separated by;;employmentTypejoins the schedule and the employment type (full-time, permanent). Pay written with a dot as thousands separator (€55.000) is not read, because it cannot be told from a decimal. - Greenhouse and Lever expose different amounts of detail: Lever has no last-updated date, so change detection on Lever
and Ashby compares the posting content. Greenhouse's job list has no department, so the Actor makes one extra small
request per board to the public departments endpoint and fills
department(a job in several departments gets the first); it does not download the full descriptions for that. - With
onlyChanges,maxJobsPerBoardis applied after unchanged jobs are removed. Jobs a run does not deliver (over the cap, or stopped bymaxJobsTotal) stay marked as not yet seen and are delivered on the next run. - Greenhouse pay ranges carry no unit. The interval is read from the range label ("Annual", "Hourly", "Monthly") and,
failing that, inferred from the size of the number; check
salaryIntervalforunknownbefore relying on it. - A board that does not exist, a name that matches no vendor, or a host that forbids access in robots.txt is skipped and
reported in
SUMMARY; the other boards still run. If every input fails, the run fails.
Legal and data note
This Actor only reads public job postings that employers publish on purpose, through the public endpoints provided by Greenhouse, Lever, Ashby, Workable, Recruitee and Personio for job boards. It does not log in, bypass any protection or use proxies, and it checks robots.txt before every host. Postings contain business information; the Actor does not collect recruiter or applicant contact details. You are responsible for using the data in line with the law and with the terms of the boards you read, including data-protection rules where they apply. This Actor is unofficial and not affiliated with, endorsed by or sponsored by Greenhouse, Lever, Ashby, Workable, Recruitee or Personio.
Support
Found a board that does not parse or a field that is wrong? Open an issue on the Actor page; include the board token.