ATS API — Job Board, Job Listings & Applicant Tracking | $3/1K
Pricing
from $3.00 / 1,000 job listings
ATS API — Job Board, Job Listings & Applicant Tracking | $3/1K
Unofficial — not affiliated with any ATS vendor. ATS aggregator and job board API: give company names, domains or careers page URLs, it detects the applicant tracking system (Greenhouse, Lever, Ashby, Workday, Workable, Personio, Rippling) and normalizes every open role into one schema.
Pricing
from $3.00 / 1,000 job listings
Rating
0.0
(0)
Developer
KENKO
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
21 days ago
Last modified
Categories
Share
ATS Jobs Scraper — Greenhouse, Lever, Ashby, Workday | $3/1K
Unofficial. This Actor is not affiliated with, endorsed by, or sponsored by Greenhouse, Lever, Ashby, Workday, Workable, Personio or Rippling. All product names and trademarks belong to their respective owners and are used here only to describe which job boards this Actor can read. It reads each vendor's own public feed, subject to that vendor's
robots.txt.
A job listings API for company career pages. Give it company names. Get back every open role, normalized.
{ "companies": ["stripe", "linear.app", "https://jobs.lever.co/spotify"] }
You do not have to know which applicant-tracking system a company uses. The Actor works it out, reads the board, and returns one row shape no matter how many different systems a run touched.
Use it as a Greenhouse job board API, a Lever jobs API, an Ashby jobs API, a Workday job board API or a Workable jobs API — or point it at a mixed list and let it sort out which is which.
Why this exists
Job data on the market is scraped HTML. That breaks every time a career page is redesigned, and it needs proxies and anti-bot handling to collect at all.
This reads the JSON and XML feeds the ATS vendors publish themselves —
boards-api.greenhouse.io, api.lever.co, api.ashbyhq.com and four more.
Nothing here logs in, uses a proxy, solves a challenge, spoofs a user agent, or
parses a layout. The user agent is atsfeed/1.0 and says where it came from.
Six of the seven are documented, unauthenticated vendor endpoints. Workday is the exception and is labelled as one: it is an undocumented career-site endpoint, read from a career-site URL you supply, not a published API. It is included because it is where a large share of enterprise roles actually live — but you should know which of these you are depending on.
What it will not read
Every host this Actor asks for job data is checked against its own robots.txt
before that first request, and a Crawl-delay published by the host it addresses
sets the spacing. If a vendor changes its mind about who may read a feed, this
Actor stops on its own rather than waiting for someone to notice.
That includes a host reached by redirect, which is the one place the claim could
quietly fail. Redirects are followed by hand rather than by the HTTP runtime — the
Location header decides the hop, before anything is sent to the destination. A
redirect to a different site is refused outright, so that host is never addressed at
all, not even for its robots.txt; a same-site hop reads the destination's own
robots.txt first rather than inheriting the origin's verdict, and if that check
disallows the path the payload is never requested. src/fetch.mjs is where that
happens, and test/redirect.test.mjs asserts the order the requests go out in,
because a test on the returned value passes either way.
What the destination's file settles is whether the path may be read, not the pace: a
Crawl-delay it publishes does not slow the hop, which rides the queue of the host
that redirected. Workable is the only source that redirects at all, and
apply.workable.com publishes no Crawl-delay.
The one fetch that is not gated is /robots.txt itself: a host may redirect that
file, including to another host, and the redirect is followed — RFC 9309 §2.3.1.2
says to, and gating a robots.txt read on a robots.txt read would not terminate.
A host reached that way is asked for that one file and nothing else, and its rules
are applied to the host that redirected.
That check is why two systems are not supported:
- SmartRecruiters —
api.smartrecruiters.com/robots.txtallows exactly one agent,LinkedInBot, and disallows everything else. We are everything else. - Breezy — its
/jsonendpoint is undocumented and its terms speak against automated collection.
Requests are also capped per host — at most 4 in flight and 5 per second, and half that into a single Workday tenant. A whole 2,000-role Workday board takes about 2½ minutes rather than 8 seconds. That is deliberate: a job feed that gets its source annoyed is worth nothing next month.
Supported systems
| ATS | Addressed by | Descriptions | Endpoint |
|---|---|---|---|
| Greenhouse | slug, boards.greenhouse.io/… | yes | documented |
| Lever | slug, jobs.lever.co/… | yes | documented |
| Ashby | slug, jobs.ashbyhq.com/… | yes | documented |
| Workable | slug, apply.workable.com/… | yes | documented |
| Personio | slug, *.jobs.personio.de | yes | documented |
| Rippling | slug, ats.rippling.com/… | — | documented |
| Workday | full careers URL (*.myworkdayjobs.com/…) | — | undocumented |
Workday is addressed by URL rather than name because its host encodes the data centre and its path encodes both tenant and site — none of which can be derived from a company name.
Input
Everything but companies is optional.
| Field | Meaning |
|---|---|
companies | Names, domains, board URLs, or provider:slug. |
keywords / excludeKeywords | Title filters. Whole-word, so go does not match Google. Multi-word entries match as phrases. |
locations / countries / departments | Further filters. countries takes ISO-2 codes. |
remoteOnly | Remote only. Hybrid is excluded, and so are roles the board says nothing about. |
postedWithinDays | Recency. Undated roles are excluded — they cannot be shown to be recent. |
searchDescriptions | Match keywords against the body text too. |
includeDescription / includeDescriptionHtml | Add the description as plain text and/or original HTML. |
maxJobsPerCompany / maxJobsTotal | Cost ceilings. Large Workday tenants list 20,000+ roles. |
Output
One row per role:
provider company companySlug jobId title department teamemploymentType seniority location locations[] country workplaceTypeisRemote postedAt updatedAt salaryText salaryMin salaryMaxsalaryCurrency salaryPeriod url applyUrl description descriptionHtmlscrapedAt
Measured coverage over 3,006 live roles from 17 boards across all 7 systems
on 2026-08-12, capped at 500 roles per board. The panel is public and named, so
this is a number you can check rather than one you have to take on trust — run
this Actor over stripe, figma, databricks, spotify, gopuff, alloy,
linear, ramp, vanta, persado, blueground, skroutz, holidu,
everphone, ottonova, rippling and NVIDIA's Workday site, with
maxJobsPerCompany: 500, and count the nulls yourself.
| Field | Present |
|---|---|
title, url, applyUrl, location | 100% |
country | 97.8% |
postedAt | 88.0% |
department | 83.0% |
updatedAt | 38.6% |
workplaceType | 36.2% |
employmentType | 32.2% |
team | 32.0% |
salaryText, salaryMin, salaryCurrency | 18.8% |
salaryPeriod | 3.5% |
seniority | 2.7% |
Every board in that panel returned; a run with one board down is a different
table, and a smaller one is not necessarily worse — the first attempt at this
measurement lost 500 pay-less Lever roles to a timeout and reported salaryMin
at 22.5% instead of 18.8%. The script prints per-board row counts for exactly
that reason.
The lower rows are not gaps in this Actor — they are fields the boards
themselves leave blank. Rippling publishes no dates at all (0 of 362); most
companies publish no pay. A field is null when the source did not say. No
pay and no date is guessed to fill a column, because a guessed salary is worse
than an empty one.
Read the middle of that table per system, not as an average. A column at 32% is usually a column that is complete on some systems and absent from the others, which is a very different thing from a column that is unreliable everywhere:
| Field | 100% on | absent from |
|---|---|---|
seniority | Personio | every other system |
updatedAt | Greenhouse | every other system |
team | Ashby, Lever, Personio | Greenhouse, Rippling, Workday |
employmentType | Ashby, Personio (Lever 98%) | Greenhouse, Rippling, Workday |
So if you need team, the answer is not "32% of the time" — it is "always, on
three of the seven systems, and never on three others." workplaceType is the
one genuinely per-posting field: 100% on Lever, 97% on Ashby, and a minority
everywhere else, because it is inferred from location text that most boards
write freely.
salaryPeriod is the sharpest case of that rule, and the reason it sits far
below the other pay columns: employers commonly label a band
"Zone 1 Pay Range"null — on 275 of the 371
Greenhouse rows that do carry pay. Where one posting quotes several zones in
different currencies, the numeric columns stay null too and only salaryText
ships, so nothing is averaged into a figure the employer never published.
Six fields are computed rather than read off the board: country,
workplaceType and isRemote from the location text, and on Lever and Ashby
the four salary scalars parsed out of the compensation string those boards
publish. Greenhouse is the exception, and the better case — it serves pay as
structured numbers, so there the scalars are read rather than parsed and
salaryText is rendered from them, not the other way round. The two workplace
fields are inferences and are sometimes wrong — "Remote, OR" reads as remote
work, not Oregon — and the judgement calls below are the rules they follow. A
seventh field, department on Workday, is computed too but not from the row: it
costs extra requests to the same host, and the next paragraph is that story.
department is 100% on Greenhouse, Ashby and Personio, 99% on Lever and 97% on
Workable — and 0% on Workday, which drags the average down on its own. Workday
postings carry no category at all, only a
jobFamilyGroup search facet. Taking a whole Workday board fills
department for 100% of its rows by replaying the search once per category —
NVIDIA's 2,000 postings, fully attributed. Taking a slice does not, because
labelling even 300 rows means sweeping all 2,617 postings, and the run log says
so out loud when it declines.
The 2.2% of rows with no country are rows whose location names no place the
rules below can resolve. Three kinds, all real: a placeholder ("Remote",
"N/A", "LOCATION"), an internal region code ("AMER", "USCA"), or a
place written in a form the rules deliberately do not cover — a US state
spelled out rather than coded ("Virginia", "Remote - Texas"), or a city
outside the dominant-reading list ("Köln", "Taipei City"). The last kind is
the honest cost of the Berlin, DE rule below: recognising bare state names
as well as codes would resolve them, at the price of reading "Georgia" as a US
state when it is a country.
Four judgement calls worth knowing about:
workplaceTypeisnull, not"onsite", when a board is silent. Silence is not evidence of an office.- A role open in several places is
"remote"if any one of those places reads remote. The order a board lists its offices in is its ordering, not a statement about the role, so reading only the first would decideremoteOnlyat random. - A bare two-letter code in a location is read as a US state (
Austin, TX→ US) because US boards writeCity, STconstantly while non-US boards spell the country out (Berlin, Germany). SoBerlin, DEresolves to US — spell the country out and it resolves correctly. - A city with one dominant reading resolves its own country (
London→ GB,Bengaluru→ IN). This is only consulted after subdivision codes, so the US homonyms still win where a board qualifies them:London, KY→ US.
Pricing
Pay per job returned: $3 per 1,000 roles, flat — the same rate on every
plan, with no volume tier that quietly makes the number in the title wrong for
whichever tier you happen to be on. Plus a $0.005 run start fee. maxJobsTotal
is a hard ceiling, so a run cannot cost more than you allowed it to — including
after a platform migration mid-run.
Notes
- De-duplication: companies commonly run two boards at once (a Greenhouse board and a Workday mirror). The same role from both collapses to one row.
- Missing companies: if a company self-hosts its careers page or uses an ATS
not listed above, the run reports it under
SUMMARY.failuresrather than failing. Pass a board URL directly if auto-detection misses it. - Moved boards: a slug whose board has been closed usually redirects to the vendor's marketing site. That is reported as a miss, not retried.
- Resumable: progress is checkpointed, so a migrated run neither re-fetches nor re-charges for boards already delivered.
- Downstream use:
apply.workable.compublishes. It does not restrict this Actor, but it is a stated preference about what may be done with the rows afterwards, and you should know it exists.Content-Signal: ai-train=no
Common questions
How do I scrape company career pages without breaking every month? Don't parse the page. Every system here publishes a machine-readable feed behind the careers page, and that feed changes far less often than the design does. This Actor reads the feed.
Is there a free job board API?
The vendor feeds are free and unauthenticated — you can call
boards-api.greenhouse.io yourself. What you pay for here is the part that is
tedious rather than hard: working out which of seven applicant tracking systems
a company is on, addressing each one correctly, obeying seven different
robots.txt files and rate limits, and flattening seven incompatible payloads
into one row shape with real country and salary parsing.
Can I get a job posting API across many companies at once?
Yes — pass a list. Boards are fetched concurrently within the per-host limits,
duplicates across a company's two boards collapse to one row, and
maxJobsTotal caps what the run can cost you.
Which applicant tracking system API does a given company use?
Pass the company name or domain and read the provider column. Detection is the
first thing the run does, and it is reported per company in SUMMARY.
Development
npm installnpm test # 66 tests, no networknpm run demo # live run against real boards