Career Site Jobs API — Greenhouse, Lever, Ashby & 10+ ATS avatar

Career Site Jobs API — Greenhouse, Lever, Ashby & 10+ ATS

Pricing

from $0.60 / 1,000 job posting returneds

Go to Apify Store
Career Site Jobs API — Greenhouse, Lever, Ashby & 10+ ATS

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. No API key.

Pricing

from $0.60 / 1,000 job posting returneds

Rating

0.0

(0)

Developer

Insight Solutions

Insight Solutions

Maintained by Community

Actor stats

0

Bookmarked

3

Total users

1

Monthly active users

9 hours ago

Last modified

Share

Scrape job postings from any company career page. Give this Actor a list of career boards — or just company domains, or one of the built-in presets — 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 13 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. $1 per 1,000 job postings, failed boards are free, filtered-out postings 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. That block is the Store prefill — paste it and run.

Do not know the slugs? Use a preset instead — {"boards": ["preset:ai-labs"]} expands to a bundle of verified boards, and {"boards": [], "listPresets": true} prints the whole registry for free before you spend anything. Filters such as titleFilter, seniorityLevels and salaryFloor run before billing, so anything they exclude costs nothing.

Output — one row per job posting; the fields you will use most are title, companyName, locationRaw, isRemote, salaryMin/salaryMax and applyUrl, 27 fields in all (full list under Output reference). A board that could not be read comes back as a free diagnostic row (ok: false, errorType, error, input) instead of a charge.

Price — $1 per 1,000 job postings on the FREE tier (+ $0.001 per run, charged only once a job row exists); dead board slugs, timeouts, domains with no detectable ATS and anything stopped by maxJobsTotal, maxRunSecs or your own budget are free, and a run that returns no postings bills nothing at all. Pay-per-event, no API key, no browser, limited permissions — works over the Apify MCP server (mcp.apify.com) and with agentic (x402) payments.

From code — client.actor("insight.solutions/ats-jobs-api").call(run_input={…}) with apify-client, or POST https://api.apify.com/v2/acts/insight.solutions~ats-jobs-api/run-sync-get-dataset-items.

Board presets — when you do not have a slug list

Pass preset:<name> anywhere in boards and it expands into a bundle of boards we have verified against the live endpoint. Presets mix freely with your own entries, and a preset that shares a board with another entry is deduplicated.

PresetWhat is in it
preset:yc-topLarge, still-hiring Y Combinator alumni
preset:fintechPayments, banking, brokerage, cards, payroll and crypto
preset:devtoolsDatabases, observability, CI, edge and data platforms
preset:ai-labsFrontier model labs and AI-native products

Run {"boards": [], "listPresets": true} to get the whole registry back as free rows — one per board, with the company, the presets it belongs to and the evidence we hold for it (evidence, e.g. "Live read 2026-09-17: … returned 300 postings"). That run reads no career board, bills nothing, and finishes SUCCEEDED. A preset name we do not know is a free unknown-preset diagnostic and the rest of the run carries on.

Boards move and companies switch ATS, so a slug that was live when the registry was built can go dead later; when it does you get the usual free board-not-found row naming the URL we tried, not a charge.

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.

ATSBoard identifierNotes
Greenhousegreenhouse:stripeFull descriptions, departments, offices, structured pay ranges
Leverlever:leverdemoFull descriptions, commitment, workplace type, salary range
Ashbyashby:rampFull descriptions, department/team, compensation tiers, remote flag
Workableworkable:lystDescription, requirements, benefits, multi-location, telecommuting flag
Recruiteerecruitee:vandebronDescription + requirements, remote/hybrid/on-site, salary when published
SmartRecruiterssmartrecruiters:VisaPaginated postings plus the full public job ad
Teamtailorteamtailor:oatlyJSON Feed + JobPosting data: description, locations, salary, employment type
Personio (beta)personio:personioXML feed, titled sections, offices. Personio rate-limits some feeds from cloud IPs (HTTP 429); those return a free diagnostic row
Breezy HRbreezy:new-incentivesStable feed, but it carries no posting body — descriptionText is null
JazzHR (beta)jazzhr:company3Public board page only — title, location, department, apply link. No unauthenticated feed exists, so no description
Workdayworkday:nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSiteFull descriptions, locations, requisition IDs. Verified live; needs the board host and site name, not a short slug
BambooHR (new)bamboohr:beehiivTitle, department, employment status, location and the posting body. Pay is published as one free-text string, so salaryMin/salaryMax stay null. postedAt comes from the posting page, so it needs includeDescription: true
Rippling (new)rippling:skillable-careersTitle, department, work locations, employment type, created date and the posting body. Pay is published as one free-text string, so salaryMin/salaryMax stay null

The two marked new were built against live captures taken on 2026-09-17 and are covered by fixture tests, but they have not yet completed a scheduled run on the platform. If one behaves unexpectedly on a board we have not seen, it produces a free diagnostic row like any other, and nothing is billed for it.

One bound worth knowing. Workday, SmartRecruiters, BambooHR and Rippling serve each posting body from a separate document, so those four 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 thirteen parsers.
  • Track who is hiring for a role — titleFilter, seniorityLevels and remoteOnly return every open senior remote engineering role on a watchlist, apply URL included, and you pay only for the ones that matched.
  • 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/salaryCurrency come 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 13 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.com instead of hunting for a board slug — or a preset when you have no list at all.
  • Filters that cut your bill, not just your rows. Every filter runs before billing, and the run tells you in a free summary row how many postings it read and how many it excluded.
  • Charge-on-success, written into the pricing table. A board that 404s produces a free diagnostic row. A run whose boards were all usable but produced nothing — a slug that does not exist, a domain with no detectable ATS, a board that refused us, filters that matched nothing — finishes SUCCEEDED with zero results, a status message that says so, and a free diagnostic row per board telling you why; nothing is charged, start fee included. A run finishes FAILED only when there was nothing usable to attempt, or the Actor itself hit an error.
  • Recruiter contact details stripped from every posting body, and robots.txt honoured 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

FieldTypeDefaultWhat it does
boards (required)array of strings["greenhouse:stripe","ashby:ramp"]One entry per company: ats:slug, a career-page URL, a bare domain for auto-detection, or preset:<name> for a bundle
listPresetsbooleanfalseReturn the board registry as free rows and read no career board at all. Pass "boards": [] with it
includeDescriptionbooleantrueTurn off for a much smaller, faster result
descriptionFormattext | markdown | htmltextHow the posting body is rendered
remoteOnlybooleanfalseKeep only postings the ATS flags remote, or whose workplace-type or location text says remote. Job titles are never used for this
maxJobsPerBoardinteger1000Per-board ceiling
maxJobsTotalinteger5000Whole-run ceiling. The run stops cleanly; you are not charged for jobs not returned
atsPlatformsmulti-selectallRestricts which platforms auto-detection considers. Ignored for explicit ats:slug entries
maxRunSecsinteger240Whole-run wall-clock budget. When reached the Actor stops fetching, keeps what it has, and says so
requestTimeoutSecsinteger20Per-request timeout. A URL is abandoned once its attempts have used twice that, or 40 seconds, whichever is longer
maxConcurrencyinteger6Boards fetched in parallel. Never more than one request at a time to any single host

Result filters

All of them are off by default, all of them are optional, and all of them run before you are charged — a posting a filter removed is never written to your dataset and never billed. One free filter-summary row per run reports how many postings were read and how many were excluded, so a heavily filtered run never looks like an outage.

FieldTypeDefaultWhat it keeps
titleFilterarray of stringsnonePostings whose title contains any of these, case-insensitively. ["engineer","developer"] keeps both
titleExcludearray of stringsnoneDrops postings whose title contains any of these. Applied after titleFilter, so a posting matching both is dropped
seniorityLevelsmulti-selectallintern, junior, mid, senior, lead, principal, manager, director, executive. Read from the title — see the rule below
employmentTypesmulti-selectallFullTime, PartTime, Contract, Temporary, Internship, Volunteer, Other. Postings whose ATS published no type are excluded when this is set
workplaceTypesmulti-selectallremote, hybrid, onsite. A posting counts as remote when the ATS says so or its location text does. Postings with no signal are excluded when this is set
countryCodesarray of stringsnoneISO 3166-1 alpha-2 codes (US, GB, DE). Country names the ATS wrote out are matched too, and a US state or Canadian province implies its country
locationFilterarray of stringsnoneCase-insensitive substring match against the location, for cities and regions a country code cannot express
hasSalarybooleanfalseOnly postings carrying a structured pay range. Most postings do not publish one, so expect this to remove most rows — and to cut your bill by the same amount
salaryFloorinteger0Only postings whose pay reaches this number, compared against the top of the range (falling back to the bottom) in the posting's own currency and period, with no conversion. Unpriced postings are excluded
postedWithinDaysinteger0Only postings first published within this many days. Postings with no published date are kept
postedAfterdatenoneKeep postings first published on or after this date. Postings with no published date are kept

How seniorityLevels reads a title. No ATS publishes a seniority level, so the Actor derives one from the title and the filter selects on it. junior covers junior, associate, entry-level, graduate, apprentice and trainee wording; lead also covers Staff; executive also covers VP and C-level titles; director also covers Head of. mid means a title carrying no seniority word at all — Software Engineer, Account Executive — which is what mid-level means in practice. The seniority output field is unchanged and stays finer-grained than the filter's vocabulary.

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.bamboohr.com/careers/… → bamboohr:acme, ats.rippling.com/acme/jobs/… → rippling: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"
}

Free rows

Everything that is not a job posting carries ok: false and a rowType, and none of it is ever charged for. Filter with ok !== false for job rows only — that rule has not changed.

rowTypeWhen you get itWhat else it carries
diagnosticA board could not be readerrorType (a stable slug — board-not-found, timeout, rate-limited, robots-disallowed, unknown-preset, …), error in plain language with the URL we tried, input, atsPlatform, boardSlug, source, sourceUrl, checkedAt
filter-summaryOnce per run, when any filter was activejobsRead, jobsExcludedByFilters, jobsDeduplicated, jobsReturned
preset-entryOnce per registry board, on a listPresets runcompanyName, presets, evidence, registryVersion

errorType is there so you can branch on the reason without pattern-matching English; the wording of error may improve, the slugs will not change meaning.

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. $1 per 1,000 job postings at the Free and Starter tiers, less above that.

EventFreeStarter (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.001$0.001
job-posting — charged once per job row written to your dataset$0.001$0.001$0.0008$0.0006

Worked example. You pull 12 companies and get 1,000 postings back on a Starter plan:

actor-start 1 × $0.001 = $0.001
job-posting 1,000 × $0.001 = $1.000
total $1.001

Four things that are unusual, and deliberate:

  1. Every row that is not a job posting is free. A dead board slug, a timeout, a domain with no detectable ATS, the filter summary row, an entire listPresets listing — none of it is billed.
  2. Filters run before billing. titleFilter, seniorityLevels, countryCodes, hasSalary, salaryFloor and the rest are applied to the row before it reaches your dataset, so narrowing the result narrows the invoice by exactly the same amount. The free filter-summary row shows the arithmetic.
  3. actor-start is 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 — costs nothing at all, not even the start fee: it finishes SUCCEEDED with zero results and a status message saying why, or FAILED if there was nothing usable to attempt.
  4. 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.

Use it from an AI agent, or 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-client
from apify_client import ApifyClient
client = 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 and SuccessFactors 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. BambooHR and Rippling used to be on that list and are now supported.

Why did a preset return fewer boards than I expected? A preset is a list of board slugs, not a live index. Companies switch ATS and boards get renamed, so some entries go dead between registry updates — each one produces a free board-not-found row naming the URL we tried, and the rest of the preset still runs. Every entry was read successfully from its live endpoint on the date in registryVersion; run listPresets to see that evidence per board.

Do filters save me money, or just bandwidth? Money. A posting a filter excludes is never written to your dataset, and the job-posting charge is raised per row written. The free filter-summary row reports how many postings were read against how many were returned, which is exactly what you paid for.

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: thirteen response shapes, four location formats, two XML feeds, one POST API, one vendor that answers a dead board with a redirect to its own marketing site, HTML entities encoded twice on another, 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 still finishes SUCCEEDED — with zero results, a status message saying so, one free diagnostic row per board, and nothing billed, start fee included. It finishes FAILED only when none of your entries could be used as a board at all, or the Actor itself hit an error.

  • 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.txt is 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 *; a Disallow means 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, BambooHR, Rippling, 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

Every Insight Solutions Actor is pay-per-result with no browser, no login and no API key, and every one of them returns free diagnostic rows instead of billing for failures. Prices are per 1,000 results.

Video, audio & social

News, documents & the web

Business, finance & jobs

Apps & games