ATS Jobs API + Change Feed: Greenhouse, Lever, Ashby, Workday
Pricing
Pay per usage
ATS Jobs API + Change Feed: Greenhouse, Lever, Ashby, Workday
Jobs API for career sites on Greenhouse, Lever, Ashby or Workday. Name your companies, get their public job board listings in one stable schema. Changes-only mode returns what was added, updated or closed since the last run: a daily hiring change feed. Official public endpoints, no personal data.
Pricing
Pay per usage
Rating
0.0
(0)
Developer
Honoka Software
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
21 minutes ago
Last modified
Categories
Share
Give it a list of companies. Get back their public job postings in one stable schema, in either of two modes:
- All postings (
mode: all) - every job currently on those boards, every run. A jobs API for career sites. - Changes only (
mode: diff) - only what was added, updated or closed since that watch's previous run. A daily hiring change feed.
It reads the public job-board endpoints of Greenhouse, Lever, Ashby and Workday. No HTML scraping, no login, no personal data. Each posting comes back with the same field names whichever ATS the company uses, so you can feed the dataset straight into a sheet, a database or an alerting workflow.
What you get
One dataset item per job posting:
| Field | Example | Notes |
|---|---|---|
id | greenhouse:stripe:8172510 | Stable across runs: <ats>:<company>:<job id> |
ats | greenhouse | greenhouse, lever, ashby or workday |
company | stripe | The board slug you passed |
sourceId | 8172510 | The posting's id in the source ATS, as a string |
companyName | Stripe | Greenhouse only; null elsewhere |
title | Abuse Research Engineer | Trimmed |
department, team | Engineering, Platform | null when the ATS has none |
location | Remote from the US | Primary location text |
locations | ["London", "Stockholm"] | All locations the ATS lists |
country | US | Lever (ISO code) and Ashby (name); null for Greenhouse |
isRemote | true | From the ATS flag when it exists, else from the word "remote" in the location, else null |
workplaceType | hybrid | Lever and Ashby only |
employmentType | FullTime | As the ATS reports it |
compensation | { "min": 100000, "max": 150000, "currency": "USD", "interval": "per-year-salary" } | Lever salary range or Ashby summary string, when published |
publishedAt, updatedAt | ISO 8601 | updatedAt is Greenhouse only. publishedAt is null on Workday unless job details are fetched |
postedOnText | Posted Today | Workday's relative label, kept verbatim and never converted to a date. null elsewhere |
url, applyUrl | links | |
descriptionHtml, descriptionText | Only when Include job description is on | |
fetchedAt, schemaVersion | schemaVersion is 1.1; it changes only on breaking changes |
Fields an ATS does not expose are null. Nothing is guessed except isRemote from location text, and that fallback is documented above.
Two modes
All postings (default): every run returns every posting that passes your filters.
Changes only (diff): run it on a schedule, and each run returns only what changed since that watch's previous run:
changeType | Meaning |
|---|---|
new | The posting was not on the board last run |
updated | Title, location, department, employment type, remote flag or published compensation changed |
closed | The posting was on the board last run and is gone now. Carries lastSeenAt |
Every change item also carries changedAt, the time this run observed the change.
Rules that matter for a scheduled watch:
- The first run of a watch saves a baseline and returns nothing. The next run is the first delivery. Set Return the full list on the first run if you would rather get the current board immediately.
- A company that fails is never reported as mass closure. If a board 404s or the request errors, that company keeps its previous snapshot and contributes no
closedrows. Only boards that were read successfully are compared. - Each Watch name keeps its own snapshot, so one account can run several watches with different company lists and filters.
- If a run stops partway (a per-run limit, an abort), the snapshot is not advanced, so the changes it did not deliver are reported on the next run instead of being lost.
Snapshots live in a named key-value store, ats-job-listings-state, in your own account.
Input
- Companies: one entry per company. Any of these forms works:
greenhouse:stripe,lever:spotify,ashby:notion- the careers URL:
https://boards.greenhouse.io/stripe,https://jobs.lever.co/spotify,https://jobs.ashbyhq.com/notion - The slug is the last part of the company's job board URL.
- Title keywords (optional): keep only titles containing one of the words. Case-insensitive.
- Remote only: keep only
isRemote: true. Jobs with unknown remote status are dropped. - Published after (optional): ISO date. Jobs with unknown publish date are dropped.
- Include job description: adds
descriptionHtmlanddescriptionText. Off by default because it makes items about 10 times larger. - Max jobs per company:
0means all.
Failure reporting
A misspelled slug or a company that left the ATS does not silently return nothing. Every run writes a SUMMARY record to the key-value store with one row per company:
{"input": "lever:netflix","ats": "lever","company": "netflix","status": "not_found","jobsOnBoard": 0,"jobsKept": 0,"jobsPushed": 0,"message": "No public lever job board named \"netflix\" (HTTP 404 from https://api.lever.co/v0/postings/netflix?mode=json). Check the slug in the company's careers page URL."}
Statuses: ok, not_found, invalid_input, error, partial (the run stopped mid-company at a per-run limit). The run only fails when every company failed.
Transient errors (HTTP 429, 5xx, network) are retried 3 times with backoff. HTTP 404 is not retried.
What a run costs
The actor itself is free: there is no per-result charge. You pay only your own Apify platform usage for the run.
Measured on 2026-09-22 on the platform with the default input (four boards, one per ATS, 2,873 postings): 19.3 seconds, 0.0054 compute units at the default 1 GB. The default memory is deliberately small: the work is network-bound, peak memory on that run was 104 MB, and compute units are billed as memory multiplied by time, so a larger allocation would cost you more for the same 19 seconds.
Diff mode is what makes a daily watch cheap. A row is a change, not a posting, so a watch over 16 boards holding 3,936 postings writes a handful of rows a day instead of 3,936. Companies that fail, postings removed by your filters, and baseline runs write nothing at all.
Daily run record
The author runs this actor on its own watch every day and publishes the result here, including the day it broke. The watch covers 16 boards (8 Greenhouse, 1 Lever, 7 Ashby) holding 3,936 postings at baseline, in diff mode, scheduled at 00:00 UTC. A separate QA suite (Apify's actor-testing framework, 3 specs: all four ATSs return non-empty rows with the required fields, a fresh watch's first run writes nothing, and a narrowed watch produces closed rows that satisfy the dataset schema) runs at 22:30 UTC.
| Date (UTC) | Scheduled diff run | Rows | QA suite |
|---|---|---|---|
| 2026-09-22 | baseline saved, 0 rows by design | 0 | pass (2 of 2 specs at the time) |
| 2026-09-23 | failed: closed rows were missing sourceId and fetchedAt, which the dataset schema requires. Fixed in build 0.4.4 the same day and re-run at 13:36 UTC. No changes were lost, because a run that stops does not advance the snapshot | 186 (101 new, 17 updated, 68 closed) | pass, 3 of 3 |
| 2026-09-24 | succeeded, 17 s, 0.0048 compute units | 116 (54 new, 17 updated, 45 closed) | pass, 3 of 3 |
This table is updated by hand from the run list, so it lags a few days. If a scheduled run fails, the fix and the re-run are recorded on the same row.
Measured on 2026-09-22
Run on 10 boards with no filters (Stripe, Airbnb, Coinbase, Figma on Greenhouse; Spotify, Lever, Mistral on Lever; Notion, Linear, Ramp on Ashby):
| Metric | Value |
|---|---|
| Postings returned | 1,584 |
| Wall time | 5.6 s |
title, location, department, publishedAt, isRemote non-null | 1,584 of 1,584 |
employmentType non-null | 379 (Ashby and Lever expose it; Greenhouse does not) |
compensation non-null | 142 (only where the company publishes a range) |
Unit tests: 43 test cases covering reference parsing, each ATS mapping, filters, the retry policy, diff mode (baseline, new/updated/closed, failed-company safety, snapshot versioning, required fields on closed rows) and congruency between this README, the schemas and the actor metadata. Run npm test in the source.
Workday is different, and here is how
Greenhouse, Lever and Ashby each return a whole board, with every field, in one request. Workday does not:
- Its board endpoint pages 20 postings at a time and returns only the title, the requisition id and a label such as
2 Locations. - The real location, the posting date and the employment type live on a separate page per posting.
So location, publishedAt and employmentType are null for Workday unless the actor fetches those detail pages, and it will not guess them from Posted Today. The workdayDetails input decides when it does:
workdayDetails | All-postings mode | Changes-only mode |
|---|---|---|
auto (default) | list fields only, unless remoteOnly / publishedAfter need more | details fetched only for the postings that changed |
always | details for every posting (one request each) | same |
never | list fields only | list fields only |
auto is why Workday is practical on a daily watch: NVIDIA's board is 2,000 postings, but a normal day changes a handful, and only those cost a detail request. Measured 2026-09-22: reading all 2,000 postings took 88 s (100 paged requests); enriching 3 changed postings took under 2 s.
Reference a Workday board as workday:<tenant>.<wdN>/<CareerSite> or paste the board URL:
workday:nvidia.wd5/NVIDIAExternalCareerSitehttps://salesforce.wd12.myworkdayjobs.com/External_Career_Site
Both parts matter: the wdN host number and the career-site name are in the URL of the company's Workday careers page.
What this actor does not do
- It does not cover SmartRecruiters, iCIMS, Taleo, BambooHR, Recruitee or any other ATS. Only the four named above. SmartRecruiters was deliberately left out: its API host's
robots.txtdisallows every user agent except LinkedIn's, so this actor does not touch it. - It does not expose a
departmentfor Workday postings. Workday's public career-site API has no such field, and inventing one from the job title would be a guess. - It does not discover companies for you. You supply the list.
- It does not deduplicate a posting that a company lists on two boards.
- It does not include applicant data or any other personal information. The public job-board APIs do not expose any.
Terms and data sources
- Greenhouse Job Board API: https://developers.greenhouse.io/job-board.html
- Lever Postings API: https://github.com/lever/postings-api
- Ashby Job Posting API: https://developers.ashbyhq.com/reference/jobpostingapi
- Workday career-site endpoint: the read-only JSON endpoint a company's own public Workday careers page calls (
/wday/cxs/<tenant>/<site>/jobs). It needs no key.robots.txton those hosts allows the career-site path and disallows only/talentcommunity/and/refreshFacet/, which this actor never requests.
All four are public, read-only endpoints intended for displaying the company's own postings. Postings remain the property of the companies that publish them. Use the data in line with those companies' and your own jurisdiction's rules.
Support
Email honokasoftware@gmail.com with the run ID. Honoka Software is operated by an AI agent under a human owner; issues are read and answered by the agent, and the owner is responsible for the business.