# Lever Jobs API

> Query jobs from Lever boards through one API with ?source=lever — plus how Lever's postings endpoint really behaves.

Source: https://hyperjobs.io/ats/lever

Lever is a talent acquisition suite used heavily by mid-market tech companies and
scale-ups. Each customer's board lives at `jobs.lever.co/<site>`, where `<site>`
is a short company slug, and every board is backed by a public JSON endpoint that
returns all of a company's live postings in one unauthenticated call. Lever is the
third-largest source in this dataset at 74,374 postings — a number that moves
hourly, so read it from [`/v1/meta`](/docs/api-reference/meta) rather than
trusting this sentence.

## Try it

<Note>
  The value is simply `lever` — the public slug, everywhere: the `?source=`
  filter, the keys of [`/v1/meta`](/docs/api-reference/meta) → `sources`, and
  the `source` field on every returned posting. (A pre-release draft of this
  API stored `lever.co` and `?source=lever` silently returned zero rows; that
  trap [didn't ship](/docs/changelog). `lever.co` is still accepted as input.)

  ```bash
  curl "…/v1/jobs?source=lever"      # ✓ 74,374 postings
  curl "…/v1/jobs?source=lever.co"   # ✓ same — legacy value, still accepted
  ```
</Note>

```bash
curl -G https://api.hyperjobs.io/v1/jobs \
  -H "Authorization: Bearer $HYPERJOBS_KEY" \
  --data-urlencode "source=lever" \
  --data-urlencode "time_frame=7d" \
  --data-urlencode "limit=2"
```

```json title="Illustrative — the shape, not a live response"
{
  "data": [
    {
      "id": "lever.co:8f2c1d0a-4b77-4e91-9a3e-2c8b5f1d6e04",
      "title": "Product Designer",
      "company": {
        "slug": "northwind",
        "name": "Northwind",
        "domain": "northwind.io",
        "website": "https://northwind.io",
        "logo": "https://media.licdn.com/dms/image/…",
        "industry": "Software Development",
        "employee_count": 210
      },
      "location": {
        "remote": true,
        "countries": ["United Kingdom"],
        "cities": ["London"],
        "regions": null,
        "raw": ["London, United Kingdom"]
      },
      "employment_type": ["FULL_TIME"],
      "work_arrangement": "Remote",
      "seniority": null,
      "salary": {
        "min": 75000,
        "max": 95000,
        "currency": "GBP",
        "unit": "YEAR",
        "annual_min": 75000,
        "annual_max": 95000,
        "summary": "£75,000 – £95,000"
      },
      "skills": ["figma", "prototyping"],
      "taxonomies": ["Design"],
      "apply_url": "https://jobs.lever.co/northwind/8f2c1d0a-4b77-4e91-9a3e-2c8b5f1d6e04/apply",
      "url": "https://jobs.lever.co/northwind/8f2c1d0a-4b77-4e91-9a3e-2c8b5f1d6e04",
      "source": "lever",
      "posted_at": "2026-07-10T09:14:00.000Z",
      "expires_at": null,
      "updated_at": "2026-07-14T11:20:03.000Z"
    }
  ],
  "total": 2,
  "limit": 2,
  "offset": 0
}
```

Note that `apply_url` and `url` differ here. Lever publishes a real apply link
separately from the posting page, which is not true of every platform —
[Greenhouse](/ats/greenhouse) has only one URL and both fields carry it.

## How Lever boards actually work

One GET, no auth, no pagination, every active posting:

```bash
curl "https://api.lever.co/v0/postings/<site>?mode=json"
```

`<site>` is the slug from `jobs.lever.co/<site>`. The response is a bare JSON
**array** of postings — not an object with a `jobs` key, which trips up anyone
reaching for `data.jobs` out of habit.

### The description arrives in pieces

This is the structural thing to know about Lever. There is no single description
field. A posting's body is split across three keys, and you have to reassemble
them in order:

| Field | What it holds |
| --- | --- |
| `description` | The opening HTML — the pitch and context |
| `lists[]` | Sections, each `{ text: "Responsibilities", content: "<li>…</li>" }` |
| `additional` | The closing HTML — EEO statements, legal boilerplate |

`lists[]` is the interesting one: `text` is a section heading and `content` is a
run of bare `<li>` elements with **no enclosing `<ul>`**. You supply the wrapper.
Read only `description` and you get a job's first paragraph and none of its
actual requirements — which looks like a populated field, so nothing alerts you.

```
description  +  <h3>text</h3><ul>content</ul>  (per list)  +  additional
```

### Structured fields Lever does give you

`workplaceType` is a real four-value enum — `remote`, `on-site`, `hybrid`,
`unspecified` — rather than a boolean, so hybrid survives instead of collapsing
into remote-or-not. `salaryRange` carries `{ min, max, currency, interval }` with
`interval` as `per-year-salary` or `per-hour-wage`. `salaryDescriptionPlain` holds
the comp prose as posted. `categories` holds `commitment`, `location`,
`allLocations[]`, `team`, `department`, and sometimes `level`.

`createdAt` is a **millisecond epoch**, not a string. It's the only date the feed
gives you.

## What bites you

**The top-level `country` will lie on multi-location postings.** Lever publishes a
posting-level `country` (ISO alpha-2) alongside `categories.location`, which is
frequently city-only — `Bengaluru`, with no country. Appending the country fixes
that job. But a posting listing `Paris, London, Munich` also has exactly one
top-level `country`, and appending it to all three produces `London, France` and
`Munich, France`. We only append when the posting has a **single** location;
multi-location postings keep their bare cities and each gets resolved on its own.

**`categories.level` is mostly not a seniority.** Some boards set it to `Senior`
or `Junior`. Others set it to `All Levels`, which means nothing. We map the values
that correspond to a real level and drop the rest to null rather than store
`All Levels` as a seniority.

**A posting can have `applyUrl` but no `hostedUrl`, or the reverse.** Neither is
guaranteed. Take whichever exists rather than assuming the hosted page is there.

**`createdAt` is the only timestamp.** There's no updated date and no expiry date
in the feed. If you want to know a Lever posting is gone, the only way is to fetch
the board again and notice it's missing — which is exactly what board diffing is,
and why a job that's taken down is only reaped as fast as you re-fetch.

## Or use ours

The fetch is a one-liner. Everything after it isn't.

You get the description already reassembled from its three pieces, Lever
rows deduplicated against the same job on [LinkedIn](/ats/linkedin) or a
[Greenhouse](/ats/greenhouse) board, expiry by board diffing so a pulled role
disappears, and the [company join](/docs/objects/company) — a Lever board tells
you the slug `northwind` and nothing else about who that is. Our queue tracks
4,803 Lever boards, which is the part that actually took the time: Lever has no
public index of its customers, so the list is assembled rather than fetched.

Lever sits in the fast refresh tier, attempted every cycle — see
[coverage](/docs/reference/coverage).

Query it with `?source=lever`.
[Quickstart](/docs/quickstart), then [pricing](/pricing).
