ATS Jobs API: Greenhouse, Lever, Ashby & More
Pricing
from $1.50 / 1,000 jobs
ATS Jobs API: Greenhouse, Lever, Ashby & More
Live open jobs straight from 7 ATS public job-board APIs (Greenhouse, Lever, Ashby, SmartRecruiters, Workable, Recruitee, Personio). Normalized schema, salary from structured fields, and new/closed job tracking between runs. No scraping, no proxies.
Pricing
from $1.50 / 1,000 jobs
Rating
0.0
(0)
Developer
Dano Desk
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Security
Limited permissionsShare
ATS Jobs API: Greenhouse, Lever, Ashby & More (+ Hiring Velocity, Ghost-Job Score & Company Enrichment)
Get every open job of the companies you choose, live from the employer's own applicant tracking system (ATS). Each run reads the official public job-board APIs that ATS vendors publish so employers' jobs can be shown and syndicated. There's no HTML scraping, no proxies and no stale cache: change the input and the output changes.
Supported ATS (7): Greenhouse · Lever (global + EU) · Ashby · SmartRecruiters · Workable · Recruitee · Personio
Why this Actor
- Live, not cached. Every run calls each company's board API directly, so the results reflect the board at that moment.
- One clean schema across 7 ATS: title, company, locations, ISO countries, remote flag and workplace type, department/team, normalized employment type, salary, posted/updated dates, job and apply URLs, and the description as text and/or HTML.
- Salary you can trust. It uses structured pay data where the ATS provides it (Ashby compensation, Lever salaryRange, Recruitee salary, and Greenhouse pay ranges when enabled). Otherwise a conservative parser pulls a clear currency range from the description, and the matched text is always attached (
salary.text,salary.source). There are no AI-guessed fields. - Hiring velocity mode. Each job's first-seen and last-seen times are remembered in a named key-value store. Every run flags new jobs and emits closed ones, and OUTPUT gets per-company
opened / closed / netcounts and new roles by department. Schedule it daily or weekly for sales triggers, investor signals, or job alerts. - Ghost-job score on every job. A transparent 0-100 risk score with the exact signals and points behind it (age, posted-date refresh, repost of a role that just closed, talent-pool wording, duplicate postings, no pay range in a pay-transparency state, thin description, stagnant board). Filter with
maxGhostScore. - Company-level enrichment. Feed it a dataset from any other Actor (Google Maps, lead lists, company lists): each row's website is checked for a supported ATS and you get one hiring-signal row per company: open jobs, posted in the last 30 days, departments, locations, remote and salary share, salary span, median days open, ghost counts, and new/closed since the last run.
- One bad board never fails the run. Each company is handled separately, and errors are reported per company with a reason (
BOARD_NOT_FOUND,ATS_NOT_DETECTED, HTTP status…). - Polite by design. Requests are paced per host (≥250 ms apart, max 2 in parallel per ATS), retried with exponential backoff on 429/5xx (honoring
Retry-After), and only GET endpoints for published jobs are called. Application and candidate endpoints are never touched.
Input
Add companies one per line, in any of these forms:
| Form | Example |
|---|---|
| Careers URL (auto-detected) | https://jobs.lever.co/spotify, https://boards.greenhouse.io/airbnb, https://jobs.ashbyhq.com/ramp, https://apply.workable.com/skroutz, https://bunq.recruitee.com, https://ottonova.jobs.personio.de, https://jobs.smartrecruiters.com/BoschGroup |
ats:token | greenhouse:stripe, lever:eu/acme, ashby:ramp, smartrecruiters:BoschGroup, workable:skroutz, recruitee:bunq, personio:ottonova |
| Bare token (probed across all 7) | stripe |
| Custom careers page | https://careers.acme.com/jobs: fetched once (robots.txt respected) to find a Greenhouse, Lever, Ashby or other embed |
| Name override | lever:spotify|Spotify (Lever and Ashby APIs don't return a company name) |
Filters (applied after fetching, so tracking still sees the full board): keywords (title, optionally description), exclude keywords, locations or countries (remote accepted), remote only, workplace types, departments/teams, employment types, and posted after (2026-10-01 or 7 days).
Output modes: all open jobs (plus closed since last run), only new, or changes (new + closed).
{"companies": ["greenhouse:airbnb", "https://jobs.lever.co/spotify|Spotify", "https://jobs.ashbyhq.com/ramp"],"keywords": ["engineer"],"locations": ["US", "remote"],"postedAfter": "14 days","outputMode": "all","descriptionFormat": "text","maxJobsPerCompany": 50}
Output (job level: one item per job)
{"jobId": "ashby:ramp:0d52b853-…","ats": "ashby", "atsJobId": "0d52b853-…", "companyToken": "ramp", "company": "ramp","title": "Lead Recruiter | Executive","location": "New York, NY (HQ)", "locations": ["New York, NY (HQ)", "San Francisco, CA", "Remote (US)"], "countries": ["US"],"remote": false, "workplaceType": "hybrid", "department": "People & Talent", "team": "People & Talent","employmentType": "full_time", "employmentTypeRaw": "FullTime","salary": { "min": 180000, "max": 250000, "currency": "USD", "period": "year", "text": "$180K - $250K", "source": "ats_structured" },"postedAt": "2026-10-19T17:06:15.365Z", "updatedAt": null,"url": "https://jobs.ashbyhq.com/ramp/0d52b853-…", "applyUrl": "https://jobs.ashbyhq.com/ramp/0d52b853-…/application","descriptionText": "ABOUT RAMP …","status": "open", "isNew": true, "firstSeenAt": "…", "lastSeenAt": "…", "daysOpen": 1, "scrapedAt": "…"}
Closed jobs come through as { "status": "closed", "closedAt", "firstSeenAt", "lastSeenAt", "daysOpen", "title", "url", ... }.
Dataset views: Jobs, New & closed, and Salaries. The OUTPUT record holds the run summary: per company, the status, open/matched/output counts, new/closed counts, velocity, and any error with its reason. It also has totals by ATS and the HTTP request count.
Ghost-job score
Every open job gets ghostScore (0-100), ghostRisk (low < 25, medium 25-49, high ≥ 50), effectiveAgeDays and ghostSignals. A real example (Airbnb on Greenhouse, 2026-10-20):
"ghostScore": 50, "ghostRisk": "high", "effectiveAgeDays": 223,"ghostSignals": [{ "code": "very_old_posting", "points": 35, "detail": "open ~223 days" },{ "code": "evergreen_language", "points": 15, "detail": "description mentions a talent pool / future openings" }]
| Signal | Points | Based on |
|---|---|---|
aging_posting / old_posting / very_old_posting | 8 (45 d), 15 (60 d), 25 (90 d), 35 (180 d) | Earlier of the ATS posted date and our own first-seen date |
not_updated | 10 | ATS updated date ≥ 90 days ago on a ≥ 90-day-old job |
date_refreshed | 15 | The ATS posted date moved ≥ 7 days after we first saw the job (needs tracking) |
reposted | 20 | A new job id with the same title + location as a job that closed on this board in the last 120 days (needs tracking) |
duplicate_postings | 8 | 3+ open postings with the exact same title + location |
evergreen_title / evergreen_language | 30 / 15 | Talent pool, general/open application, future openings, "always hiring" wording |
no_pay_range_in_pay_transparency_state | 10 | No structured or clearly stated pay range, and a location in a US state/DC whose posting law requires one (CA, CO, CT, DC, HI, IL, MA, MD, ME, MN, NJ, NY, VT, VA, WA as of Oct 2026) |
thin_description | 5 | Description under 300 characters |
board_stagnant | 10 | No new job on the board for 45+ days of tracking |
This is a heuristic risk indicator, not a finding that a job is fake, and it is not legal advice (pay-law thresholds and remote-role rules vary). Signals that need history only start after the second tracked run. SmartRecruiters jobs skip the description and pay signals because the list API has no description.
Company level and dataset enrichment
Set Output level = companies for one row per company, or point Enrich a dataset at another Actor's dataset (it defaults to company level). Each row's website, domain or careers URL is found automatically (careersUrl, website, domain, url, …) or from Company field(s).
- ATS links and URLs are read directly. For a plain website, the homepage is fetched (robots.txt respected), then up to 2 careers links on the same site (or
/careersand/jobs). That's at most 3 page fetches per company, and each company is looked up once even if it appears on many rows. - Companies with no supported ATS are listed in
OUTPUTand left out of the dataset unless Output rows with no ATS found is on. - Fields to copy from each row go into
inputRow, so you can join results back to the source dataset.
{"inputValue": "https://www.figma.com", "inputField": "website", "inputRow": { "title": "Figma", "website": "https://www.figma.com" },"status": "ok", "company": "Figma", "ats": "greenhouse", "token": "figma", "boardUrl": "https://boards.greenhouse.io/figma","careersPageUrl": "https://www.figma.com/careers/", "detectedBy": "careers_html","isHiring": true, "openJobs": 141, "matchedJobs": 141, "postedLast30Days": 37, "medianDaysOpen": 88,"remoteShare": 0, "withSalaryShare": 0.652, "salarySpan": { "currency": "USD", "period": "year", "jobs": 83, "lowest": 70500, "highest": 376000, "medianMin": 155500 },"departments": [{ "name": "Sales", "count": 45 }, { "name": "Engineering", "count": 26 }, ...], "locations": [...], "countries": [...], "employmentTypes": [...], "newestTitles": [...],"ghost": { "high": 0, "medium": 69, "low": 72, "avgScore": 20, "topSignals": [{ "name": "very_old_posting", "count": 45 }, ...] },"newJobs": null, "closedJobs": null, "netChange": null, "trackingNote": "tracking off", "checkedAt": "…"}
(Real output from a local test on 2026-10-20 with tracking off, shortened. With tracking on, newJobs/closedJobs/netChange fill in from the second run.) Filters (keywords, locations…) narrow the stats to matching jobs, while openJobs stays the full board count.
How tracking works
- On the first run for a company, a baseline is saved and nothing is flagged new, so you never get "500 new jobs" on day one.
- On later runs, a job not seen before is
isNew: true. A job that disappeared becomes acloseditem and is removed from state. - State is per board in the named KV store (
ats-jobs-trackerby default). Use different store names for separate watchlists. - If a board couldn't be fully fetched (very large SmartRecruiters boards beyond the page cap), closed-job detection is skipped for it. A missing page never produces false "closed" jobs.
Sources and terms
| ATS | Endpoint | Documentation |
|---|---|---|
| Greenhouse | boards-api.greenhouse.io/v1/boards/{token}/jobs?content=true | Job Board API: "Job Board data is publicly available, so authentication is not required for any GET endpoints." |
| Lever | api.lever.co/v0/postings/{site}?mode=json (EU: api.eu.lever.co) | Postings API: "all job postings in the published state are publicly viewable. These jobs may be scraped by third parties." |
| Ashby | api.ashbyhq.com/posting-api/job-board/{name}?includeCompensation=true | Public Job Posting API |
| SmartRecruiters | api.smartrecruiters.com/v1/companies/{id}/postings (+ /postings/{id} for descriptions) | Posting API, listed under "No authentication… public data" |
| Workable | apply.workable.com/api/v1/widget/accounts/{subdomain}?details=true | Public published-jobs endpoint documented in Workable Help ("Using the Workable API to create a careers page") |
| Recruitee | {company}.recruitee.com/api/offers/ | Careers Site API |
| Personio | {company}.jobs.personio.de/xml | Recruiting API: "Retrieving open positions" XML feed |
Only published job data is read. No personal data is collected, and nothing is ever posted to these APIs. In enrichment mode, company websites are read politely: robots.txt is checked for every page, at most 3 pages per company, with an identifying user agent.
Empty input
With no companies and no dataset, the Actor runs a small read-only demo (Airbnb on Greenhouse, Spotify on Lever, Ramp on Ashby; 5 jobs each, tracking off) so a test run always returns data.
Limits
- Website discovery only finds boards linked or embedded in the HTML. Pages that build their jobs list with JavaScript from an unknown source, and Workday/iCIMS/Taleo/SuccessFactors, report
ATS_NOT_DETECTED. - The ghost-job score is a heuristic from public posting data and our own history. Treat
highas "check before applying or reaching out", not as proof. - Lever and Ashby APIs don't return a company name.
companyfalls back to the token unless you add|Name. - SmartRecruiters returns an empty list (not 404) for unknown company IDs, and IDs are case-sensitive. Those entries show
status: "empty"with a hint. - SmartRecruiters descriptions need one extra request per output job. Large SmartRecruiters boards (5,000+ jobs) take ~50 requests to list.
- Custom careers pages that load jobs with JavaScript only, or that use other systems (Workday, iCIMS, Taleo, SuccessFactors…), can't be auto-detected. Pass the ATS token instead.
- Personio and Workable don't expose an updated date; Lever, Ashby, SmartRecruiters, Workable and Personio expose posted (or created) dates.
- Description salary parsing only accepts clear currency-marked ranges. It's a hint, so check
salary.source.