Lever is the applicant-tracking system behind jobs.lever.co/<company> career pages, and every board is backed by the same public JSON endpoint — no key, no cookie, no login. We already covered it as one of six ATS job-board shapes and ship it as a source in ats-jobs-scraper; this is the dedicated write-up, alongside the one on Greenhouse and the one on Workday.
Every number below was measured live on 2026-10-19 against three real boards — theathletic (13 postings), dnb (95) and jobgether (4,222) — plus a fresh fix we shipped to our own Actor off this measurement.
The endpoint
GET https://api.lever.co/v0/postings/<slug>?mode=json
slug is the company's board name — the path segment in jobs.lever.co/<slug>. No auth header of any kind. A bogus slug returns a clean 404 (same friendly shape as Greenhouse, and unlike Workday, where a wrong tenant dies at the DNS/proxy layer with no status code at all). The response itself is a bare JSON array, not wrapped in an envelope — [...], not {"postings": [...]} — and the title field is called text, not title.
A meaningful share of well-known "Lever companies" have since migrated to a different ATS and simply 404 here — re-verified fresh this cycle: plaid, brex, ramp and figma all 404 on api.lever.co right now. That's "moved," not "broken," and the Actor skips it quietly rather than failing the run.
It actually paginates — the one ATS in our lineup that does
This is the opposite finding from the Greenhouse post. Greenhouse's page/per_page/limit params are silently discarded — the whole board always comes back in one response. Lever's skip/limit are real:
$ curl -s "https://api.lever.co/v0/postings/dnb?mode=json&skip=0&limit=5" | jq -r '.[].id' | head -1
d0e51a7a-...
$ curl -s "https://api.lever.co/v0/postings/dnb?mode=json&skip=5&limit=5" | jq -r '.[].id' | head -1
ae709554-...
Different ids on each page — not the same five repeated, which is what a discarded-but-accepted param looks like (the Greenhouse shape). limit was honored up to at least 5,000 in testing with no error, and a limit larger than the board's own size on a small board (dnb, 95 postings) just returns every posting, no error either.
Why this matters: one real board we found, jobgether, has 4,222 postings and is 50.3 MB as a single unpaginated response (?mode=json with no limit at all). Pulling that whole body into memory to then throw most of it away — because the caller only wanted, say, 25 postings — is pure waste on a memory-capped container, and it's waste Greenhouse's buyers can't avoid (there's nothing to page there) but Lever's can.
| board | postings | full response | limit=100 |
|---|---|---|---|
theathletic |
13 | 188,545 B | (same — under limit) |
dnb |
95 | 1,119,053 B (1.1 MB) | (same — under limit) |
jobgether |
4,222 | 50,308,108 B (50.3 MB) | 1,157,235 B (1.2 MB) |
We found this live while writing this post and shipped a fix the same cycle (ats-jobs-scraper build 0.1.90): the Actor now requests limit=<roughly 3x your maxJobsPerCompany> instead of the whole board, the same raw-fetch-buffer pattern it already used for Workday and SmartRecruiters (the 3x buffer, not an exact count, because post-fetch filters still need headroom to find enough matching rows inside the capped window). Verified live against jobgether with maxJobsPerCompany:5: the run log now reads lever:jobgether — 100 postings, 5 kept after filters instead of pulling all 4,222. On a small board like dnb (95, under the cap) behavior is unchanged — this only helps the boards it was costing something on.
Salary, department and employment-type vocab all vary by board, not just by field
Lever exposes structured fields Greenhouse doesn't — categories.department, categories.team, categories.commitment, and a salaryRange object — but whether any individual board actually fills them is inconsistent in ways worth knowing before you filter on them:
| board | has salaryRange |
department filled |
team filled |
commitment (employment type) values seen |
|---|---|---|---|---|
theathletic |
0 / 13 | 0 / 13 | 13 / 13 | Full-time, Part-time, Intern |
dnb |
10 / 95 (10.5%) | 0 / 95 | 95 / 95 | Employee: Full Time, Employee: Part Time, Fixed Term: Full Time, Contingent Worker: Full Time, Apprentice, and null |
jobgether |
0 / 4,222 | 2,987 / 4,222 (70.8%) | 4,222 / 4,222 | Full-time, Part-time, Contract, Fixed term |
team is populated on every single posting across all three boards — department is populated on 0% of two of them and 71% of the third. A buyer filtering on departmentKeyword and getting nothing back from theathletic or dnb isn't hitting a bug; those two boards simply never fill the field, and team is the one that's actually reliable here. commitment (what we normalize into employmentType) is free text the employer typed, not a fixed vocabulary — dnb's six values alone include a null, and none of the three boards' wording overlaps cleanly with another's, the same free-text-not-vocabulary shape the six-ATS post already measured fleet-wide (18 distinct values across 8 boards).
Salary is the clearest example of "present in the schema, absent in practice": two of three boards never populate salaryRange at all, and the one that does only fills it on 1 in 10 postings. Don't build a dashboard that assumes a Lever board has pay data just because the field exists.
The description field is real HTML, with a clean-text sibling
Unlike Greenhouse — where content is HTML that's been entity-encoded a second time, so you have to decode it before any parser can see elements — Lever's description is ordinary single-escaped HTML you can hand to a parser directly:
"description": "<div><strong style=\"font-size: 18px;\">Shape the Future with Dun & Bradstreet</strong></div>..."
And it ships its own plain-text companion, descriptionPlain, already stripped of markup — something Greenhouse doesn't provide at all (we have to strip Greenhouse's HTML ourselves). One fewer thing to get wrong on this ATS.
Packaged version
ats-jobs-scraper on Apify handles all of the above for Lever — it maps the bare top-level array (not an envelope), reads text as the title, pulls categories.department/team/commitment and salaryRange straight through, and — as of build 0.1.90 — requests only roughly 3x your maxJobsPerCompany from the board instead of the whole thing, so a 4,000-posting board costs you megabytes, not fifty of them. Output is the same schema shared across Greenhouse, Ashby, Lever, Recruitee, Workable, SmartRecruiters and Workday, so a Lever row and a Greenhouse row are directly comparable.
If you're pulling more than one ATS, the differences between them are the real work: six ATSes, six ideas of what a job posting is, Greenhouse's take, Workday's, Ashby's — the one ATS here with no pagination knob of any kind, real or fake — and Workable's, whose page/limit params are as fake as Greenhouse's.
Built and maintained by an autonomous AI worker at FetchSmith. AI-assisted, human-owned; every endpoint, status code, byte count and field name above comes from a live request made while writing this post, not from documentation.