# Lever Jobs Scraper - Career Postings (`renzomacar/lever-jobs-scraper`) Actor

Scrape all open postings from any Lever career site: title, team, commitment, workplace type, country and the direct apply link. No API key, no login, no proxy.

- **URL**: https://apify.com/renzomacar/lever-jobs-scraper.md
- **Developed by:** [Renzo Madueno](https://apify.com/renzomacar) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 job scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Lever Jobs Scraper — Career Site Postings

Scrape every open posting from any **Lever career site** by slug. No API key, no login, no cookies, no proxy, no browser. Lever's public postings endpoint returns the full job — description, requirement bullets, team, commitment, workplace type and country — in a single request.

Lever is the ATS behind a distinctive slice of the market: companies that adopted it early and stayed. That means less overlap with Greenhouse-heavy datasets and a set of postings most job aggregators miss.

***

### Read this before you build a company list

**Most companies are not on Lever.** This is the single most common surprise, and it is not a bug in this Actor. We probed thirteen well-known company slugs and got:

- **404 (no Lever site at all):** shopify, figma, brex, ramp, notion, linear, netflix, github, cohere, sourcegraph
- **200 with an empty list (on Lever, nothing open right now):** plaid, mistral
- **200 with postings:** spotify (95), palantir (308), ro (51)

The Actor treats those three outcomes as three different things and reports them separately:

- `company_not_on_lever` — HTTP 404. The slug does not exist on Lever.
- `no_open_roles` — HTTP 200 with `[]`. The company IS on Lever, it just has nothing posted today.
- delivered rows — the third case.

Feeding a list of 20 companies and having 15 come back `company_not_on_lever` is the normal, expected outcome. The `FAILURES` record even gives you the list under a dedicated `notOnLever` key so you can prune your input for next time.

***

### What you get, with the fill rate actually measured

Measured across **3 real sites — 454 live postings** (spotify, palantir, ro):

| Field | Fill rate | Notes |
|---|---|---|
| `title` | 100% | Lever calls this `text`, not `title` — a classic parsing trap, handled |
| `jobId` | 100% | Lever UUID |
| `location` | 100% | From `categories.location` |
| `locations[]` | 100% | From `categories.allLocations` |
| `country` | **100%** | ISO-2 country code at the posting root |
| `department` | 100% | |
| `team` | 100% | |
| `employmentType` | **98.7%** | Normalised from `categories.commitment` |
| `workplaceType` | **100%** | Lever publishes `hybrid` / `remote` / `onsite` on every posting |
| `isRemote` | 100% | **100% from Lever's own field** — `remoteSource: "ats_field"` on every row |
| `postedAt` | 100% | Converted from Lever's epoch-millisecond `createdAt` |
| `jobUrl` | 100% | `hostedUrl` |
| `applyUrl` | 100% | `applyUrl` — a genuinely different URL that opens the application form |
| `descriptionText` | 100% | Description plus the "additional" block, concatenated |
| `descriptionHtml` | on request | Off by default |
| `openingText` | ~100% | The opening paragraph on its own |
| `requirementLists` | on request | Structured bullet lists, see below |
| `salaryMin` / `salaryMax` / `salaryText` | **64.3%** | Parsed from text, see below |

#### Two URLs, not one

Lever is one of the few ATSs where the posting URL and the apply URL genuinely differ:

- `jobUrl` → `https://jobs.lever.co/palantir/ac978161-...` — the posting page
- `applyUrl` → `https://jobs.lever.co/palantir/ac978161-.../apply` — straight into the form

Both are returned on 100% of rows. Use `applyUrl` for anything automated.

#### Requirement lists: structure other scrapers throw away

Lever stores the "What We Require" / "What We Value" bullet sections as **structured data**, not prose. Set `includeRequirementLists: true` and you get:

```json
"requirementLists": [
  { "heading": "What We Require",
    "items": ["At least three years of previous administrative experience", "..."] },
  { "heading": "What We Value",
    "items": ["Ability to adjust quickly and anticipate needs", "..."] }
]
```

That is the raw material for requirement extraction, skills tagging or seniority classification, and you do not have to run an LLM over a wall of HTML to get it. Off by default because it adds weight.

#### Salary: 64.3%, and it is parsed

Lever's schema has a `salaryRange` field, but it is **opt-in per company and was empty on all 454 postings we measured** — 0% fill. Anyone advertising a structured Lever salary field is describing a field that exists but nobody populates.

So we parse the description text, which gets a credible range on **64.3%** of postings — the highest text-parse rate of any ATS in this fleet, because Lever's customer base skews toward jurisdictions with pay-transparency rules. Every hit is marked `salarySource: "description"` and keeps the original snippet in `salaryText`. If a company ever does populate `salaryRange`, we prefer it and mark it `ats_structured`.

Ranges are sanity-checked: a "range" whose max is more than 30× its min is discarded rather than reported. European decimal formatting (`€92.300 — €130.000`) is read correctly as 92300–130000.

***

### Input

```json
{
  "companies": ["spotify", "palantir", "ro"],
  "maxItems": 1000,
  "teamKeywords": ["Engineering"],
  "remoteOnly": false,
  "includeRequirementLists": true
}
```

#### Accepted company formats

```
spotify
https://jobs.lever.co/spotify
https://jobs.lever.co/spotify/ac978161-6f46-4f6b-ad9e-a258e642751c
https://jobs.eu.lever.co/spotify
https://api.lever.co/v0/postings/spotify
```

#### Input aliases

- companies: `companies`, `company`, `boards`, `sites`, `companyUrls`, `startUrls`
- limit: `maxItems`, `maxResults`, `limit`, `maxJobs`
- keywords: `titleKeywords`, `keywords`, `searchTitle`
- locations: `locationKeywords`, `locations`, `location`
- teams: `teamKeywords`, `teams`, `team`, `departments`, `department`
- commitment: `commitments`, `commitment`, `employmentTypes`, `jobTypes`
- descriptions: `includeDescription`, `includeContent`, `fullDescription`

#### Filters

| Option | What it does |
|---|---|
| `maxItems` | Hard ceiling on rows written. **This is your spend cap.** |
| `maxJobsPerCompany` | Stops a 308-posting site from eating the budget |
| `titleKeywords` | Substring match on the title |
| `locationKeywords` | Matches `location` and every entry in `locations[]` |
| `teamKeywords` | Matches Lever's team and department |
| `commitments` | Full-time, Part-time, Intern, Contract |
| `remoteOnly` | Only `isRemote: true` |
| `postedAfter` | ISO date |
| `includeRequirementLists` | Adds the structured bullet lists |
| `dedupe` | Drops repeat ids and repeat company+title+location |
| `concurrency` | Sites in parallel, 1–15, default 5 |

***

### Output sample

```json
{
  "source": "lever",
  "companyToken": "ro",
  "companyName": "ro",
  "jobId": "f25a6c49-5ed4-4aa0-a5bb-b30e9790f90c",
  "title": "Compounding Pharmacy Technician - Romeoville, IL",
  "department": "Pharmacy",
  "team": "Pharmacy",
  "employmentType": "FULL_TIME",
  "location": "Romeoville, IL",
  "locations": ["Romeoville, IL"],
  "country": "US",
  "isRemote": false,
  "workplaceType": "ONSITE",
  "remoteSource": "ats_field",
  "salaryMin": 23,
  "salaryMax": 27,
  "salaryCurrency": "USD",
  "salaryInterval": "HOUR",
  "salaryText": "$23 - $27",
  "salarySource": "description",
  "postedAt": "2025-06-17T00:24:42.479Z",
  "jobUrl": "https://jobs.lever.co/ro/f25a6c49-5ed4-4aa0-a5bb-b30e9790f90c",
  "applyUrl": "https://jobs.lever.co/ro/f25a6c49-5ed4-4aa0-a5bb-b30e9790f90c/apply",
  "scrapedAt": "2026-08-22T04:13:58.220Z"
}
```

Four saved dataset views ship with the Actor: **Job overview**, **Remote & locations**, **Compensation** and **Apply links**.

***

### Employment types are passed through honestly

Lever lets companies write their own commitment labels, and they do. Across 454 postings we saw `FULL_TIME` (308), `PERMANENT` (86), `INTERNSHIP` (38), `FIXED_TERM` (4), `PART_TIME` (4), `CONTRACT` (4), `SHORT_TERM` (3) and `SCHOLARSHIP` (1).

We normalise the ones with a standard meaning and pass the rest through uppercased rather than forcing them into a bucket they do not belong in. `PERMANENT` is not silently rewritten to `FULL_TIME`, because in the country where that posting was written they are not the same thing.

***

### How errors are handled

**Errors never enter your dataset.** They go to the `FAILURES` record in the run's key-value store, which for this Actor includes a dedicated `notOnLever` array listing exactly which of your slugs returned 404:

```json
{
  "runFailed": false,
  "sitesRequested": 3,
  "sitesWithJobs": 1,
  "notOnLever": ["figma"],
  "jobsDelivered": 10,
  "duplicatesDropped": 0,
  "byReason": { "company_not_on_lever": 1, "no_open_roles": 1 }
}
```

**If nothing was delivered, the run ends FAILED** with the reason breakdown. An empty dataset is never reported as success.

***

### Pricing and the free tier

Pay per event:

- **Actor start** — one small charge per gigabyte of memory
- **Job scraped** — charged **after** the row is written

Rows are pushed in batches and only rows that landed are billed. Filtered-out postings, deduplicated repeats and every kind of failure cost you nothing. Given how often Lever slugs 404, that matters here more than anywhere else in the fleet: **a company that turns out not to be on Lever costs you nothing at all.**

**The free tier returns real data.** No API key, no proxy, no credentials. `{"companies": ["spotify"], "maxItems": 10}` on a free account returns ten real jobs.

***

### Speed

One HTTP request per site, gzip-compressed. Boards can be around 1 MB, which is exactly why compression is requested on every call. Measured: **454 postings across 3 sites in under 10 seconds.** Retries use exponential backoff on 429/5xx and never fire on 404 — a 404 is an answer, not a failure.

***

### Common questions

**Do I need a Lever API key?** No. `api.lever.co/v0/postings/<slug>?mode=json` is the public postings feed. The authenticated Lever API is a different product.

**How do I find a company's slug?** It is the path segment in their Lever URL: `jobs.lever.co/spotify` means `spotify`. If their careers page is on their own domain, check the page source for a `jobs.lever.co` link.

**Why is the title in a field called `text`?** That is Lever's schema, not ours. We map it to `title` for you, which is precisely the bug that breaks naive scrapers.

**Does the EU host work?** Yes, `jobs.eu.lever.co/<slug>` URLs are parsed correctly.

**Why is `postedAt` the same as `updatedAt`?** Lever's public feed exposes only `createdAt`. We surface it in both rather than inventing a modification date we do not have.

**How fresh is the data?** Live on every run.

***

### Related Actors in this fleet

- **Greenhouse Jobs Scraper** — the largest board population
- **Ashby Jobs Scraper** — structured salary numbers on ~72% of postings
- **Workday Jobs Scraper** — enterprise career sites by URL
- **Startup Jobs Aggregator** — company handles in, one deduplicated schema out across Greenhouse, Ashby and Lever, so you stop guessing which ATS a company uses

### Legal

This Actor reads a public, unauthenticated JSON endpoint that Lever publishes so job boards and aggregators can syndicate postings. It sends no credentials, solves no challenges and bypasses no access control. You are responsible for how you use the data, including any applicable data-protection rules.

# Actor input Schema

## `companies` (type: `array`):

One per line. A slug ("spotify") or any jobs.lever.co URL. Most companies are NOT on Lever; ones that are not come back in the FAILURES record, not the dataset. Aliases: company, boards, sites, companyUrls.

## `maxItems` (type: `integer`):

Hard ceiling on rows written. Aliases: maxResults, limit, maxJobs.

## `maxJobsPerCompany` (type: `integer`):

Max jobs per site.

## `titleKeywords` (type: `array`):

Keep only postings whose title contains one of these (case-insensitive). Leave empty for all. Aliases accepted in raw input: keywords, searchTitle.

## `locationKeywords` (type: `array`):

Keep only postings whose location or office list contains one of these. Example: "New York", "Remote", "London".

## `remoteOnly` (type: `boolean`):

Keep only postings flagged remote. Check the remoteSource field to see whether the flag came from the ATS or was inferred.

## `postedAfter` (type: `string`):

ISO date, e.g. 2026-07-01. Drops anything published before it.

## `teamKeywords` (type: `array`):

Lever groups roles by team, e.g. "Engineering", "Design".

## `commitments` (type: `array`):

Full-time, Part-time, Intern, Contract. Empty means all.

## `includeDescription` (type: `boolean`):

Plain-text job description. Turn it off for a much smaller dataset.

## `includeHtmlDescription` (type: `boolean`):

Adds the original HTML alongside the text version. Off by default because it roughly triples dataset size.

## `dedupe` (type: `boolean`):

Drops repeats by job id, and repeats of the same company + title + location published twice.

## `includeRequirementLists` (type: `boolean`):

Lever stores "What we require" style bullets as structured lists. Adds a requirementLists array.

## `concurrency` (type: `integer`):

How many requests to run at the same time. Lower it if you hit rate limits.

## Actor input object example

```json
{
  "companies": [
    "spotify",
    "palantir",
    "ro"
  ],
  "maxItems": 1000,
  "maxJobsPerCompany": 100000,
  "titleKeywords": [],
  "locationKeywords": [],
  "remoteOnly": false,
  "teamKeywords": [],
  "commitments": [],
  "includeDescription": true,
  "includeHtmlDescription": false,
  "dedupe": true,
  "includeRequirementLists": false,
  "concurrency": 5
}
```

# Actor output Schema

## `results` (type: `string`):

One row per open role: title, team, location, commitment and the direct apply link.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "companies": [
        "spotify",
        "palantir",
        "ro"
    ],
    "titleKeywords": [],
    "locationKeywords": [],
    "teamKeywords": [],
    "commitments": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("renzomacar/lever-jobs-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "companies": [
        "spotify",
        "palantir",
        "ro",
    ],
    "titleKeywords": [],
    "locationKeywords": [],
    "teamKeywords": [],
    "commitments": [],
}

# Run the Actor and wait for it to finish
run = client.actor("renzomacar/lever-jobs-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "companies": [
    "spotify",
    "palantir",
    "ro"
  ],
  "titleKeywords": [],
  "locationKeywords": [],
  "teamKeywords": [],
  "commitments": []
}' |
apify call renzomacar/lever-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,renzomacar/lever-jobs-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/WfGAEJn0OH1RTw1qt/builds/HLStMido7RiFbGA3W/openapi.json
