Greenhouse, Lever and Ashby Jobs Scraper
Pricing
from $1.50 / 1,000 job returneds
Greenhouse, Lever and Ashby Jobs Scraper
Pricing
from $1.50 / 1,000 job returneds
Rating
0.0
(0)
Developer
Bravona
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
15 hours ago
Last modified
Categories
Share
Greenhouse, Lever and Ashby Jobs Scraper collects every open job from company career boards hosted on Greenhouse, Lever and Ashby and returns them in one normalized dataset, with salary ranges and new / open / closed tracking between runs. Enter company board names or careers URLs and download the jobs as JSON, CSV or Excel.
Unofficial tool. Not affiliated with, endorsed by, or sponsored by Greenhouse, Lever or Ashby.
What does Greenhouse, Lever and Ashby Jobs Scraper do?
- Reads public job boards on Greenhouse, Lever and Ashby through each platform's public job-board API. No login, no browser, no proxy.
- Detects which ATS a company uses from its board name (slug) or careers URL.
- Returns one schema for all three platforms.
- Adds salary min, max, currency and period: structured where the ATS provides it, otherwise parsed from the job text, with a
salarySourcefield saying which. - Marks each job as
new,openorclosedcompared with your previous run, so a scheduled run shows what changed since yesterday. - Leaves out recruiter and hiring-manager fields and removes email addresses from description text.
- Runs on Apify: call it via API, schedule runs, export to JSON, CSV or Excel, and connect integrations.
Who is it for?
Job seekers and job-search tools: watch the companies you care about and see new openings first. Job-market and labor analysts: track hiring by company, department, location and salary over time. Job-board and aggregator builders: feed one clean jobs feed from many company boards.
What data can you get from Greenhouse, Lever and Ashby job boards?
One record per job. Fields that can be empty are null; allLocations can be an empty list.
| Field | Type | Description |
|---|---|---|
company | string | Company board name (slug), lowercased |
ats | string | Which platform hosts the board: greenhouse, lever or ashby |
jobId | string | The job's ID on that platform |
title | string | Job title |
department | string or null | Department (Greenhouse: first department; Lever and Ashby: department) |
team | string or null | Team (Greenhouse: second department; Lever and Ashby: team) |
location | string or null | Primary job location as the ATS shows it |
allLocations | list of strings | Primary plus any other listed locations |
isRemote | boolean | True when the ATS marks the workplace as Remote or a listed location says remote, anywhere or work from home. Hybrid alone is not remote |
employmentType | string or null | FullTime, PartTime, Contract, Intern or Temporary. Greenhouse's API has no standard field, so it is often null there |
salaryMin | number or null | Lowest salary in the range (null for "up to" amounts) |
salaryMax | number or null | Highest salary in the range (null for "from" amounts) |
salaryCurrency | string or null | ISO code, e.g. USD, EUR, GBP |
salaryPeriod | string or null | year, month, week, day, hour, or null |
salarySource | string or null | structured (from the ATS), parsed (from the job text), or null (no salary) |
postedAt | string or null | When the job was published, ISO 8601 UTC (Greenhouse first_published, Lever createdAt, Ashby publishedAt) |
status | string | new, open or closed compared with the previous run |
descriptionText / descriptionHtml | string or null | Job description in the format you picked, with email addresses replaced by [email removed]. Only present when Job description is text or html; null on closed records |
applyUrl | string or null | Link to apply (Greenhouse: the job's public URL, which may be on the company's own careers site) |
jobUrl | string or null | Link to the job posting |
firstSeenAt | string | When this Actor first output the job (ISO 8601 UTC) |
lastSeenAt | string | The last run that saw the job on its board |
scrapedAt | string | When this run read the board |
No recruiter or hiring-manager fields are output.
Salary
- Structured first. Greenhouse pay ranges (
pay_transparency=true, the first range listed; amounts converted from cents), LeversalaryRange, Ashby compensation summary (the Salary component). - Otherwise parsed from the salary text and description. Supported formats: ranges (
$120,000 - $150,000,between $100,000 and $130,000), theksuffix ($120k–$150k,$120-150K,$211.4K), single values (Salary: $95,000,up to £60k,from €50.000), hourly / daily / weekly / monthly / yearly wording ($45/hr,per day,monthly,p.a.), currencies USD, EUR, GBP, CAD and AUD by symbol or ISO code before or after the number ($,US$,CA$,C$,A$,AU$,€,£; a bare$is read as USD); "up to" sets onlysalaryMax, "from" / "starting at" sets onlysalaryMin, and labels: a range marked base wins over one marked OTE. - Ambiguous text gives null: no currency, mixed currencies, funding or revenue amounts, benefits (bonus, HRA, relocation, budgets), max below min, a max more than 5× the min, hourly values over 1,000, or yearly values under 10,000. A single amount needs a pay word (salary, pay, compensation, base...) nearby or a period.
- salaryPeriod comes from the ATS's interval field (Lever
per-hour-wage, Ashby1 YEAR, a Greenhouse range titled "Monthly Pay Range") or from period words in the text. When neither exists, amounts of 15,000 or more are treated as yearly and smaller amounts keepsalaryPeriod: null.
Sources and terms
| Source | Access method | Bravona's pacing | What the source says |
|---|---|---|---|
| Greenhouse | Public Job Board API (boards-api.greenhouse.io/v1/boards/{token}/jobs) | 2 requests per second | Greenhouse docs: "Job Board data is publicly available, so authentication is not required for any GET endpoints." |
| Lever | Public Postings API (api.lever.co/v0/postings/{company}; EU boards on api.eu.lever.co) | 1 request per second | Lever postings API README: "all job postings in the published state are publicly viewable." The API's robots.txt sets Crawl-delay: 1. |
| Ashby | Public Job Postings API (api.ashbyhq.com/posting-api/job-board/{org}) | 1 request per second | Ashby docs: the API returns "data for all currently published Job Postings" for an organization. |
Pacing is the Actor's own request rate, with backoff and retries on HTTP 429 and 5xx. Greenhouse and Ashby publish no rate limit. Requests identify the Actor with the User-Agent Bravona-ATSJobs/0.1 (+apify.com/bravona).
How to find a company's board name
Enter either the company's careers URL on Greenhouse, Lever or Ashby, or just its board name (slug), e.g. airbnb. The board name is the first part of the path in the board URL, which isn't always the company name.
| ATS | Board URL | Example |
|---|---|---|
| Greenhouse | boards.greenhouse.io/<name> or job-boards.greenhouse.io/<name> | https://job-boards.greenhouse.io/discord |
| Lever | jobs.lever.co/<name> (EU: jobs.eu.lever.co/<name>) | https://jobs.lever.co/spotify |
| Ashby | jobs.ashbyhq.com/<name> | https://jobs.ashbyhq.com/ramp |
- Greenhouse: open any job on the company's careers page. If the address is
boards.greenhouse.io/<name>/jobs/...orjob-boards.greenhouse.io/<name>/jobs/...,<name>is the board name. If the careers page is on the company's own site, look forgh_jid=in job links or an embedboards.greenhouse.io/embed/job_board?for=<name>. You can paste the embed URL too. - Lever: job links look like
jobs.lever.co/<name>/<job id>. For boards in Lever's EU region (jobs.eu.lever.co/<name>), paste the URL so the EU API is used directly. - Ashby: job links look like
jobs.ashbyhq.com/<name>/<job id>.
With ATS on auto, a URL decides the platform. For a bare name the Actor checks Greenhouse, Lever (US, then EU) and Ashby and uses the board with the most jobs, because a name can exist on more than one ATS (Ramp has a leftover 1-job Greenhouse board and its real board on Ashby). The result is cached and reused on later runs; if the cached board disappears or goes empty, the Actor checks again.
Careers pages on other systems (Workday, iCIMS, SmartRecruiters, a company's own site without one of the boards above) are not supported.
How to use Greenhouse, Lever and Ashby Jobs Scraper
- Open the Actor and go to the Input tab.
- Enter companies: 1 to 500 board names or careers URLs (e.g. airbnb, spotify, ramp).
- Leave ATS on auto, or pick Greenhouse, Lever or Ashby.
- Optionally filter by title keywords, locations or remote only, choose how to include descriptions, and set Max jobs.
- Click Start, then download the dataset as JSON, CSV or Excel, or fetch it via the Apify API.
Track new and closed jobs
Every record carries a status on every run, whatever your settings. Only new since last run does not turn status on; it only limits output to new and closed records.
- First run for a state key: every job is
new. - Later runs: a job not returned before is
new; a job returned before and still on the board isopen; a job returned before that is gone from the board isclosed(once). A closed record carries the job's last known fields, withlastSeenAt= the last run that saw it. onlyNewSinceLastRun: trueoutputs onlynewandclosed;openjobs are still tracked.- Order and caps: per company,
closedrecords come first, thennewandopentogether, most recently posted first. SomaxJobsPerCompany: 5gives the 5 most recent jobs, and on the next run those same jobs come back asopen(with any newer posting asnew). - "Seen" means delivered: a job counts as seen only after it has been output. A job held back by
maxJobs,maxJobsPerCompany, a filter, or your max-charge limit is returned asnewon a later run instead of being lost. - A company switches ATS: if the detected ATS differs from the one stored, all previously stored jobs are output as
closed(free) and all current jobs asnew, and the log shows a warning. - If a company's board cannot be read in a run (error or not found), its state is left unchanged and the error is in the run summary.
Where state lives: the named key-value store bravona-ats-jobs-state in your Apify account (it persists across runs, unlike a run's default store). Keys:
<stateKey>--<company>: the jobs seen for one company, with the ATS it was on. Characters other than letters, digits and!-_.'()are replaced by_plus a short hash.<stateKey>--INDEX: the list of companies under that state key, used byresetState.DETECT--<company>: the cached auto-detected ATS (shared by all state keys; not cleared byresetState).
The default stateKey is jobs- plus a hash of the sorted, lowercased company list, so the same list always uses the same state and a different list starts fresh. Set your own stateKey to keep history when you add or remove companies, or to give two tasks with the same companies separate state. resetState: true deletes that state key's records before the run.
Schedule example: save a task with your company list, a fixed stateKey (e.g. daily-fintech) and onlyNewSinceLastRun: true, then add an Apify schedule with cron 0 7 * * *. Each morning the dataset holds only jobs posted and closed since the previous run. Connect the dataset to a webhook, Google Sheets or your database to process it.
Run summary
Each run writes OUTPUT to its default key-value store: the state store and key, totals by status, how many results were charged, whether the max-charge limit or maxJobs was reached, and per company: input, detected ATS (and Lever region), how it was detected (url, explicit, cache, probe, with what each probe found), jobs on the board, jobs matching filters, output counts by status, results charged, a previous ATS if it switched, and any error. A company that can't be read doesn't fail the run; the run fails only if no company could be read.
Input
| Field | Type | Required | Default | Allowed values / limits | What it does |
|---|---|---|---|---|---|
companies | array of strings | yes | none | 1 to 500 entries | Board slugs or careers URLs (patterns above). Duplicates (case-insensitive) are removed. |
ats | string | no | auto | auto, greenhouse, lever, ashby | auto detects per company. A fixed value skips detection for bare slugs. A URL always wins over this setting. |
titleKeywords | array of strings | no | [] | any | Keep a job if its title contains any keyword (case-insensitive substring). |
locations | array of strings | no | [] | any | Keep a job if any entry is a case-insensitive substring of its location or one of its other listed locations. The entry Remote also matches jobs flagged remote. |
remoteOnly | boolean | no | false | Keep only jobs with isRemote: true. | |
includeDescription | string | no | text | text, html, none | Adds descriptionText, descriptionHtml, or neither. |
onlyNewSinceLastRun | boolean | no | false | Output only new and closed jobs (skip open). | |
maxJobs | integer | no | 10000 | 1 to 100000 | Max output records per run (all companies, all statuses). |
maxJobsPerCompany | integer | no | no limit | 1 to 100000 | Max output records per company (all statuses). |
stateKey | string | no | derived from the company list | letters, digits, -, _, .; up to 100 chars | Name of the change-tracking state. |
resetState | boolean | no | false | Delete the stored state for this stateKey before the run. |
Filters combine with AND (titleKeywords AND locations AND remoteOnly). Jobs removed by a filter are never output, never charged and never stored as seen.
Example input (this is also the prefilled input, and the input used for the output example below):
{"companies": ["airbnb", "spotify", "ramp"],"ats": "auto","includeDescription": "none","maxJobsPerCompany": 5}
Output example
One record per job. Real, unedited records from a second run of the example input on Oct 30, 2026 (includeDescription: "none", so no description fields), one per ATS. The first run returned 15 jobs in about 3.3 seconds; this rerun returned the same 15 as open.
Greenhouse (airbnb):
{"company": "airbnb","ats": "greenhouse","jobId": "8337229","title": "Senior Manager, Community Policy","department": "Public Policy","team": null,"location": "United States","allLocations": ["United States"],"isRemote": true,"employmentType": null,"salaryMin": 204000,"salaryMax": 255000,"salaryCurrency": "USD","salaryPeriod": "year","salarySource": "structured","postedAt": "2026-10-30T19:19:06.000Z","applyUrl": "https://careers.airbnb.com/positions/8337229?gh_jid=8337229","jobUrl": "https://careers.airbnb.com/positions/8337229?gh_jid=8337229","status": "open","firstSeenAt": "2026-10-30T19:56:03.273Z","lastSeenAt": "2026-10-30T19:58:06.840Z","scrapedAt": "2026-10-30T19:58:06.840Z"}
Lever (spotify):
{"company": "spotify","ats": "lever","jobId": "2d67c471-9015-41c0-a041-b982ec4a14fd","title": "Account Executive - Commercial, Spotify Technology","department": "Operations and Business Support","team": "Platform","location": "London","isRemote": false,"allLocations": ["London"],"employmentType": "FullTime","salaryMin": null,"salaryMax": null,"salaryCurrency": null,"salaryPeriod": null,"salarySource": null,"postedAt": "2026-10-23T21:51:17.203Z","applyUrl": "https://jobs.lever.co/spotify/2d67c471-9015-41c0-a041-b982ec4a14fd/apply","jobUrl": "https://jobs.lever.co/spotify/2d67c471-9015-41c0-a041-b982ec4a14fd","status": "open","firstSeenAt": "2026-10-30T19:56:03.273Z","lastSeenAt": "2026-10-30T19:58:06.840Z","scrapedAt": "2026-10-30T19:58:06.840Z"}
Ashby (ramp):
{"company": "ramp","ats": "ashby","jobId": "aa0c645d-7aa0-40b0-bdfa-9b3fb19da404","title": "Customer Activation Manager | Stack Accounting","department": "Sales","team": "Customer Success Management","location": "New York, NY (HQ)","isRemote": false,"allLocations": ["New York, NY (HQ)"],"employmentType": "FullTime","salaryMin": 121000,"salaryMax": 184000,"salaryCurrency": "USD","salaryPeriod": "year","salarySource": "structured","postedAt": "2026-10-30T19:35:16.997Z","applyUrl": "https://jobs.ashbyhq.com/ramp/aa0c645d-7aa0-40b0-bdfa-9b3fb19da404/application","jobUrl": "https://jobs.ashbyhq.com/ramp/aa0c645d-7aa0-40b0-bdfa-9b3fb19da404","status": "open","firstSeenAt": "2026-10-30T19:56:03.273Z","lastSeenAt": "2026-10-30T19:58:06.840Z","scrapedAt": "2026-10-30T19:58:06.840Z"}
That run's OUTPUT record showed 15 records (5 per company, all open), every company detected from the cache, and nothing failed.
How much does it cost to get job postings from Greenhouse, Lever and Ashby?
You pay for new and open job records saved to your dataset. closed records are free. Jobs removed by your filters are never output or charged.
| Event | When it is charged | Price |
|---|---|---|
| Actor start | Once per run | $0.00005 |
| Job returned | Each new or open job saved | $0.0015 ($1.50 per 1,000) |
Important: a full rerun charges again for jobs that are still open. With Only new since last run on, a run is charged only for new jobs.
Examples:
- 100 jobs: about $0.15 plus the $0.00005 start fee. 1,000 jobs: about $1.50 plus the start fee.
- Prefilled run (airbnb, spotify, ramp, Max jobs per company 5, descriptions off): up to 3 × 5 = 15 jobs × $0.0015 = about $0.0225, plus the $0.00005 start fee.
- Daily schedule for 30 days, illustrative numbers: boards with 300 open jobs, about 10 new jobs a day.
- New-only off: each run charges about 300 jobs, about $0.45 a day, about $13.50 for 30 days.
- New-only on: the first run charges all 300 (every job is new on a first run), $0.45; then about 10 new jobs a day, $0.015 a day, about $0.89 for 30 days.
- Plus $0.00005 per run start ($0.0015 for 30 runs).
- A run that finds no jobs pays only the start fee.
Platform usage (compute, proxy, storage) is included in these prices. You pay only the event prices listed above, nothing extra on top.
Works on Apify's free plan; you pay a tiny start fee plus only for results returned, meaning new and open jobs saved. closed records are free, and a rerun charges again for jobs that are still open. Charges come out of your Apify account's prepaid usage; on the Free plan, once that monthly usage is used up, runs stop until the next billing cycle.
Spending limit: set a maximum cost per run. When it is reached, the Actor stops saving chargeable jobs (closed records of the company in progress still go out), skips the remaining companies and keeps their state, so nothing is lost on the next run.
Limits
- The Actor paces itself: Greenhouse 2 requests per second, Lever and Ashby 1 per second. Greenhouse and Ashby publish no limit.
- Each board is downloaded in full (one request per company, descriptions included) whatever the settings, so Job description: none and Max jobs per company make the output smaller, not the download faster. Only Max jobs per company (fewer records) lowers the cost; descriptions don't change the price, since charges are per record. When Max jobs or the spending limit is reached, the remaining companies are skipped without being fetched. Detecting the ATS of a new bare name takes up to 4 requests.
- Covers only boards hosted on Greenhouse, Lever and Ashby. Other ATS and company sites are not supported.
- Up to 500 companies per run; up to 100,000 output records (default 10,000).
- Salary appears only when the ATS provides it or the job text states it clearly; parsed salaries can be wrong, and ambiguous values are left null.
- Recruiter and hiring-manager fields are not collected; email addresses are removed from descriptions.
FAQ
Is it legal to use this data? The Actor reads public job boards through each platform's public API without logging in. You are responsible for how you use the data. This is not legal advice.
Does it include recruiter or contact details? No. Recruiter and hiring-manager fields are not collected, and email addresses are removed from description text.
Why is a company not found? The name must match the board name (see How to find a company's board name), and the board must be public on Greenhouse, Lever or Ashby. The log and the OUTPUT record say which platforms were checked.
What if a company switches ATS? Its old jobs are reported once as closed (free), its current jobs come through as new, and a warning is logged. See Track new and closed jobs.
How is salary parsed? Structured ATS salary data is used first. Otherwise the Actor parses ranges, "k" suffixes, single values ("up to" gives only a max, "from" only a min), and hourly, daily, weekly, monthly or yearly figures in USD, EUR, GBP, CAD or AUD, and sets salaryPeriod and salarySource. Without a period word, amounts of 15,000 or more count as yearly. Ambiguous values are left null. See Salary above.
Why is employmentType null for Greenhouse jobs? Greenhouse's public API has no standard employment type field. The Actor uses a company's custom field (e.g. "Employment Type") when there is one.
Why do closed jobs have no description? Descriptions aren't stored between runs, to keep state small. All other fields are the last known values.
Am I charged again for the same job on every run? On a full run, yes: every new or open job is charged each time. Turn on Only new since last run to be charged only for new jobs; closed records are always free. See the daily schedule example under Pricing.
Can I run different filters on the same companies? Yes. Give each task its own State key so each keeps its own history.
Do I pay for Apify compute on top? No. Platform usage is included. You pay only the listed event prices: $0.00005 per run start and $0.0015 per job.
Can I use it via API or integrations? Yes, like any Apify Actor, through the API tab, schedules and integrations.
Something broke or a field is missing? Open an issue in the Issues tab.
Changelog
- 0.1 (Unreleased): First version. Greenhouse, Lever (US and EU) and Ashby; auto-detect with caching; normalized salary and employment type; new/open/closed tracking with State key and Reset state; per-company
OUTPUTsummary.
Disclaimer
Greenhouse, Lever and Ashby Jobs Scraper is an unofficial tool built by Bravona. It is not affiliated with, endorsed by, or sponsored by Greenhouse, Lever or Ashby. All product names and trademarks belong to their owners and are used only to describe compatibility. The Actor reads publicly available job postings through public job-board APIs without logging in, and follows a conservative request rate. Data is provided as-is; accuracy and availability depend on the source. You are responsible for using the data in line with each platform's terms and applicable law (including privacy law).