ATS Jobs API with Salary: Greenhouse, Lever, Ashby & more avatar

ATS Jobs API with Salary: Greenhouse, Lever, Ashby & more

Pricing

$1.20 / 1,000 jobs

Go to Apify Store
ATS Jobs API with Salary: Greenhouse, Lever, Ashby & more

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

Christopher Nowlan

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Security

Limited permissions

Share

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:

  1. 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,000 or $75/hr - $90/hr (salarySource: "text"). It never estimates: a posting that states no pay returns null.
  2. What changed since your last run. Each job is flagged new, changed or unchanged against the previous run of the same board, and with onlyChanges you also get a free closed row for every job that left the board.
  3. 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 onlyChanges so 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 in salaryRanges. salarySource says where the number came from: structured (the field the employer filled in on the board) or text (a range such as $150,000 - $200,000 written 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, changed or unchanged since the last run of the same board. Turn on onlyChanges to be charged only for new and changed jobs, which suits a daily schedule, and to get a free closed row for every job that left the board (see below).
  • No tokens to look up: type airbnb, paste https://careers.airbnb.com or a board link, or pick a preset. The Actor finds which of the six vendors hosts the company and tells you in the run's SUMMARY.
  • 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,url
.

Input

FieldWhat it doesDefault
boardsCompanies 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
presetsReady-made company lists added to boards: ai-labs, devtools (see below)none
keywordsKeep jobs whose title or department contains any of these wordsall jobs
locationsKeep jobs whose location contains any of these textsall locations
remoteOnlyOnly jobs the employer marks or labels as remoteoff
onlyWithSalaryOnly jobs with a published salary, structured or written in the descriptionoff
includeDescriptionAdd the plain-text description to each job (the salary is read from it either way)off
onlyChangesDeliver only new and changed jobs since your previous run, plus a free closed row per job that leftoff
maxJobsPerBoard / maxJobsTotalCaps (per board, counted after onlyChanges); maxJobsTotal is your budget cap1000 / 1000
requestDelayMsMinimum gap between requests to one host (200 to 10000)500
maxRetriesRetries 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
}

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, then scale-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 (ashby reads Ashby's own careers board). Check ats and board in SUMMARY, 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.

PresetBoards
ai-labsAnthropic, OpenAI, xAI, Cohere, Perplexity, Scale AI, ElevenLabs, Together AI (8)
devtoolsVercel, 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

FieldMeaning
platform, board, company, jobIdWhere the job lives and its id on that board
title, department, team, locationAs the board states them (team is Lever and Ashby only; the other vendors have none)
isRemotetrue when the board marks the job remote or the location or title says Remote; otherwise null (never false from text)
workplaceType, employmentTyperemote / hybrid / onsite and the employment type, where the board states them; otherwise null
postedAt, updatedAtISO 8601 dates; updatedAt is Greenhouse and Recruitee only; Workable and Personio give only a publish date
url, applyUrlJob page and application link
salaryMin, salaryMax, salaryCurrency, salaryIntervalFirst salary range, in normal currency units; null when there is none
salarySourcestructured, text or null (see Salary below)
salaryRangesEvery range found, one per pay zone: label, min, max, currency, interval
descriptionTextPlain-text description, only with includeDescription
changeStatus, scrapedAtnew, changed, unchanged (or closed) since your previous run, and when the row was read

Salary

  1. Structured first. If the employer filled in the board's pay fields (Greenhouse pay transparency, Lever salaryRange, Ashby compensation, Recruitee salary), that range is used and salarySource is structured. It always wins over text.
  2. 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 Stunde and 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 have salarySource: "text".
  3. 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 is null and salarySource is null.

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.

MeasureResult
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 none7 of 84 sampled (6 state one value, not a range); 9.1% population-weighted
Delivered salaries exact, population-weighted99.7%
isRemote: true right17 of 17 (100.0%)
isRemote: false right61 of 62 (98.4%)
isRemote: null (no remote signal in the vendor's fields, location or title)66 of 145
Before this release's fixestext 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 customer ARR band read as pay, and Ashby's isRemote flag, which is set on hybrid jobs too (522 of OpenAI's 811 postings). isRemote now 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 (salarySource tells you which). The Actor never estimates a salary: a posting that states none returns null.
  • 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 onlyWithSalary reports the board as an error in SUMMARY (instead of silently returning fewer jobs); a run without it still delivers the board with structured salaries only and puts a warning on that board in SUMMARY.
  • Workable publishes no structured pay, so its salaries always come from a range written in the description (salarySource: "text"); a posting that states none returns null. 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. isRemote is true for a job the account marks as telecommuting (workplaceType: "remote") or whose location or title says Remote. A board is its account name: hirefellows for apply.workable.com/hirefellows (or hirefellows.workable.com); company is 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 with salarySource: "structured" (a pay of 0 or an empty one counts as none); otherwise a range in the text is read. Recruitee does not document the salary field of this API, so a change on their side would show as null salaries rather than errors. workplaceType comes from the offer's remote, hybrid and on-site flags; employmentType is Recruitee's code (for example fulltime_permanent). A board is the company's <company>.recruitee.com subdomain, even when its careers page runs on its own domain (jobs.channable.com is recruitee:channable).
  • Personio boards are read from the company's public XML feed, https://<company>.jobs.personio.de/xml (the .jobs.personio.com domain 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, so salarySource is text or null, url and applyUrl are the job page, and company is the board name. location lists the main office and any additional ones, separated by ;; employmentType joins 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, maxJobsPerBoard is applied after unchanged jobs are removed. Jobs a run does not deliver (over the cap, or stopped by maxJobsTotal) 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 salaryInterval for unknown before 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.

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.