Career Site Jobs API — Greenhouse, Lever, Ashby & 10+ ATS
Pricing
from $1.00 / 1,000 job posting returneds
Career Site Jobs API — Greenhouse, Lever, Ashby & 10+ ATS
Scrape job postings from any company career page. One Actor covers Greenhouse, Lever, Ashby, Workable, Recruitee, SmartRecruiters, Teamtailor, Personio, Breezy, JazzHR and Workday, returning normalized JSON: title, location, remote flag, salary, apply URL and full description.
Pricing
from $1.00 / 1,000 job posting returneds
Rating
0.0
(0)
Developer
Insight Solutions
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
Scrape job postings from any company career page. Give this Actor a list of career boards — or just company domains — and get back every open role as normalized JSON: title, location, remote flag, salary, posted date, apply URL and full description, 27 fields in one shape across 11 applicant tracking systems.
Data comes straight from each ATS's own public job feed, so runs finish in seconds. No browser, no proxy, no API key, no login. $2 per 1,000 job postings, failed boards are free, and a run that returns no jobs costs nothing at all.
Try it in 30 seconds
{"boards": ["greenhouse:stripe", "ashby:ramp", "vandebron.nl"],"includeDescription": true,"maxJobsTotal": 100}
Two of those are explicit boards; vandebron.nl is a bare domain the Actor resolves on its own, then remembers for later runs.
Supported ATS platforms
Every serious ATS publishes its customers' open roles on a public, unauthenticated endpoint — that is how postings reach Google and the aggregators. Each vendor uses a different URL, response shape and name for "location". This Actor reads those endpoints directly and flattens them into one schema.
| ATS | Board identifier | Notes |
|---|---|---|
| Greenhouse | greenhouse:stripe | Full descriptions, departments, offices, structured pay ranges |
| Lever | lever:leverdemo | Full descriptions, commitment, workplace type, salary range |
| Ashby | ashby:ramp | Full descriptions, department/team, compensation tiers, remote flag |
| Workable | workable:lyst | Description, requirements, benefits, multi-location, telecommuting flag |
| Recruitee | recruitee:vandebron | Description + requirements, remote/hybrid/on-site, salary when published |
| SmartRecruiters | smartrecruiters:Visa | Paginated postings plus the full public job ad |
| Teamtailor | teamtailor:oatly | JSON Feed + JobPosting data: description, locations, salary, employment type |
| Personio (beta) | personio:personio | XML feed, titled sections, offices. Personio rate-limits some feeds from cloud IPs (HTTP 429); those return a free diagnostic row |
| Breezy HR | breezy:new-incentives | Stable feed, but it carries no posting body — descriptionText is null |
| JazzHR (beta) | jazzhr:company3 | Public board page only — title, location, department, apply link. No unauthenticated feed exists, so no description |
| Workday | workday:nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite | Full descriptions, locations, requisition IDs. Verified live; needs the board host and site name, not a short slug |
One bound worth knowing. Workday and SmartRecruiters serve each posting body from a separate document, so both adapters fetch descriptions for the first 250 postings per board. Beyond that the posting is still returned with descriptionText: null; split a large board across runs by postedAfter if you need every body. Every other platform ships the description in the board response.
Use cases
- Build a jobs board or aggregator — pull 500 companies nightly into one consistent schema instead of maintaining eleven parsers.
- Track who is hiring for a role —
locationFilterplusremoteOnlyreturns every open remote role on a watchlist, apply URL included. - Enrich a CRM or lead list with hiring data — pass bare company domains straight from your account list and let auto-detection find the board.
- Benchmark salaries and compensation —
salaryMin/salaryMax/salaryCurrencycome only from structured ATS compensation data, never parsed out of prose. - Feed a recruiting or job-matching AI agent — one JSON object in, one flat array out, over the Apify API or MCP.
- Seed a database before change detection — pull full boards here, then hand the watchlist to New Job Postings Monitor.
How it compares
- One Actor for 11 ATS platforms, where most career-board scrapers cover three to six. One input format, one output schema, one thing to maintain.
- Auto-detection from a bare domain — pass
stripe.cominstead of hunting for a board slug. - Charge-on-success, written into the pricing table. A board that 404s produces a free diagnostic row. A run that delivers zero postings finishes FAILED with the reason in its status message — never a green run containing an apology — and costs nothing, start fee included.
- Recruiter contact details stripped from every posting body, and
robots.txthonoured on every HTML page fetched.
We are not the cheapest per row in this category and will not pretend otherwise. We are the one that covers the whole field in a single schema and does not bill you for failures.
Input reference
| Field | Type | Default | What it does |
|---|---|---|---|
boards (required) | array of strings | ["greenhouse:stripe","ashby:ramp"] | One entry per company: ats:slug, a career-page URL, or a bare domain for auto-detection |
atsPlatforms | multi-select | all | Restricts which platforms auto-detection considers. Ignored for explicit ats:slug entries |
includeDescription | boolean | true | Turn off for a much smaller, faster result |
descriptionFormat | text | markdown | html | text | How the posting body is rendered |
locationFilter | array of strings | none | Case-insensitive substring match against the location |
remoteOnly | boolean | false | Keep only postings the ATS flags remote, or whose workplace-type or location text says remote. Job titles are never used for this |
postedAfter | date | none | Keep postings first published on or after this date. Postings with no published date are kept |
maxJobsPerBoard | integer | 1000 | Per-board ceiling |
maxJobsTotal | integer | 5000 | Whole-run ceiling. The run stops cleanly; you are not charged for jobs not returned |
maxRunSecs | integer | 240 | Whole-run wall-clock budget. When reached the Actor stops fetching, keeps what it has, and says so |
requestTimeoutSecs | integer | 20 | Per-request timeout. A URL is abandoned once its attempts have used twice that, or 40 seconds, whichever is longer |
maxConcurrency | integer | 6 | Boards fetched in parallel. Never more than one request at a time to any single host |
Finding a board slug: look at where the Apply button goes — the slug is the identifier in that URL (jobs.lever.co/leverdemo/… → lever:leverdemo, acme.breezy.hr/p/… → breezy:acme, acme.wd5.myworkdayjobs.com/en-US/SiteName → workday:acme.wd5.myworkdayjobs.com/SiteName). You can also paste the whole URL, or pass the bare domain.
Output reference
One row per posting, always the same keys in the same order:
{"jobId": "8077887", // stable ID inside its ATS"atsPlatform": "greenhouse", "boardSlug": "stripe", "companyName": "Stripe","companyDomain": "stripe.com", // the employer's own domain — never the ATS host, never a guess"title": "Account Executive, Bridge", "department": "Product Sales", "team": null,"employmentType": "FullTime", // FullTime|PartTime|Contract|Temporary|Internship|Volunteer|Other"seniority": "senior", // from the title; null when the title says nothing"locationRaw": "SF, NYC, SEA, CHI", // exactly as the ATS published it"locations": [{ "city": "SF", "region": null, "country": null, "isRemote": false, "note": null }],"isRemote": false,"workplaceType": "hybrid", // onsite|hybrid|remote, when the ATS says so"salaryMin": 152000, "salaryMax": 228000, "salaryCurrency": "USD","salaryPeriod": "YEAR", // YEAR|MONTH|WEEK|DAY|HOUR. Structured ATS data only, never guessed"descriptionText": "Who we are\n\nStripe is…","descriptionHtml": null, // used instead when descriptionFormat is "html""applyUrl": "https://job-boards.greenhouse.io/stripe/jobs/8077887","jobUrl": "https://stripe.com/jobs/search?gh_jid=8077887","postedAt": "2026-07-22T13:15:53-04:00", "updatedAt": "2026-08-06T12:10:17-04:00","scrapedAt": "2026-09-08T07:05:51.244Z","source": "boards-api.greenhouse.io","sourceUrl": "https://boards-api.greenhouse.io/v1/boards/stripe/jobs?content=true"}
An unreadable board produces a diagnostic row instead — ok: false plus input, atsPlatform, boardSlug, error (reason and the URL we tried), source, sourceUrl, checkedAt — and no charge. Filter with ok !== false for jobs only.
Roles syndicated to two boards are deduplicated. Two genuinely different requisitions on the same board are never collapsed, however alike their titles look.
Pricing
Pay per event. $2 per 1,000 job postings at the Free and Starter tiers, less above that.
| Event | Free | Starter (Bronze) | Scale (Silver) | Business (Gold) |
|---|---|---|---|---|
actor-start — charged once per run, and only after the run has produced at least one job | $0.001 | $0.001 | $0.0005 | $0.0002 |
job-posting — charged once per job row written to your dataset | $0.002 | $0.002 | $0.0015 | $0.001 |
Worked example. You pull 12 companies and get 1,000 postings back on a Starter plan:
actor-start 1 × $0.001 = $0.001job-posting 1,000 × $0.002 = $2.000total $2.001
Three things that are unusual, and deliberate:
- Diagnostic rows are free. A dead board slug, a timeout, a domain with no detectable ATS — none of it is billed.
actor-startis only charged once real results exist. It becomes payable once your input has been validated and the first board is about to be fetched, but it is only actually billed after at least one job row is safely in your dataset. A run that returns no postings — for any reason — finishes FAILED and costs nothing at all, not even the start fee.- Caps are honoured before billing. Hit
maxJobsTotal,maxRunSecs, or your own run budget, and the Actor stops cleanly. You are never charged for a job that did not make it into your dataset.
Integrations and use from code
One JSON object in, one flat array out — the shape agent runtimes want. The Actor runs with limited permissions, uses pay-per-event pricing and never enters Standby, so it works over the Apify MCP server and with x402 agentic payments. The Integrations tab pushes results to Slack, a webhook, Zapier, Make, Google Sheets, Snowflake or BigQuery.
curl -X POST "https://api.apify.com/v2/acts/insight.solutions~ats-jobs-api/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"boards":["greenhouse:stripe"],"includeDescription":false,"maxJobsTotal":100}'
# pip install apify-clientfrom apify_client import ApifyClientclient = ApifyClient("<APIFY_TOKEN>")run = client.actor("insight.solutions/ats-jobs-api").call(run_input={"boards": ["greenhouse:stripe", "ashby:ramp"],"remoteOnly": True,"descriptionFormat": "markdown","maxJobsTotal": 500,})for job in client.dataset(run["defaultDatasetId"]).iterate_items():if job.get("ok") is not False:print(job["companyName"], job["title"], job["locationRaw"], job["applyUrl"], sep=" | ")
Set includeDescription to false when you only need the role list — the payload shrinks by roughly 90% and costs the same.
FAQ
How fresh is the data? Live. Every run reads the ATS endpoint at that moment; nothing is cached except the domain-to-ATS mapping, which records only which board a company uses, never the postings.
Will I get rate limited? These are syndication endpoints — vendors publish them so postings get crawled. The Actor sends at most one request at a time to any single host and backs off exponentially on 429 and 5xx. In normal use you will not see limits.
What stops a run from hanging?
maxRunSecs, 240 seconds by default, checked before each board and each auto-detection probe. When it runs out the Actor stops fetching, keeps and bills for everything already collected, writes a diagnostic row for each board it never reached, and finishes. Auto-detection has a tighter budget of its own: six candidate pages, one attempt each, five seconds apiece.
Why is a company I want missing? It is on an ATS we do not support yet (iCIMS, Taleo, SuccessFactors and BambooHR are the usual suspects); or it uses a supported ATS under a slug that differs from its brand name — check the apply link; or it hosts its own careers page with no ATS behind it, so there is no public feed to read.
Why are salaries often null?
Most postings do not publish one. We fill salaryMin/salaryMax only from structured compensation data the ATS actually returns — never from "competitive salary" or numbers scraped out of description text.
How is this different from calling the ATS APIs myself? For one company, do that. This exists for fifty companies on nine platforms: eleven response shapes, four location formats, two XML feeds, one POST API, HTML entities encoded twice on one of them, and recruiter emails you should not be storing.
Does it handle pagination?
Yes, where the platform has it — SmartRecruiters and Workday page automatically up to maxJobsPerBoard. The others return the whole board in one response.
What happens if one board is down?
The others still run. The failed one produces a diagnostic row, the run finishes SUCCEEDED, and you are not billed for the failure. If every board is down the run finishes FAILED and you are billed nothing at all.
Legal and data-protection notes
- Public endpoints only. Every source is an unauthenticated, publicly documented job-syndication feed or a public career board page. The Actor never logs in, never accepts cookies or session tokens, and never uses an API key belonging to anyone else.
- No personal data by design. Recruiter contact details — email addresses,
mailto:links, personal LinkedIn profile URLs — and recruiter or hiring-manager attribution lines are removed from every description before it is written to your dataset. Recruiter names exposed as structured fields by an ATS are never mapped into the output at all. What this does not do is chase a name written into free prose with no attribution marker; a regex that tried would mangle more descriptions than it cleaned, so we state the limit rather than overstate the guarantee. robots.txtis respected on every HTML page we fetch — the career pages read during domain auto-detection and the JazzHR board page. It is fetched once per domain per run, cached, and evaluated per RFC 9309 against our user-agent token and*; aDisallowmeans the page is skipped, and the board says so in its diagnostic row. The remaining sources are the vendors' own JSON and XML syndication feeds.- You are the controller of what you do next. If you export this data, combine it with personal data, or contact people based on it, GDPR/CCPA obligations are yours, not ours.
- Not affiliated with Greenhouse, Lever, Ashby, Workable, Recruitee, SmartRecruiters, Teamtailor, Personio, Breezy HR, JazzHR, Workday, or with any employer whose postings you retrieve. All product names and trademarks belong to their respective owners and are used only to describe which public feeds this Actor reads.
Our other Actors
| Actor | What it does |
|---|---|
| New Job Postings Monitor | The same 11 ATS connectors with memory: only the roles that opened, closed or changed since your last run |
| Website Contact Extractor | Business emails, phone numbers and social profiles from a list of website domains |
| SEC EDGAR API | SEC filings, normalized XBRL financials and full-text search, by ticker or CIK |
| Wayback Machine Toolkit | Archived URLs, archived page text, and a diff of one page between two dates |