ATS Jobs Scraper: Greenhouse, Lever, Ashby, Workable
Pricing
from $0.65 / 1,000 job postings
ATS Jobs Scraper: Greenhouse, Lever, Ashby, Workable
Public job-board JSON from Greenhouse, Lever, Ashby, Workable and SmartRecruiters for a list of company slugs. One row per posting: title, location, department, apply URL, full description, and salary when published. Title, location and remote filters; only-new-since-last-run. No login.
Pricing
from $0.65 / 1,000 job postings
Rating
0.0
(0)
Developer
E A
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
Reads the public job-board JSON that Greenhouse, Lever, Ashby, Workable and SmartRecruiters publish for a company's careers page, for a list of companies in one run. Each posting is one row: title, location, department, apply URL, the full description, and salary when that board publishes it. Filter by title, location or remote before anything is read; ask for only the postings you have not seen since your last run. No login, no browser, no applicant or Harvest APIs. The default company is ramp (Ramp's Ashby board).
Who uses ATS Jobs Scraper
| Who | What they run it for |
|---|---|
| Talent intelligence and aggregators | Pull every open role across a list of companies, whichever of the five vendors each one uses, into one table with one shape. |
| Recruiters and sourcers | Refresh a target list's live postings, keep apply URLs and published pay, and see only what is new since last week. |
| Sales and hiring-signal work | Watch which roles a company opens, in which department and where, and whether pay is posted. |
| Agents and pipelines | Pass a list of slugs and get rectangular rows instead of five vendor JSON dialects. |
What you can count on
- every row is charged only after it is written to your dataset; a row you cannot see is never billed
- a run that finds nothing returns one NO_MATCHING_LISTINGS row that says so, never an empty dataset
- a spending limit stops the run cleanly with a STOPPED_EARLY row saying how many rows were returned and how many were not
- every run writes a RUN_SUMMARY record with rowsFetched, rowsPushed, rowsCharged and duplicatesDropped, so a short run and a broken one are told apart
- if a board changes its response, the run fails with the error in the log; it never returns rows full of nulls and calls it success
- no column is always empty: every field in the table below is filled by at least one vendor, and the table says which
Measured against the alternative
The most-used alternative on this board was run on 2026-09-10, on its own default board, for 10 postings. Its rows left salary empty on 10 of 10, employment_type empty on 10 of 10, remote_type empty on 9 of 10, and parse_confidence empty on 10 of 10. On our 100-row capture of the same day, salary_minimum is filled on 97 of 100 rows, job_type on 100 of 100, and is_remote on 100 of 100. It runs at 1024 MB; this Actor runs at 256 MB. It charges $0.0015 a posting; this Actor charges $0.0012.
What data does ATS Jobs Scraper return?
One real posting from the default Ramp board (description shortened):
{"platform": "ashby","official_url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245","title": "Security Engineer, Cloud","posted_date": "2026-04-07T17:12:35.753+00:00","location": "New York, NY (HQ)","is_remote": true,"workplace_type": "Hybrid","description": "ABOUT RAMP\n\nRamp is building the smart infrastructure for finance teams …","job_type": "FullTime","department": "Engineering","job_id": "34413f8d-26bf-4bbc-8ade-eb309a0e2245","salary_minimum": 211400,"salary_maximum": 290600,"salary_currency": "USD","compensation": { "summaryComponents": [ { "compensationType": "Salary", "minValue": 211400, "maxValue": 290600, "currencyCode": "USD" } ] },"company_name": "ramp","address": { "postalAddress": { "addressLocality": "New York", "addressRegion": "NY", "addressCountry": "US" } },"apply_url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application","result": "ROW"}
The capture that froze this example returned 100 postings on the default input (company = ["ramp"], maxItemsPerCompany = 100); the board listed 146. Ten rows across all five vendors, two per vendor, are the test fixture this Actor is held to.
Fields on every posting row
| Field | Meaning | Ashby | Greenhouse | Lever | Workable | SmartRecruiters |
|---|---|---|---|---|---|---|
platform | Which vendor answered | ashby | greenhouse | lever | workable | smartrecruiters |
official_url | Public posting URL (link) | jobUrl | absolute_url | hostedUrl | url | postingUrl |
title | Job title | title | title | text | title | name |
posted_date | Published time | publishedAt | first_published | createdAt | published_on | releasedDate |
location | Workplace text as published | location | location.name | categories.location | city, state, country | fullLocation |
is_remote | Vendor's remote flag; null when it has none | isRemote | word Remote in location, else null | workplaceType | telecommuting | location.remote |
workplace_type | remote, hybrid or onsite | workplaceType | null | workplaceType | workplace | remote / hybrid flags |
description | Full posting text, plain or HTML as the vendor gives it | descriptionPlain | content | descriptionPlain | description + requirements + benefits | jobDescription + qualifications + additionalInformation |
job_type | Employment type in the vendor's words | employmentType | null | categories.commitment | employment_type | typeOfEmployment.label |
department | Department or team | department | departments[0].name | categories.department | department | department.label |
job_id | The vendor's own posting id | id | id | id | shortcode | id |
salary_minimum | Published pay floor | compensation minValue | pay_input_ranges | salaryRange.min | null | null |
salary_maximum | Published pay ceiling | compensation maxValue | pay_input_ranges | salaryRange.max | null | null |
salary_currency | ISO code of the published pay | currencyCode | currency_type | salaryRange.currency | null | null |
compensation | The vendor's whole pay object when published | compensation | pay_input_ranges | salaryRange | null | null |
company_name | The slug you passed in company | slug | slug | slug | account | company |
address | Structured location when published | address | null | null | city, state, country | city, region, country, coordinates |
apply_url | Where a person applies (link) | applyUrl | posting URL | applyUrl | application_url | applyUrl |
result | ROW on a posting | |||||
reason | Status rows only: why there were no postings, or why the run stopped early | |||||
rowsFetched | Status rows only: postings received before de-duplication and the cap | |||||
rowsReturned | Status rows only: posting rows written to the dataset | |||||
rowsRemaining | Status rows only: fetched postings not returned |
A null in a cell above means that vendor does not publish the value on its public API; the row still carries the column, as null, so every vendor's rows have the same shape. Salary is present whenever the board publishes it: Ashby and Lever do on many postings, Greenhouse on boards that expose pay ranges, Workable and SmartRecruiters never on these endpoints.
Pricing
Pay-per-event: $0.0012 per listing-returned, one posting written to the dataset. The platform also charges apify-actor-start at $0.00005 per compute unit allocated for the run (256 MB is a quarter of a unit).
Status rows (NO_MATCHING_LISTINGS, STOPPED_EARLY) and dropped duplicates are not charged. Postings removed by a filter are never read in full and never charged.
| Postings written | listing-returned | Start (default) | Total (those two) |
|---|---|---|---|
| 100 | $0.12 | $0.00005 | $0.12005 |
| 1,000 | $1.20 | $0.00005 | $1.20005 |
| 10,000 | $12.00 | $0.00005 | $12.00005 |
Platform compute is billed separately by Apify according to memory and duration. A run of 100 Ramp postings finished in about 10 seconds at 256 MB. Workable and SmartRecruiters descriptions cost one request per kept posting; a run of 100 such postings takes about half a minute more.
How do I use ATS Jobs Scraper?
- Open the Actor on Apify and press Start. The default input reads Ramp's board.
- In
company, add one entry per board: a slug (ramp), a tagged slug (greenhouse:stripe,lever:spotify,workable:nuvei,smartrecruiters:Ubisoft2), or a public board URL. - Optionally set
titleKeyword,locationKeywordorremoteOnlyto keep only the postings you want,maxItemsPerCompany(default 100), andonlyNewto skip what an earlier run already returned. - Download the dataset.
RUN_SUMMARYin the default key-value store has the counts.
Example input, the one this page's sample row came from:
{"company": ["ramp"],"maxItemsPerCompany": 100}
Three boards, engineering roles, remote only:
{"company": ["ramp", "greenhouse:stripe", "lever:spotify"],"maxItemsPerCompany": 50,"titleKeyword": "engineer","remoteOnly": true}
Weekly watch of a target list, new postings only:
{"company": ["workable:nuvei", "smartrecruiters:Ubisoft2", "ramp"],"onlyNew": true}
API (replace the token):
https://api.apify.com/v2/acts/Pradio~greenhouse-lever-ashby-job/runs?token=YOUR_TOKEN
with body {"company":["ramp"],"maxItemsPerCompany":20}.
Input
| Input | Type | Default | Required | What it does |
|---|---|---|---|---|
company | list of strings | ["ramp"] | yes | One entry per company board: slug, vendor:slug, or public board URL |
maxItemsPerCompany | integer | 100 | no | Most posting rows per company; each board is capped on its own |
maxItems | integer | (none) | no | Overall cap across the run, on top of the per-company cap |
titleKeyword | string | (empty) | no | Keep postings whose title contains this text, case-insensitive |
locationKeyword | string | (empty) | no | Keep postings whose location text or structured address contains this text |
remoteOnly | boolean | false | no | Keep only postings the vendor marks remote; an unclassified posting is dropped, not guessed |
onlyNew | boolean | false | no | Skip postings an earlier run under your account already returned |
includeDescription | boolean | true | no | Read the per-posting description on Workable and SmartRecruiters (one request per kept posting) |
company
Accepted forms, one per entry:
- Bare slug:
ramp— tried on Ashby, Greenhouse, Lever, Workable and SmartRecruiters in that order until one answers. - Tagged:
ashby:ramp,greenhouse:stripe,lever:spotify,workable:nuvei,smartrecruiters:Ubisoft2. Pin the vendor when two of them know the same slug. - URLs:
https://jobs.ashbyhq.com/ramp,https://boards.greenhouse.io/stripe,https://jobs.lever.co/spotify,https://apply.workable.com/nuvei/,https://jobs.smartrecruiters.com/Ubisoft2/, and the vendors' API URLs for the same boards.
It does not accept applicant Data API URLs, Harvest tokens, or login cookies.
maxItemsPerCompany and maxItems
Each company keeps at most maxItemsPerCompany postings after the filters, so a board of 300 never starves the next one on the list. maxItems is an extra ceiling on the whole run; leave it empty unless you want one.
titleKeyword, locationKeyword, remoteOnly
Substring matches, case-insensitive, on the title and on the location text plus the structured address. remoteOnly keeps only postings whose vendor marks them remote. All three run before any per-posting description is read, so filtered-out postings cost nothing.
onlyNew
Postings this Actor has returned under your account are remembered in a named key-value store, greenhouse-lever-ashby-job-seen, by posting URL. A posting is added only after it was actually returned, so a run cut short by a spend cap leaves the rest for next time. Delete that store to forget everything.
includeDescription
Ashby, Greenhouse and Lever include the description in the board response. Workable and SmartRecruiters publish it on a per-posting endpoint, one request per kept posting. Set this to false for a cheaper title-and-link run on those two vendors; their description is then null.
Output
- Posting rows carry
result: "ROW"and every field in the table above (a value the vendor does not publish isnull, never omitted). Duplicateofficial_urlvalues are dropped before charge. - Zero postings: one uncharged row with
result: "NO_MATCHING_LISTINGS"and areason. The dataset is never empty. - Spend cap: one uncharged
STOPPED_EARLYrow withrowsReturnedandrowsRemaining. - RUN_SUMMARY in the default key-value store:
rowsFetched,rowsPushed,rowsCharged,duplicatesDropped,stoppedEarly, andskippedSeenwhenonlyNewis on.
An HTTP error, a JSON parse failure, or a 429 that still fails after retries fails the run. That is deliberate: a refused read is not an empty board.
What can you do with the data?
A researcher compiling every open engineering role across twenty portfolio companies passes their twenty slugs in company with titleKeyword set to engineer and exports title, department, location, apply_url and pay where published.
A sourcer watching Stripe's Greenhouse board and Spotify's Lever board sets locationKeyword to London and onlyNew to true, and gets each week only the postings that appeared since the last run.
A pipeline that already stores company slugs maps each one to this Actor, writes rows into a warehouse, and joins on official_url or job_id as the stable posting key.
An agent asks for one slug and reads rectangular fields instead of learning five vendor JSON dialects.
Use ATS Jobs Scraper with AI agents
claude mcp add --transport http apify "https://mcp.apify.com?tools=Pradio/greenhouse-lever-ashby-job"
Paste that line to give an MCP client this Actor as a tool (the tool name is the Store identity Pradio/greenhouse-lever-ashby-job).
Limits
- Public job-board JSON only. Ashby posting-api, Greenhouse boards-api, Lever postings, Workable's widget and v2 job endpoints, SmartRecruiters postings. No Harvest, no authenticated Data APIs, no apply POST, no shared ATS accounts, no HTML scrape of careers sites.
- Five vendors, named. Recruitee, Personio and Workday are not read: Recruitee's public offers endpoint answered no live board we tried, Personio publishes XML, and Workday has no logged-out JSON board. A slug on one of those returns NO_MATCHING_LISTINGS, not a guess.
- A list of company boards per run, not a search engine across every company on a vendor.
- No browser, no proxy. If a host refuses the GET, the run fails (with backoff on HTTP 429).
- Salary only when the board publishes it. Workable and SmartRecruiters never do on these endpoints; many Greenhouse boards omit it; those rows carry null pay fields and a null
compensation. remoteOnlydrops the unclassified. Greenhouse has no remote flag, so a Greenhouse posting passesremoteOnlyonly when its location text says Remote.- Not affiliated with Greenhouse, Lever, Ashby, Workable, SmartRecruiters, or any employer whose board you read. Re-read each vendor's public API docs and site terms before you rely on a high-volume schedule; stop if they object.
Adjacent Actors from the same publisher: Authentic Jobs Listings Scraper for that board's public listings feed, and Voice Over Jobs Monitor for voice-over castings. This Actor is only the five vendors' public job-board JSON.
Report problems on the Actor's Issues tab on Apify.
Troubleshooting
I got fewer rows than maxItemsPerCompany. The board had fewer postings after the filters, or Ashby skipped isListed: false items. That is the board running out, not a silent error. Check RUN_SUMMARY.rowsFetched.
I got one row and no jobs. Read result. NO_MATCHING_LISTINGS means every vendor tried for that slug answered and there were no postings, or none passed the filters, or onlyNew had already returned them all. That one row is the answer; it is not billed.
The run failed instead of returning empty titles. A 401/403/5xx or truncated JSON fails the run. Fix the slug, wait out a 429, or stop. Empty-looking success rows are not produced on a refused read.
A bare slug found the wrong company. Two vendors can know the same slug. Pin it: greenhouse:your-slug, workable:your-account.
SmartRecruiters returned nothing for a bare slug. SmartRecruiters answers an unknown company with an empty list rather than a 404, so a bare slug moves on; use smartrecruiters:Company to read it directly, and note that their company identifiers are case-sensitive (Ubisoft2).
FAQ
Can I use integrations with ATS Jobs Scraper?
Yes. Apify integrations can start this Actor and pass any of the inputs above. Chain it by storing the dataset and pointing the next Actor at that dataset URL.
Can I use ATS Jobs Scraper with the Apify API?
Yes. Start runs against Pradio/greenhouse-lever-ashby-job (Pradio~greenhouse-lever-ashby-job in the path). The input JSON is the same as the Console. Dataset items and RUN_SUMMARY are the same records as a Console run.
Can I use ATS Jobs Scraper through an MCP server?
Yes. Use the snippet in Use ATS Jobs Scraper with AI agents so the client exposes this Actor as a tool.
Is it legal to scrape the boards?
This Actor only GETs the vendors' public job-board / posting JSON (logged-out), the same endpoints their own careers-page embeds read. It does not use Harvest or applicant APIs and it does not collect recruiter emails. You are responsible for each vendor's current public API rules and terms, for your jurisdiction, and for stopping if a host objects. This page is not legal advice. The Actor is not an official product of any vendor named here.
Not affiliated
ATS Jobs Scraper is an independent tool. It is not affiliated with, endorsed by, or maintained by Greenhouse Software, Lever (Employ Inc.), Ashby, Workable, SmartRecruiters, or any employer whose public job board you request. Job content belongs to the posting company.