Here you will find a small Python wrapper, with tests, around the Lever Jobs Scraper that Devil Scrapes runs on Apify. Many companies host their careers page on Lever, and every such board lives at an address like jobs.lever.co/plaid. Pass the last part of that address (or the whole link) for as many employers as you like, and the Actor returns their current openings as rows: title, team, department, location, workplace type, commitment, posting date, plain-text description and the requirement sections. Apify runs the collection in the cloud; your script just saves results.json and results.csv for pandas, a spreadsheet or a scheduled report.
Actor: Lever Jobs Scraper Β· Publisher: Devil Scrapes Β· Also in JavaScript: lever-jobs-scraper-javascript
- 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.
Cross-tabulate department against workplace_type for a list of employers. One table shows whether a target company is growing engineering or sales, and how much of that growth is remote, hybrid or on-site.
Because created_at is an exact timestamp, you can count postings opened in the last 7 or 30 days per company. Run the script weekly from cron and the counts become a time series of hiring momentum.
Explode the lists column, keep blocks whose heading mentions requirements or qualifications, and count keywords such as Python, Kubernetes or SQL. Labour-market researchers get a skills-demand sample from real postings rather than a survey.
Filter to one team or location, sort by newest, and export title plus hosted_url to CSV. A recruiter can work through the list without opening each career site.
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-python.git
cd lever-jobs-scraper-python
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
export APIFY_TOKEN=your_token_here
python main.py # uses input.json
python main.py 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.
Leave out the command-line handling and the integration fits in a few lines. ApifyClient.actor().call() launches the run and returns once it has stopped; iterate_items() then yields one posting dict after another.
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("DevilScrapes/lever-jobs-scraper").call(run_input={'companies': ['leverdemo', 'plaid'], 'maxItems': 5, 'proxyConfiguration': {'useApifyProxy': True}})
for item in client.dataset(run.default_dataset_id).iterate_items():
print(item)main.py sends input.json to the Actor unchanged. The sample reads two boards, Lever's own leverdemo board and plaid, and keeps at most 5 postings from each, so the trial run stays at 10 rows or fewer. To narrow a large employer before rows are charged, add department, team, location or commitment; Lever matches these values exactly, including capitalisation.
{
"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} |
β |
Each row is one open posting. company holds the token you supplied, not a display name, which makes it a reliable grouping key across runs. lists is an array of {text, content} objects: text is a section heading and content is that section's cleaned bullet text. created_at and scraped_at are ISO 8601 timestamps in UTC.
| 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.
The pandas snippet below (pip install pandas) prints a company-by-department table split by workplace type and saves the postings opened in the last 30 days to opened_last_30_days.csv. Missing departments and workplace types are labelled rather than dropped, so totals still add up.
import json
import pandas as pd
jobs = pd.DataFrame(json.load(open("results.json", encoding="utf-8")))
jobs["created_at"] = pd.to_datetime(jobs["created_at"], utc=True)
jobs["age_days"] = (pd.Timestamp.now(tz="UTC") - jobs["created_at"]).dt.days
jobs["department"] = jobs["department"].fillna("(none)")
jobs["workplace_type"] = jobs["workplace_type"].fillna("unspecified")
mix = pd.crosstab([jobs["company"], jobs["department"]], jobs["workplace_type"], margins=True)
print(mix.to_string())
fresh = jobs[jobs["age_days"] <= 30].sort_values("created_at", ascending=False)
columns = ["company", "title", "team", "location", "commitment", "hosted_url"]
fresh[columns].to_csv("opened_last_30_days.csv", index=False)
print(f"{len(fresh)} of {len(jobs)} postings were opened in the last 30 days")Open the company's careers page and follow any job link. If it lands on jobs.lever.co/<something>, that something is the token. Pasting the full link into companies works too; the Actor extracts the token for you.
Fetching is one request either way. You pay per posting written to the dataset, so maxItems and the four filters are what control cost on a board with hundreds of openings.
A token Lever does not know is skipped and reported in the run's status message. A real board with no openings is a valid answer with zero rows. In both cases the other employers still produce data.
The Actor stores Lever's primary location for each posting. Some postings list extra locations on the careers page; those are not expanded into extra rows.
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
- JavaScript 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.