Lever Jobs Scraper: Postings, Teams & Commitments
Pricing
from $0.74 / 1,000 posting scrapeds
Lever Jobs Scraper: Postings, Teams & Commitments
Pull every public posting from any Lever careers site: title, team, commitment, workplace type, location and full description. No login, no proxy, no browser.
Pricing
from $0.74 / 1,000 posting scrapeds
Rating
0.0
(0)
Developer
Arman Hossain
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
21 hours ago
Last modified
Categories
Share

Pulls every public posting from any Lever-hosted careers site. You get the title, team, department, commitment, workplace type, location list, requirement bullets, the salary range where the company publishes one, and the direct apply URL.
Lever runs the applicant tracking system behind thousands of startups and scale-ups, and every careers site is backed by a public endpoint. This Actor reads that endpoint directly, so there's no browser, no proxies, no login and no rate-limit games. A board with 400 postings comes back in a few seconds.
Agent skill: SKILL.md
https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/lever-jobs-scraper.md
What you get
| Field | What it holds |
|---|---|
company | The Lever slug the posting came from |
postingId | Lever's UUID for the posting |
title | Job title, which Lever calls text on the wire |
team, department, level | Lever's category taxonomy. department and level are only set by companies that use them |
commitment | Free-text employment type, e.g. Full-time, Contract, Internship |
workplaceType | remote, onsite, hybrid or unspecified |
location, allLocations, country | Primary location, the full location list, and the ISO country code |
categories | The raw Lever category object, kept verbatim |
salaryRange, salaryDescription | { min, max, currency, interval } and any free-text pay note, when the company publishes them |
descriptionPlain | Opening and body as plain text, straight from Lever |
lists | Requirement and responsibility blocks as { text, items[] }, with bullets split into an array |
additionalPlain | The closing "about the company" section as plain text |
hostedUrl, applyUrl | Posting page and the direct application form |
createdAt | Posting creation time, converted from Lever's epoch milliseconds to ISO 8601 |
scrapedAt | When the run happened |
RUN_SUMMARY in the key-value store holds per-run counts, the filters you used, and any company that failed.
Use cases
- Portfolio hiring tracking. Watch every company a fund has invested in and diff headcount signals week over week.
- Job aggregators. Ingest hundreds of Lever careers sites on a schedule into one normalised feed.
- Remote-supply analysis. Count
workplaceType: "remote"postings by team across a sector. - Lead generation. A company hiring three data engineers is a company buying data tooling.
- Compensation research. Collect the
salaryRangeobjects that pay-transparency laws now force onto public postings.
Quick start
Two companies, everything they have:
{"companies": ["leverdemo", "spotify"]}
Remote engineering roles only, capped:
{"companies": ["leverdemo", "https://jobs.lever.co/ro"],"searchTerms": ["engineer", "developer"],"teamFilter": ["engineering"],"workplaceType": ["remote"],"maxJobsPerCompany": 50,"includeDescription": true}
A light sweep for change detection:
{"companies": ["spotify"],"includeDescription": false}
Input
| Field | Type | Default | Notes |
|---|---|---|---|
companies | array | required | Company slugs like leverdemo, or full careers URLs. You can mix both. URLs get normalised to slugs for you. |
searchTerms | array | [] | Keep only titles containing one of these terms, case-insensitive. Empty means keep everything. |
teamFilter | array | [] | Matched against team and department. Empty means all teams. |
commitmentFilter | array | [] | Substring match, so full catches both Full-time and Regular Full Time (Salary). Empty means all. |
workplaceType | array | [] | Exact match on remote, onsite, hybrid, unspecified. on-site and on site are normalised to onsite. Empty means all. |
locationFilter | array | [] | Matched against the primary location and every entry in allLocations. Empty means all locations. |
maxJobsPerCompany | integer | 0 | Cap postings saved per company, applied after filtering. 0 means no limit. |
includeDescription | boolean | true | Include descriptionPlain, lists and additionalPlain. Turn it off for a far smaller dataset. |
All filters are independent and combine with AND. A posting has to match at least one term in each filter you actually filled in.
Output example
A real record, trimmed only in descriptionPlain:
{"company": "ro","postingId": "d2e897ae-3b4c-4219-ab0d-0403decc2115","title": "Engineering Manager, Core Infrastructure","team": "Engineering","department": null,"level": null,"commitment": "Full-time","workplaceType": "hybrid","location": "New York, NY","allLocations": ["New York, NY"],"country": "US","categories": {"commitment": "Full-time","location": "New York, NY","team": "Engineering","allLocations": ["New York, NY"]},"salaryRange": null,"salaryDescription": null,"descriptionPlain": "Join Tech @ Ro to build the future of healthcare, from the ground up .","lists": [{"text": "What You'll Do","items": ["Lead and develop a Core Infrastructure team responsible for production-critical systems and foundational engineering capabilities.","Own execution across the team's portfolio: clarify priorities, sequence work, manage tradeoffs, and ensure high-quality delivery."]}],"additionalPlain": "The target base salary for this position ranges from $219,300 to $260,000 .","hostedUrl": "https://jobs.lever.co/ro/d2e897ae-3b4c-4219-ab0d-0403decc2115","applyUrl": "https://jobs.lever.co/ro/d2e897ae-3b4c-4219-ab0d-0403decc2115/apply","createdAt": "2026-06-15T13:33:29.004Z","scrapedAt": "2026-08-06T11:40:18.574Z"}
This posting leaves salaryRange empty and states pay in prose instead, which is a common pattern. Where the structured field is filled in it looks like { "min": 108097, "max": 135121, "currency": "USD", "interval": "per-year-salary" }.
Finding a company slug
Open the company's careers page and look at the URL.
| URL you see | Slug |
|---|---|
jobs.lever.co/acme | acme |
jobs.eu.lever.co/acme | acme |
jobs.lever.co/acme/1234-5678 | acme |
You can paste the whole URL and let the Actor pull the slug out. If a company's careers page sits on its own domain, view source and search for lever.co. The slug is almost always in an iframe or a fetch URL.
API example
curl -X POST "https://api.apify.com/v2/acts/arman-bd~lever-jobs-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \-H "Content-Type: application/json" \-d '{"companies": ["leverdemo"],"searchTerms": ["engineer"],"maxJobsPerCompany": 25}'
JavaScript example
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: 'YOUR_TOKEN' });const run = await client.actor('arman-bd/lever-jobs-scraper').call({companies: ['leverdemo', 'spotify'],searchTerms: ['engineer'],workplaceType: ['remote'],});const { items } = await client.dataset(run.defaultDatasetId).listItems();for (const job of items) console.log(`${job.company} - ${job.title} (${job.location})`);
Notes
- Lever returns every posting in one response, so there is no pagination to walk. Filtering and
maxJobsPerCompanyare applied after the fetch, which means a capped run is not a cheaper request. - Timeouts are generous. Large boards return multi-megabyte payloads and Lever has been observed taking around 100 s, so each request gets 120 s before it is abandoned, with up to three attempts and linear backoff.
- A bad slug won't kill the run. An unknown company returns HTTP 404 with
{"ok":false,"error":"Document not found"}and lands inRUN_SUMMARY.failures. The Actor only errors out if every company fails. - Bullets are split, not stripped. Lever ships requirement blocks as a bare run of
<li>elements, and each block becomes{ text, items[] }so you can count or filter requirements without touching HTML. - Public data only. No authentication, no personal data, nothing that gets around access controls.
FAQ
Do I need a Lever API key? No. You supply no credentials.
Why is title not called text? Because Lever's wire field is text, which is unusable in a spreadsheet. It is renamed to title, and everything else keeps Lever's own naming.
Does every posting have a salary? No. salaryRange is populated only where the company filled it in, commonly for US roles covered by pay-transparency rules. Elsewhere it is null, and nothing is inferred.
Why did I get an empty array back? A valid slug with no open roles returns [] with HTTP 200, which is different from a wrong slug returning HTTP 404. RUN_SUMMARY distinguishes the two: the first shows 0 listed, the second shows up in failures.
Can I get only new postings? Run it on a schedule and diff on postingId. createdAt gives you a genuine first-seen date, so a daily run reconstructs a hiring timeline.
Can I plug it into something else? Yes. Apify API, the client libraries, webhooks, scheduled runs, dataset exports to JSON, CSV or Excel, or MCP. The output is structured JSON.