Job Postings API: Greenhouse, Lever, Ashby, SmartRecruiters avatar

Job Postings API: Greenhouse, Lever, Ashby, SmartRecruiters

Pricing

from $1.20 / 1,000 jobs

Go to Apify Store
Job Postings API: Greenhouse, Lever, Ashby, SmartRecruiters

Job Postings API: Greenhouse, Lever, Ashby, SmartRecruiters

One feed of open jobs from company career pages on Greenhouse, Lever, Ashby and SmartRecruiters. Give company names or careers-page URLs, filter by keyword, location, remote, department and date, and get only new jobs each run. Salary where shown. Free hiring-signal row per company. Pay per job.

Pricing

from $1.20 / 1,000 jobs

Rating

0.0

(0)

Developer

Solid Fetch

Solid Fetch

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

18 hours ago

Last modified

Share

Job Postings API: Greenhouse, Lever, Ashby and SmartRecruiters Jobs Feed

Get the open jobs of any company that publishes its jobs on Greenhouse, Lever, Ashby or SmartRecruiters, in one schema, as an API or a scraper alternative. Give a list of company names or careers-page URLs and the actor works out the vendor from each address. Filter by keyword, location, remote, department and posting date, or switch on the monitor to get only the jobs that are new since your last run. Each job is one row with the title, department, team, locations, remote and workplace type, employment type, salary when the vendor shows it, posting date, job and apply URLs and the description. Every company also gets a free hiring-signal row: open roles, roles by department, remote roles, postings in the last 7 days, and what is new or closed since the last run.

The actor reads only the public job-board APIs that the four vendors offer for publishing jobs. There is no login, no browser, no proxy and no scraping of company websites or of LinkedIn.

What people use it for

  • Hiring signals for sales and recruiting. A company that opens ten engineering roles in a week is growing a team. Run the monitor daily over a list of target accounts and get the new roles, plus a free row per company with its open roles by department.
  • Job boards and alerts. Feed a niche job board or a Slack channel with fresh remote roles from the companies you care about. Each run returns only what is new, so the feed has no duplicates.
  • Market and salary research. Collect open roles with their salary ranges (Ashby, Lever and Greenhouse show them when the company turns pay transparency on) across many companies in one table, without learning four different APIs.
  • Competitor watch. Follow which teams a competitor is hiring for and where.

How to use it

Read three companies on three vendors. The vendor follows from the address, or from the prefix of a board id:

{
"companies": [
"https://boards.greenhouse.io/airbnb",
"https://jobs.lever.co/zoox",
"ashby:ramp",
"https://careers.smartrecruiters.com/Canva"
],
"maxItems": 500
}

Remote engineering jobs posted in the last two weeks, in Europe or the US:

{
"companies": ["greenhouse:stripe", "lever:spotify", "ashby:notion"],
"keywords": ["engineer", "developer"],
"excludeKeywords": ["intern"],
"remoteOnly": true,
"locations": ["Europe", "United States", "Poland", "Germany"],
"postedSince": "2 weeks",
"includeDescription": "none"
}

The monitor, scheduled daily. The first run records what each company has open and returns no jobs. From the next run on, you get only the new ones:

{
"companies": ["greenhouse:airbnb", "lever:zoox", "ashby:ramp"],
"departments": ["Engineering", "Product"],
"onlyNew": true
}

A field name the actor does not know stops the run with a hint such as "keyword" (did you mean "keywords"?), so a typo cannot turn into a run of unfiltered jobs that you pay for.

Input

FieldWhat it does
companiesThe companies to read, up to 2,000. Each entry is a careers-page address (boards.greenhouse.io/stripe, job-boards.greenhouse.io/stripe, jobs.lever.co/spotify, jobs.eu.lever.co/acme, jobs.ashbyhq.com/ramp, careers.smartrecruiters.com/Canva, an address with a job id in it works too), a board id with its vendor (greenhouse:stripe, lever:spotify, ashby:ramp, smartrecruiters:Canva, short forms gh and sr work), or a bare id such as stripe. A start-URL object with a url is also accepted.
atsWhich vendor to read for bare ids. auto (default) reads Greenhouse, Lever, Ashby and SmartRecruiters and takes the one vendor with open jobs. When more than one vendor has open jobs under the id, nothing is saved or charged for it and it gets a free ambiguous-company error row that names the vendors. Name the vendor if you know it, to save requests and to avoid a same-named board on another vendor.
keywords, excludeKeywordsKeep jobs whose title has any of the keywords, and drop jobs that have any of the excluded ones. Matching ignores case and accents and starts at a word: engineer finds "Engineering Manager", go does not find "Django".
keywordScopetitle (default) or titleAndDescription.
locationsKeep jobs that have any of these in one of their locations or in the country. Locations are free text as the company wrote them ("Remote (US)", "NYC"), so list the variants you need.
departmentsKeep jobs whose department or team contains any of these.
remoteOnlyKeep jobs marked remote, or with a remote location.
postedSinceA date (2026-10-15) or a period back from today (7 days, 2 weeks, 1 month). Jobs with no posting date are dropped.
onlyNewMonitor mode. See "How the monitor works".
emitOnFirstRunWith onlyNew, also return every job of a company the first time the actor sees it.
includeDescriptiontext (default), html or none.
maxItemsStop after this many jobs in the run, up to 1,000,000. Default 1,000.
maxJobsPerCompanySave at most this many jobs, the newest, from each company.
stateStoreNameDefault ats-jobs-feed-state. The key-value store on your account that holds the monitor's memory. Use another name to keep a separate watch, for example per client.

Output

The dataset has three kinds of rows, told apart by rowType: job (charged), summary (one per company, free) and error (one per entry that gave no jobs, free). The dataset views "Jobs", "Hiring signals per company" and "Entries that gave no jobs" show each kind as a table. To keep only jobs in an export, filter on rowType being job.

A job row, built from a recorded Ashby posting:

{
"rowType": "job",
"id": "ashby:ramp:34413f8d-26bf-4bbc-8ade-eb309a0e2245",
"ats": "ashby",
"companyId": "ramp",
"company": "ramp",
"jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
"title": "Security Engineer, Cloud",
"department": "Engineering",
"team": "Backend",
"employmentType": "full-time",
"location": "New York, NY (HQ)",
"locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)", "Miami, FL"],
"country": "USA",
"remote": true,
"workplaceType": "hybrid",
"salaryMin": 211400,
"salaryMax": 290600,
"salaryCurrency": "USD",
"salaryPeriod": "year",
"salaryText": "$211.4K - $290.6K",
"postedAt": "2026-04-07T17:12:35.753Z",
"updatedAt": null,
"url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
"applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
"language": null,
"description": "About Ramp\nRamp is building the smart infrastructure for finance teams...",
"descriptionError": null,
"source": "Ashby public job-board API",
"fetchedAt": "2026-10-22T09:00:00.000Z"
}
FieldMeaning
id, jobIdid is vendor:company:jobId and is unique across vendors. jobId is the vendor's own id.
ats, companyId, companyThe vendor (greenhouse, lever, ashby, smartrecruiters), the board id you gave, and the company name. Greenhouse and SmartRecruiters send the company name, Lever and Ashby do not, so for those company is the board id.
title, department, teamAs the company wrote them. SmartRecruiters has no team, and its department falls back to the job function (Sales, Engineering) when the company sets none. Greenhouse has no team.
employmentTypefull-time, part-time, contract, internship, temporary or other, read from the vendor's label. null when the vendor sends none (Greenhouse sends it only on some boards).
location, locations, countryThe main location, all locations of the job, and the country where the vendor gives one (Lever: two-letter code. Ashby: as written, "USA" or "United States". SmartRecruiters: two-letter code. Greenhouse: not given, null).
remote, workplaceTyperemote is true when the vendor marks the job remote or lists a remote location. workplaceType is remote, hybrid or onsite where the vendor says it, null where it does not. Ashby marks a hybrid job remote when it also lists a remote location, so check workplaceType when the difference matters.
salaryMin, salaryMax, salaryCurrency, salaryPeriod, salaryTextThe salary the vendor shows. Greenhouse: the pay range (no period, so salaryPeriod is null, ranges for several locations are merged when they share a currency). Lever: the salary range and its period. Ashby: the salary component and its period, with the vendor's short summary in salaryText. SmartRecruiters has no salary field. null when the company shows no pay. Bonus and equity are not included.
postedAt, updatedAtWhen the job was first published (Greenhouse first_published, Lever createdAt, Ashby publishedAt, SmartRecruiters releasedDate) and, for Greenhouse only, when it was last updated. Lever and Ashby give no update time.
url, applyUrlThe job page and the apply page.
descriptionPlain text, HTML or null, as set by includeDescription. E-mail addresses are replaced with [email removed].
descriptionErrornull on a charged job. When a description was asked for and could not be fetched (SmartRecruiters needs one extra request per job), the job is saved free with description null and the reason here.
languageThe language code where the vendor gives one (Greenhouse, SmartRecruiters).
source, fetchedAtThe vendor API the row comes from, and when it was read.

A summary row, saved free for each company that was read:

{
"rowType": "summary",
"ats": "ashby",
"companyId": "ramp",
"company": "ramp",
"boardUrl": "https://jobs.ashbyhq.com/ramp",
"openRoles": 4,
"postedLast7Days": 0,
"remoteRoles": 4,
"newestPostedAt": "2026-08-12T20:22:32.685Z",
"byDepartment": [
{ "department": "Engineering", "openRoles": 3 },
{ "department": "Risk", "openRoles": 1 }
],
"matchedThisRun": 1,
"savedThisRun": 1,
"newSinceLastRun": null,
"closedSinceLastRun": null,
"baseline": false,
"truncated": false,
"fetchedAt": "2026-10-22T09:00:00.000Z"
}

openRoles counts every posting on the company's board, whatever your filters. matchedThisRun counts what passed the filters (in monitor mode, among the new postings only) and savedThisRun what was saved and charged. newSinceLastRun and closedSinceLastRun are filled in monitor mode only. baseline is true on the run that recorded the starting point. truncated is true when a SmartRecruiters board has more than 10,000 postings, which is the most the actor reads for one company.

An error row, saved free for each entry that gave no jobs:

{
"rowType": "error",
"input": "ashby:nope",
"ats": "ashby",
"companyId": "nope",
"company": null,
"outcome": "not found",
"error": "Ashby has no job board \"nope\"",
"fetchedAt": "2026-10-22T09:00:00.000Z"
}

outcome is invalid (not a job-board address or id, for example a company's own careers site), not found (the vendor has no such board), ambiguous-company (a bare id with open jobs on more than one vendor: give vendor:id or the careers-page URL) or failed (the vendor did not answer after retries, or its format changed). A company with a board and no open roles gets a normal summary row with openRoles 0.

The run also saves a SUMMARY record in the key-value store: the companies read with their counts, the entries not found, failed or invalid, the bare ids found on more than one vendor, the companies not read because the run stopped, and how many descriptions could not be fetched.

How the monitor works

With onlyNew on, the actor keeps a record of each company's open jobs in a named key-value store on your Apify account (stateStoreName). A run returns only the jobs that were not in the record, then updates the record.

  • The first run for a company records the starting point and returns no jobs. The status message says so. This is on purpose: adding 200 companies must not return and charge for their whole job lists. Set emitOnFirstRun to get them anyway.
  • Jobs that pass the filters are saved. A new job that fails a filter is still remembered, so it does not come back later as new.
  • A job left out because of maxJobsPerCompany, maxItems or your cost limit stays unseen, and the next run returns it.
  • A job whose description could not be fetched is saved free with descriptionError and stays unseen, so the next run tries again and charges it once it has its description.
  • Each set of filters (keywords, excludeKeywords, keywordScope, locations, departments, remoteOnly) keeps its own record of a board, so two scheduled tasks with different filters on the same company do not hide jobs from each other. Order and case of the values do not matter, and postedSince is not part of it. Changing the filters starts a new record and a new starting point. A watch with no filters uses the same record as earlier versions of the actor.
  • The summary row says how many postings were new and how many closed since the last run.
  • Run it from a schedule (daily is usual). Two runs in a row over the same board return no jobs the second time.
  • Jobs are matched by the vendor's job id. A job that the company deletes and posts again gets a new id and counts as new.
  • If you change stateStoreName, the actor starts a new record and records a new starting point.

Send results to Make, Zapier, n8n, Slack or a CRM

Start the actor or a saved task on a schedule. Trigger the next step on the finished run: in Make the Apify module "Watch Actor Runs", in Zapier the Apify trigger for a finished actor run, in n8n the Apify node. Read the rows with "Get Dataset Items" using the run's dataset ID. For your own endpoint, open the actor's Integrations tab and add an HTTP webhook on "Run succeeded". The payload contains resource.defaultDatasetId, which you can read at https://api.apify.com/v2/datasets/<datasetId>/items?format=json. Add &view=jobs for the job columns only, or &format=csv for a spreadsheet.

Pricing

You pay per job saved (event job), and the price is on the Pricing tab. The summary rows and error rows are free and do not count against maxItems. So is a job saved without the description you asked for (descriptionError set), and a bare id found on more than one vendor is not saved or charged at all. A run that finds no new jobs costs only the start of the run. When a run reaches the maximum cost you set, it stops and keeps what was saved, and it never saves a job the remaining budget cannot pay for. If the platform migrates or restarts a run, the run picks up its own record of what it saved (RUN_STATE in the key-value store) and does not save or charge those jobs again.

The actor calls only the public job-board APIs that the vendors publish for showing a company's own open jobs on its website:

  • Greenhouse: boards-api.greenhouse.io/v1/boards/{board}/jobs
  • Lever: api.lever.co/v0/postings/{company} (and the EU site api.eu.lever.co for a company not found on the global site)
  • Ashby: api.ashbyhq.com/posting-api/job-board/{board}
  • SmartRecruiters: api.smartrecruiters.com/v1/companies/{company}/postings

These need no login or key. The actor sends one request at a time with a pause between requests, retries politely, and honours Retry-After. It does not open company websites, career pages behind a login, or any page that is not one of these APIs, and it reads nothing from LinkedIn, Indeed or other job boards.

A company that runs its own careers site on top of one of these vendors usually still has a board on the vendor. Use the vendor address (for example boards.greenhouse.io/stripe for Stripe's jobs page) and not the company's own address, which the actor cannot read.

Personal data

Job postings are company information. The actor does not output the names or e-mail addresses of recruiters or hiring managers, even where a vendor's API returns them (SmartRecruiters returns a posting creator on some postings, and the actor ignores it). Descriptions sometimes contain a recruiter's e-mail address, and every address is replaced with [email removed]. Names written inside a description text are not detected.

When a run fails, an error report with the error message and the shape of the input (which fields are set, how long the lists are), never your input values or your results, is sent to the developer to fix bugs.

Limits

  • A vendor's API shows what the company chose to publish. A job that is not on the public board is not in the feed. Ashby jobs marked unlisted are left out.
  • Posting dates mean "first published" on all four vendors, and Lever and Ashby give no update time, so an edited job does not move.
  • Salary appears only where the company turned it on. Greenhouse gives no pay period. SmartRecruiters gives no salary at all.
  • A bare id looked up with ats set to auto takes the vendor that has open jobs under that name. If two vendors have jobs under it, nothing is saved for it, and if a vendor does not answer, the entry fails rather than guessing. One vendor with jobs can still be a different company of the same name, so name the vendor to be sure.
  • Greenhouse boards on the EU site (boards.eu.greenhouse.io) are not read yet and get an invalid row.
  • SmartRecruiters answers with an empty list both for a company it does not know and for one with no open jobs, so both give a not found row.
  • SmartRecruiters descriptions need one extra request per saved job, so a run over a big SmartRecruiters board with descriptions is slower. Use includeDescription: "none" for counts and titles.
  • A full run is as fast as the vendors answer. Greenhouse and Lever send a whole board in one response (several MB for the largest boards), and the actor waits a short time between requests.
  • The actor stops with an error row for a company when its board comes back in a format the actor does not read, before saving anything from it, and the other companies continue.

If this actor saved you time, a short review on the actor page helps other people find it.