Skip to content

About

Lever jobs scraper for Node.js: turn jobs.lever.co boards into JSON postings with team, department, workplace type, commitment and apply links for your backend, cron worker or webhook. JavaScript example for the Devil Scrapes Apify Actor.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

1 Commit

Folders and files

Repository files navigation

Lever Jobs Scraper β€” JavaScript example

Lever Jobs Scraper for JavaScript and Node.js: jobs.lever.co boards as JSON for your app

Run on Apify JavaScript MIT license CI

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

πŸ”₯ What you get

  • 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 the api.lever.co feed address.
  • Whole board in one request per employer: Lever returns every open posting at once, so there are no pages to walk and maxItems simply 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 lists array 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.

🎯 Use cases

Power a niche job board

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.

Fire webhooks on new openings

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.

Serve hiring data from a serverless function

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.

Normalise Lever next to other ATS feeds

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.

⚑ Quick start

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 2

The 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.

🧩 The core call

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);

πŸ“₯ Input

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} β€”

πŸ“€ Output

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

πŸ’° Pricing

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.

πŸ› οΈ Working with the data in JavaScript

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));

❓ FAQ

Does Lever require an API key for this?

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.

Can I use it from TypeScript?

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.

Will the run fail if one employer is misspelled?

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.

How do I limit spend inside a backend?

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.

🧯 Troubleshooting

  • Set APIFY_TOKEN first: export the token in the same shell, or put it in a local .env file your shell loads. Never commit it.
  • Run finished with FAILED or ABORTED: open the printed Console link; the run log says why. Input validation errors name the field.
  • Fewer rows than expected: a --max-charge cap 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.

πŸ”— More from Devil Scrapes

Related Actors:


MIT licensed example code. The Actor itself runs on Apify and is maintained by Devil Scrapes. Not affiliated with Lever.

About

Lever jobs scraper for Node.js: turn jobs.lever.co boards into JSON postings with team, department, workplace type, commitment and apply links for your backend, cron worker or webhook. JavaScript example for the Devil Scrapes Apify Actor.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages