ATS Jobs Hiring Signals: Greenhouse, Lever & Ashby API
Pricing
from $1.00 / 1,000 job returneds
ATS Jobs Hiring Signals: Greenhouse, Lever & Ashby API
ATS jobs from Greenhouse, Lever and Ashby public APIs: salaries, remote flag, hiring signals and new-job alerts as clean JSON. No login.
Pricing
from $1.00 / 1,000 job returneds
Rating
0.0
(0)
Developer
Derek VanLaningham
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
12 days ago
Last modified
Categories
Share
Get every open job from any company on Greenhouse, Lever or Ashby: clean JSON, salaries, hiring signals, no login.
Turn a list of companies into every open job they're hiring for, in one run. This Actor reads the official public job-board APIs of Greenhouse, Lever and Ashby, so it's fast, stable and needs no login or proxies. Paste board URLs or just slugs (stripe, ramp) and get clean, typed JSON: title, department, locations, remote/hybrid, salary range (normalized to annual), posting date and apply link. Add a per-company hiring-signal summary (open roles, new in the last 7/30 days, top departments) or turn on monitor mode to get only new and removed jobs on a schedule.
Ready-to-run examples
Each example below is a published task: open its page and click Try to copy it into your account and run it, or paste the JSON into the Input tab (JSON view). Times, rows and costs are from real runs (FREE-tier prices; hiring summaries Oct 3 on 1.0.10, Figma and remote data jobs Oct 5 on 1.0.14); results change as the source data changes.
Figma Engineering Jobs with Salary Ranges (Greenhouse API)
Open this example: 3 s, 11 rows, $0.0185. All 11 rows had a salary range.
{"companies":["figma"],"ats":["greenhouse"],"keywords":["engineer"],"descriptionFormat":"none","maxTotalJobs":11}
Output excerpt (one row, selected fields):
{"title": "Data Platform Engineer","companyName": "Figma","department": "Engineering","locations": ["San Francisco, CA • New York, NY • United States","Canada","US"],"salary": {"min": 153000,"max": 376000,"currency": "USD","interval": "year","minAnnual": 153000,"maxAnnual": 376000,"summary": "Annual Base Salary Range:","source": "greenhouse"},"postedAt": "2026-09-21T16:13:46.000Z","applyUrl": "https://boards.greenhouse.io/figma/jobs/6201407004?gh_jid=6201407004"}
Hiring Signals: Stripe (Greenhouse), Palantir (Lever), Ramp
Open this example: 3 s, 3 rows, $0.017.
{"companies":["https://boards.greenhouse.io/stripe","https://jobs.lever.co/palantir","https://jobs.ashbyhq.com/ramp"],"outputMode":"summary_only"}
Output excerpt (one row, selected fields):
{"recordType": "company_summary","companyName": "Ramp","ats": "ashby","openJobs": 157,"newJobsLast30Days": 54,"remoteShare": 0.09,"salaryDisclosedShare": 0.96,"medianSalaryMinAnnualUSD": 168000,"topDepartments": [{"name": "Sales","count": 62},{"name": "Engineering","count": 38},{"name": "People & Talent","count": 13},{"name": "Design","count": 7},{"name": "Product","count": 7}]}
Remote Data Jobs at Stripe, Ramp, Figma, Notion & Databricks
Open this example: 5 s, 11 rows, $0.0185. Uses "remoteMatch": "eligible": jobs where remote is at least one listed option (remoteEligible). The default strict match returned 2 rows.
{"companies":["stripe","ramp","figma","notion","databricks","plaid"],"keywords":["data"],"remoteOnly":true,"remoteMatch":"eligible","descriptionFormat":"none","maxTotalJobs":11}
Output excerpt (one row, selected fields):
{"title": "Specialist Solutions Architect, Data","companyName": "Stripe","ats": "greenhouse","primaryLocation": "SF, NY, Remote","workplaceType": null,"remoteEligible": true,"postedAt": "2026-08-27T00:33:39.000Z","applyUrl": "https://stripe.com/jobs/search?gh_jid=8144262"}
What you get
One dataset row per job with: title, companyName, department, team, employmentType, workplaceType (remote / hybrid / onsite), locations, country, salary (min, max, currency, interval, annualized min/max), postedAt, updatedAt, jobUrl, applyUrl, description (text / markdown / html / none) and stable IDs (jobKey).
{"recordType": "job","jobKey": "ashby:ramp:34413f8d-26bf-4bbc-8ade-eb309a0e2245","ats": "ashby","boardSlug": "ramp","companyName": "Ramp","companyNameSource": "slug","atsJobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245","title": "Security Engineer, Cloud","department": "Engineering","team": "Backend","employmentType": "full_time","workplaceType": "hybrid","locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)"],"primaryLocation": "New York, NY (HQ)","country": "USA","salary": { "min": 211400, "max": 290600, "currency": "USD", "interval": "year", "minAnnual": 211400, "maxAnnual": 290600, "summary": "$211.4K – $290.6K • Offers Equity", "source": "ashby" },"postedAt": "2026-04-07T17:12:35.753Z","updatedAt": null,"jobUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245","applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application","description": "About Ramp ...","descriptionTruncated": false,"language": null,"sourceApiUrl": "https://api.ashbyhq.com/posting-api/job-board/ramp?includeCompensation=true","scrapedAt": "2026-10-03T15:30:00.000Z","isNew": null}
Why this Actor
- Official public APIs: the same JSON endpoints companies use to embed their job boards. Stable, ToS-friendly, no bot-fighting, no login, no proxy needed.
- Three ATSs in one call: Greenhouse, Lever (incl. EU) and Ashby, normalized into one schema.
- Auto-detects the slug: pass
stripeand it checks all three ATSs in parallel (keeps every board that exists). - Salaries normalized: min/max, currency, interval, plus
minAnnual/maxAnnual(never currency-converted). - Hiring signals: one summary row per company (open jobs, new in 7/30 days, remote share, top departments/locations, salary disclosure, median USD salary).
- Monitor mode: only new jobs since the last run, plus
job_removedrows for jobs that disappeared. - Cheap per-job pricing, and MCP-ready for AI agents (stable camelCase keys, every key always present,
nullwhen unknown).
Use cases
- Sales / GTM: hiring = buying intent. "Which of my 500 target accounts are hiring data engineers this month?"
- Recruiting / sourcing: track openings at target companies.
- Job boards and aggregators: bulk, normalized feeds.
- Market and comp research: salary ranges, remote share, department mix.
- AI agents: "Is X hiring for Y?" in one tool call.
How to use the Greenhouse, Lever and Ashby jobs API
- Add companies. Paste job-board URLs (
https://boards.greenhouse.io/stripe,https://jobs.lever.co/palantir,https://jobs.ashbyhq.com/ramp) or just slugs (stripe,ramp) into Companies. Slugs are checked on Greenhouse, Lever and Ashby automatically (limit this with ATS). - Filter (optional). Use Keywords (e.g.
engineer,data), Departments, Location contains, Employment types, Remote only and Posted within days. - Pick the output.
jobsreturns one row per job, with salary ranges converted to annual figures where the company publishes pay.summary_onlyreturns one hiring-signal row per company (open jobs, new jobs in the last 7 and 30 days, remote share, top departments and locations, salary disclosure).jobs_and_summaryreturns both. - Run it. One request per job board, so most runs finish in seconds. Example: 1,000 SpaceX jobs in 5.4 s.
- Monitor (optional). Turn on Only new jobs with a Monitor ID and schedule the Actor daily to get only jobs you haven't seen.
- Use it from AI agents. The Actor works as a tool through Apify's MCP server (
mcp.apify.com) for Claude, Cursor and other MCP clients.
You pay per result: $1.50 per 1,000 jobs on the Free plan (less on paid plans), plus $0.002 per run start. Data comes only from the companies' public job-board APIs. No login or proxy is needed.
Example inputs
Single company
{ "companies": ["https://boards.greenhouse.io/stripe"] }
Bulk list, small payload
{ "companies": ["stripe", "ramp", "palantir", "figma", "notion"], "descriptionFormat": "none", "outputMode": "jobs_and_summary" }
Filtered remote engineering
{ "companies": ["ramp", "figma"], "keywords": ["engineer"], "excludeKeywords": ["intern"], "remoteOnly": true, "postedWithinDays": 30 }
Monitor mode (schedule daily)
{ "companies": ["stripe", "ramp", "palantir"], "onlyNewJobs": true, "monitorId": "my-accounts", "descriptionFormat": "none" }
Create a schedule for this input. The first run outputs every job as new (that's expected); later runs output only new jobs and job_removed rows.
Input reference
| Field | Type | Default | Description |
|---|---|---|---|
companies | string[] (required) | – | ATS board URLs, ATS API URLs or bare slugs. Max 1000. |
ats | string[] | all three | Which ATSs to try for bare slugs (greenhouse, lever, ashby). URLs always use their own ATS. For a bare slug, Lever is tried on api.lever.co first and then on the EU host api.eu.lever.co. The EU fallback is only tried for bare slugs. A jobs.lever.co/... URL is looked up only on the global host, so paste the jobs.eu.lever.co/... URL for an EU board. |
outputMode | jobs | jobs_and_summary | summary_only | jobs | Job rows, job rows + one summary per company, or summaries only. Summary stats cover the whole board, except matchingJobs, which counts only the jobs that pass your filters. |
keywords | string[] | [] | Keep jobs whose title contains any of these (case-insensitive). With keywordScope: "title_and_description", the description is searched too. |
keywordScope | title | title_and_description | title | Where keywords match. title_and_description searches the full job description as well, even with descriptionFormat: "none" (no extra cost). excludeKeywords always match the title only. |
excludeKeywords | string[] | [] | Drop jobs whose title contains any of these. |
locationContains | string[] | [] | Keep jobs where any location contains one of these. |
departments | string[] | [] | Keep jobs whose department or team contains any of these (case-insensitive), e.g. Engineering. Jobs with neither field are dropped when set. |
employmentTypes | string[] | [] | Keep only these normalized employmentType values: full_time, part_time, contract, intern, temporary, other. Jobs with an unknown type are kept too (see next row). Listing unknown always keeps them. |
includeUnknownEmploymentType | boolean | true | Only used with employmentTypes. true also keeps jobs whose ATS publishes no employment type (employmentType: null, e.g. most Greenhouse boards). false drops them, unless unknown is listed. |
remoteOnly | boolean | false | Keep only remote jobs (see remoteMatch). |
remoteMatch | strict | eligible | strict | With remoteOnly: strict keeps workplaceType == "remote"; eligible keeps remoteEligible == true. |
postedWithinDays | integer | 0 | Only jobs first published within N days (0 = no limit; undated jobs are kept only when 0). |
descriptionFormat | none | text | markdown | html | text | Description format. none is smallest. |
maxDescriptionChars | integer | 0 | Truncate descriptions (sets descriptionTruncated). |
maxJobsPerCompany | integer | 0 | Cap job rows per company after filters. In monitor mode, new jobs cut off by this cap stay "new" and are output by the next run. |
maxTotalJobs | integer | 0 | Global cap on job rows (cost control). In monitor mode, new jobs cut off by this cap stay "new" and are output by the next run. |
onlyNewJobs | boolean | false | Monitor mode: output only jobs not seen in earlier runs with the same monitorId, plus free job_removed rows. With outputMode: "summary_only", the seen-jobs list is not updated. |
monitorId | string | default | Monitor state name (letters, digits, -). |
proxyConfiguration | proxy | off | Not needed for these official APIs. |
Output reference
Every row has recordType. Filter on it first. All keys of a record type are always present.
job
| Field | Type | Notes |
|---|---|---|
| jobKey | string | {ats}:{boardSlug}:{atsJobId}, globally unique |
| ats | greenhouse|lever|ashby | |
| boardSlug | string | lower-case |
| companyName / companyNameSource | string|null / ats|slug | Greenhouse provides the name; Lever/Ashby use the title-cased slug |
| atsJobId, title | string | |
| department, team | string|null | |
| employmentType | full_time|part_time|contract|intern|temporary|other|null | Greenhouse: always null (not standardized) |
| workplaceType | remote|hybrid|onsite|null | Lever/Ashby: from the board's workplace field. Greenhouse has no such field: remote only when every location in the name says remote (US-Remote, Remote US); office lists that also mention remote (SF, NYC, Remote) and all other jobs are null |
| remoteEligible | boolean | true when the workplace field says remote or any listed location mentions remote |
| locations / primaryLocation | string[] / string|null | |
| country | string|null | Lever ISO-2, Ashby as given, Greenhouse null |
| salary | object|null | {min, max, currency, interval, minAnnual, maxAnnual, summary, source} |
| postedAt / updatedAt | ISO 8601 UTC|null | |
| jobUrl / applyUrl | string / string|null | |
| description / descriptionTruncated | string|null / boolean | per descriptionFormat |
| language | string|null | Greenhouse only |
| sourceApiUrl, scrapedAt | string | |
| isNew | boolean|null | true in monitor mode, else null |
company_summary: ats, boardSlug, companyName, boardUrl, openJobs (all jobs on the board), matchingJobs (after your filters), newJobsLast7Days, newJobsLast30Days, remoteShare (0–1; Lever/Ashby: over jobs with a known workplace type, null if none; Greenhouse: clearly-remote jobs ÷ all jobs, since Greenhouse has no workplace field), topDepartments, topLocations (top 5 {name, count}), salaryDisclosedShare (0–1), medianSalaryMinAnnualUSD, scrapedAt. Stats other than matchingJobs are computed over all open jobs on the board.
error (free): input, ats, boardSlug, errorCode (BOARD_NOT_FOUND, NOT_DETECTED, INVALID_INPUT, UPSTREAM_ERROR, RATE_LIMITED), message, scrapedAt. One row per company entry that returned nothing, so you (or your agent) can see why.
job_removed (free, monitor mode): jobKey, ats, boardSlug, title, jobUrl, lastSeenAt, scrapedAt.
Remote: strict vs eligible. workplaceType: "remote" is strict (the source says the job is remote, and on Greenhouse every listed location says remote); remoteEligible: true is loose (remote is at least one option, e.g. SF, NYC, Seattle, Remote). Stripe, Oct 3: 35 strict vs 114 eligible of 715.
Salary notes: Ashby uses the first Salary component of compensation.compensationTiers. Lever uses salaryRange. Greenhouse uses the first of pay_input_ranges[] (min_cents/max_cents ÷ 100, currency_type, title as summary). Greenhouse has no interval field. interval is hour when the range title (or, failing that, the blurb) mentions an hour ("hour", "hourly", "per hour", "/hr"). It is year when it mentions a year ("year", "yearly", "annual", "per annum"). Otherwise it is null, and then minAnnual/maxAnnual are null too. Annualization: year ×1, month ×12, week ×52, day ×260, hour ×2080. Example from Figma: title "Annual Base Salary Range:" with min_cents 16500000 gives {min: 165000, max: 190000, currency: "USD", interval: "year", minAnnual: 165000, maxAnnual: 190000}. Greenhouse only returns pay ranges when the request includes pay_transparency=true. The Actor always sends it on the same single request (no extra calls). Currencies are never converted.
Pricing
Pay per event: you only pay for what you get. No platform usage charges.
| Event | Price (Free tier) | When |
|---|---|---|
job | $0.0015 | per job row |
company-summary | $0.005 | per company summary row |
company-checked | $0.0005 | per board checked, monitor mode only |
apify-actor-start | $0.002 | once per run (charged by Apify) |
Higher Apify plans get lower per-job prices (down to $0.0010 per job). error and job_removed rows are free.
Worked examples: 1,000 jobs ≈ $1.50 on the Free tier. Checking 100 companies daily in monitor mode ≈ $0.05/day + $0.0015 per new job. "Is X hiring?" for 50 companies with summary_only ≈ $0.25.
Set Max cost per run to cap spending. The Actor stops cleanly when it's reached ("Stopped: your max-cost-per-run limit was reached after N jobs") and you keep everything delivered so far.
Use as an MCP tool / with AI agents
Add it to any MCP client (Claude, Cursor, …) via the Apify MCP server:
https://mcp.apify.com/?tools=OptimistPrime/ats-jobs-hiring-signals
Example tool call:
{ "companies": ["ramp"], "descriptionFormat": "none" }
Tips for agents:
- Use
descriptionFormat: "none"(ormaxDescriptionChars: 1000) for large lists to keep payloads small. - Use
outputMode: "summary_only"for "is company X hiring?" questions (one row per company). - Use
maxTotalJobsto bound cost and response size. - Check
recordType: "error"rows to learn why a company returned nothing (e.g. not on these ATSs).
Integrations
API (JavaScript)
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('OptimistPrime/ats-jobs-hiring-signals').call({ companies: ['stripe', 'ramp'] });const { items } = await client.dataset(run.defaultDatasetId).listItems();
API (Python)
from apify_client import ApifyClientclient = ApifyClient(os.environ["APIFY_TOKEN"])run = client.actor("OptimistPrime/ats-jobs-hiring-signals").call(run_input={"companies": ["stripe", "ramp"]})items = client.dataset(run["defaultDatasetId"]).list_items().items
- Schedules + webhooks: run monitor mode daily and POST new jobs to your CRM or Slack.
- Make, Zapier, n8n: use the Apify app/node, "Run Actor" + "Get dataset items".
- Google Sheets: export the
overviewview as CSV, or use the Google Sheets integration.
Limits and FAQ
- How is this different from other Greenhouse, Lever or Ashby scrapers on Apify? Other Greenhouse/Lever/Ashby Actors on the Store cost between $0.0015 and $0.004 per job and are reliable. Choose this one if you want all three ATSs in one run, normalized salaries, per-company hiring-signal summaries, or new-jobs-only monitoring.
- Which companies are covered? Only companies whose job board is hosted on Greenhouse, Lever or Ashby. Companies on Workday, iCIMS, SmartRecruiters, Workable etc. are not covered in v1.
- How do I find a company's slug? Open their careers page and click a job. If the URL is
boards.greenhouse.io/<slug>,job-boards.greenhouse.io/<slug>,jobs.lever.co/<slug>orjobs.ashbyhq.com/<slug>, paste that URL. Company career-site URLs (e.g.stripe.com/jobs) return aNOT_DETECTEDrow in v1. - Do summary stats respect my filters? Only
matchingJobsdoes. It counts the jobs that passkeywords(withkeywordScope),excludeKeywords,locationContains,departments,employmentTypes,remoteOnlyandpostedWithinDays.openJobs,newJobsLast7Days/newJobsLast30Days,remoteShare,topDepartments,topLocations,salaryDisclosedShareandmedianSalaryMinAnnualUSDdescribe the whole board. - Am I charged for jobs my filters drop? No. The
jobevent is charged only for job rows that reach your dataset after all filters and caps. Filtered-out jobs are free. employmentTypesand Greenhouse: Lever and Ashby publish an employment type for each job. Greenhouse has no standard field; it is filled only when a board publishes it as custom metadata (e.g. "Time Type: Full Time"), otherwiseemploymentTypeisnull. By default these unknown-type jobs are kept when you filter byemploymentTypes, so Greenhouse boards don't silently disappear. SetincludeUnknownEmploymentType: falsefor a strict filter.- Does
excludeKeywordssearch descriptions? No, it always matches the title only, also withkeywordScope: "title_and_description". departmentsmatching: a case-insensitive substring ofdepartmentorteam, soEngineeringalso matches "Platform Engineering". Greenhouse boards sometimes prefix departments with cost-centre numbers (e.g. "7112 Data Science"); substring matching handles that. Greenhouse only returns departments and offices together with job content, so the Actor always requests it (still one request per board, no extra charge). WithdescriptionFormat: "none"the description HTML is dropped right after download and never output.- Keywords in descriptions: with
keywordScope: "title_and_description", a job matches if any keyword is in the title or the full description text. Matching uses the full text even when the output description isnone, HTML or truncated. On Greenhouse this fetches the job content in the same single request, so runs move a little more data but cost the same. - Monitor mode +
maxJobsPerCompany/maxTotalJobs: only jobs that were actually delivered (and charged) are marked as seen. New jobs that a cap cut off stay "new", so the next run outputs them. Nothing is skipped, and nothing is charged twice. - Monitor mode +
summary_only: a summaries-only run delivers no job rows, so it doesn't touch the seen-jobs list. It marks nothing as seen and emits nojob_removedrows. To advance monitoring, run a full jobs pass (outputModejobsorjobs_and_summary) with the samemonitorId. - EU Lever boards: the EU host (
api.eu.lever.co) is only tried as a fallback for bare slugs. For a Lever URL, the URL's own host is used. Pastehttps://jobs.eu.lever.co/<slug>for EU boards. - First monitor run outputs everything. That's how monitoring starts; later runs only output changes. Jobs are only marked removed for boards fetched successfully in that run.
- Data freshness: live; each run reads the boards at run time.
- Rate limits: requests are throttled to 2/s per API host with retries; you shouldn't need a proxy.
Legal / ethical note
This Actor reads public job postings from official job-board APIs that companies publish for embedding. It collects no personal data and uses no login. You are responsible for how you use the data downstream. Not affiliated with, or endorsed by, Greenhouse, Lever or Ashby.
Support / changelog
Open an issue in the Issues tab; we aim to reply within 24 hours. See ./CHANGELOG.md.