ATS Jobs API: Greenhouse, Lever, Ashby & More avatar

ATS Jobs API: Greenhouse, Lever, Ashby & More

Pricing

from $1.50 / 1,000 jobs

Go to Apify Store
ATS Jobs API: Greenhouse, Lever, Ashby & More

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

Dano Desk

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Categories

Security

Limited permissions

Share

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 / net counts 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:

FormExample
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:tokengreenhouse:stripe, lever:eu/acme, ashby:ramp, smartrecruiters:BoschGroup, workable:skroutz, recruitee:bunq, personio:ottonova
Bare token (probed across all 7)stripe
Custom careers pagehttps://careers.acme.com/jobs: fetched once (robots.txt respected) to find a Greenhouse, Lever, Ashby or other embed
Name overridelever: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" }
]
SignalPointsBased on
aging_posting / old_posting / very_old_posting8 (45 d), 15 (60 d), 25 (90 d), 35 (180 d)Earlier of the ATS posted date and our own first-seen date
not_updated10ATS updated date ≥ 90 days ago on a ≥ 90-day-old job
date_refreshed15The ATS posted date moved ≥ 7 days after we first saw the job (needs tracking)
reposted20A new job id with the same title + location as a job that closed on this board in the last 120 days (needs tracking)
duplicate_postings83+ open postings with the exact same title + location
evergreen_title / evergreen_language30 / 15Talent pool, general/open application, future openings, "always hiring" wording
no_pay_range_in_pay_transparency_state10No 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_description5Description under 300 characters
board_stagnant10No 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 /careers and /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 OUTPUT and 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 a closed item and is removed from state.
  • State is per board in the named KV store (ats-jobs-tracker by 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

ATSEndpointDocumentation
Greenhouseboards-api.greenhouse.io/v1/boards/{token}/jobs?content=trueJob Board API: "Job Board data is publicly available, so authentication is not required for any GET endpoints."
Leverapi.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."
Ashbyapi.ashbyhq.com/posting-api/job-board/{name}?includeCompensation=truePublic Job Posting API
SmartRecruitersapi.smartrecruiters.com/v1/companies/{id}/postings (+ /postings/{id} for descriptions)Posting API, listed under "No authentication… public data"
Workableapply.workable.com/api/v1/widget/accounts/{subdomain}?details=truePublic 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/xmlRecruiting 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 high as "check before applying or reaching out", not as proof.
  • Lever and Ashby APIs don't return a company name. company falls 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.