Use this repository to run the Lever Jobs Scraper from Node.js. It is a single ES module wrapped around the official apify-client package. Your service lists Lever employers by token (plaid, leverdemo) or by board URL, Apify executes the Actor in the cloud, and the response is an array of plain posting objects with stable IDs, separate team and department labels, a normalised workplace type, the commitment string, both the job page and apply URLs, and the requirement sections. Drop it into an Express handler, a queue consumer or a scheduled function and you have Lever coverage without writing a client for each board.
Actor: Lever Jobs Scraper Β· Publisher: Devil Scrapes Β· Also in Python: lever-jobs-scraper-python
- Path-based board names understood: Lever puts the employer in the URL path, as in
jobs.lever.co/plaid, and the Actor accepts that bare token, the board link, a single posting link or theapi.lever.cofeed address. - Whole board in one request per employer: Lever returns every open posting at once, so there are no pages to walk and
maxItemssimply trims the list afterwards. - Team and department kept as two columns: Lever tracks both, and neither is used to fill the other when one is missing.
- Optional exact-match filters on team, department, location and commitment are sent to Lever with the request and apply to every employer in the run.
- Structured requirement blocks: each posting's
listsarray keeps Lever's section headings with the bullet text cleaned of HTML, beside a plain-text description. - Unknown tokens are skipped, not fatal: a token Lever does not recognise is counted in the final status message while the remaining employers finish.
- Short-lived errors are retried with increasing delays before an employer is marked as failed.
Keep a list of Lever employers in your database, run the Actor nightly and upsert by posting_id. Link candidates to apply_url so applications still reach the employer's own form.
Diff today's IDs against yesterday's snapshot and POST each new posting to Slack, Discord or a CRM endpoint. A sales team can treat a burst of new openings at an account as a reason to reach out.
Call the Actor from a Vercel or Cloud Functions handler with the department filter set, cache the result for an hour and return it as JSON to your dashboard.
Map title, location, workplace_type and hosted_url into your internal job schema. Lever's separate team and department fields can feed two facets in your search UI.
You need an Apify account and an API token from Console β Settings β Integrations. Apify's free plan includes monthly usage credit; this Actor's event prices are listed under Pricing below.
git clone https://github.com/devilscrapes/lever-jobs-scraper-javascript.git
cd lever-jobs-scraper-javascript
npm install
export APIFY_TOKEN=your_token_here
node index.js # uses input.json
node index.js my-input.json --out data --max-charge 2The script starts the Lever Jobs Scraper, waits for it to finish, prints a link to the run in Apify Console and saves every result to results.json and results.csv. It exits with code 1 and the run's status message if the run fails.
Without argument parsing and file output, the integration reduces to the five statements below. call() waits for the run to end; listItems() fetches the postings from its default dataset.
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('DevilScrapes/lever-jobs-scraper').call({"companies": ["leverdemo", "plaid"], "maxItems": 5, "proxyConfiguration": {"useApifyProxy": true}});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);index.js forwards input.json exactly as written. The example asks for two Lever boards with a ceiling of 5 postings each, so a first run returns no more than 10 objects. Entries in companies may be bare tokens or links such as https://jobs.lever.co/plaid; anything that looks like a URL but is not a Lever address is rejected before the run starts.
{
"companies": [
"leverdemo",
"plaid"
],
"maxItems": 5,
"proxyConfiguration": {
"useApifyProxy": true
}
}| Field | Type | Required | Default | Allowed values |
|---|---|---|---|---|
companies |
array | yes | ["leverdemo", "plaid"] |
β |
team |
string | no | β | β |
department |
string | no | β | β |
location |
string | no | β | β |
commitment |
string | no | β | β |
maxItems |
integer | no | 200 |
β |
proxyConfiguration |
object | no | {"useApifyProxy": true} |
β |
You receive one object per posting. workplace_type is remote, hybrid, on-site or null, while commitment is whatever text the employer chose, for example Full-time. lists holds the employer's own sections as heading and text pairs, handy for rendering a job detail page.
| Field | Type | Always present |
|---|---|---|
posting_id |
string | yes |
company |
string | yes |
title |
string | yes |
team |
string or null | no |
department |
string or null | no |
location |
string or null | no |
workplace_type |
string or null | no |
commitment |
string or null | no |
hosted_url |
string | yes |
apply_url |
string | yes |
created_at |
string | yes |
description |
string | yes |
lists |
array | yes |
scraped_at |
string | yes |
| Event | Price (USD) |
|---|---|
| Actor start | $0.2 |
| Result emitted | $0.0015 |
Prices as configured on 2026-08-20. The Actor page always shows the current price. Runs are billed to the Apify account that owns the token. Use --max-charge to cap what a single run can cost.
This module compares the latest results.json with a saved snapshot.json, prints the postings that appeared since last time with their section headings, and counts the ones that vanished. It uses only Node.js built-ins; if you set maxItems below a board's size, raise it first so trimmed postings are not mistaken for closed ones.
import { readFile, writeFile } from 'node:fs/promises';
const current = JSON.parse(await readFile('results.json', 'utf8'));
let previous = [];
try {
previous = JSON.parse(await readFile('snapshot.json', 'utf8'));
} catch {
// first run: every posting counts as new
}
const known = new Set(previous.map((job) => job.posting_id));
const live = new Set(current.map((job) => job.posting_id));
const opened = current.filter((job) => !known.has(job.posting_id));
const closed = previous.filter((job) => !live.has(job.posting_id));
const digest = opened.map((job) => ({
company: job.company,
title: job.title,
workplace: job.workplace_type ?? 'unspecified',
sections: job.lists.map((block) => block.text).join(' / '),
url: job.hosted_url,
}));
console.table(digest);
console.log(`${opened.length} opened, ${closed.length} closed since the last snapshot`);
await writeFile('snapshot.json', JSON.stringify(current));No Lever credential is involved. The Actor reads the postings feed that Lever's hosted career pages are built on, so the only secret your service holds is the Apify token.
Yes. apify-client includes type declarations. Define a LeverPosting interface from the Output table, including lists: { text: string; content: string }[], and cast the items to it.
No. The run ends successfully, and its status message reports how many companies returned postings and how many had tokens Lever did not recognise. Check that message in your job if you need to alert on typos.
Lower maxItems, which applies per employer, and set department or team when you only need part of a board. While testing, run node index.js --max-charge 0.5 to cap the cost of a single run.
Set APIFY_TOKEN first: export the token in the same shell, or put it in a local.envfile your shell loads. Never commit it.- Run finished with
FAILEDorABORTED: open the printed Console link; the run log says why. Input validation errors name the field. - Fewer rows than expected: a
--max-chargecap also limits how many results can be charged. Raise the cap or lower the limits in your input. - Something looks wrong in the data: open an issue on the Actor page so the maintainers see it.
- Lever Jobs Scraper on Apify Store
- Python version of this example
- All Devil Scrapes Actors
- Apify web scraping cookbook
Related Actors:
- Ashby Jobs Scraper β postings from employers whose careers pages run on Ashby.
- Workable Jobs Scraper β openings from Workable-hosted company boards.
- SmartRecruiters Jobs Scraper β jobs from SmartRecruiters career sites, filterable by city or country.
MIT licensed example code. The Actor itself runs on Apify and is maintained by Devil Scrapes. Not affiliated with Lever.