# Lever Jobs Scraper (`scrapyx/lever-jobs-scraper`) Actor

Jobs from any company's Lever job board (jobs.lever.co, jobs.eu.lever.co) via the public Postings API: title, department, team, location, remote/hybrid, commitment, salary range, posted date, apply URL and the full description. Filter by keyword, department, location, remote.

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

## Pricing

from $0.84 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Lever Jobs Scraper

Every open job on any company's **Lever** job board — Zoox, Binance,
Palantir and the thousands of others hosted at `jobs.lever.co/<site>` (or
`jobs.eu.lever.co/<site>`) — through Lever's public Postings API: title,
department, team, commitment, location(s) and country, remote / hybrid /
on-site, **salary range with currency and interval** where the company
publishes it, posted date, apply URL, and the full description assembled
from Lever's four description pieces, as text and HTML.

HTTP only, no login, no key, no browser. Boards are walked in pages of 100
postings; Lever's `Crawl-delay: 1` is honoured.

### What it is for

- **Company watchlists** — a list of sites on a schedule, diff by `jobId`.
- **Multi-company search** — many sites, one keyword, one dataset.
- **Salary datasets** — `salaryRange` is structured where present
  (Zoox 225 of 239 postings; Binance none).
- **EU-hosted boards** — a `jobs.eu.lever.co` URL is fetched from the EU
  API host automatically.

### Input

| field | what it does |
| --- | --- |
| `sites` | Site slugs (`zoox`) or URLs (`jobs.lever.co/zoox`, `jobs.eu.lever.co/<site>`, a job URL under the site). Case-sensitive on Lever's side; sent as given. |
| `searchTerms`, `departments`, `locations`, `remoteOnly` | Local filters on the walked board (see #3 below for why they are local). |
| `includeDescription` | On by default; same requests, rows ~10× bigger. |
| `maxItems`, `maxConcurrency`, `minRequestInterval`, `proxyConfiguration` | Limits. `minRequestInterval` never goes below 1 s. |

Each site gets a `BOARD_SUMMARY` with the pages walked, the total (known
only when the board was walked to its end), how many rows carry a salary,
what the filters dropped, and the board's own department and location
vocabularies.

### Four things about this API worth knowing before you trust a run

#### 1. Offset pages with no total, and bad values fail soft

`skip`/`limit` work and the order is stable — but nothing says how many
postings exist; a page shorter than `limit` is the end. `limit=abc` answers
`200 []` (indistinguishable from an empty board), `limit=-5` answers
*N−5* postings, `skip=-1` answers `[]`. The Actor only ever sends
non-negative integers and stops at the first short page.

#### 2. An empty board and a missing board differ only by status

`kraken`, `mistral`, `wealthsimple` → `200 []` (a real site with nothing
public). `plaid`, `figma` → `404 {"ok":false,"error":"Document not found"}`.
And `LEVERDEMO` is a 404 while `leverdemo` has 11 postings: slugs are
case-sensitive. 404 → `site_not_found`; `[]` → summary with
`totalJobsOnBoard: 0` and `stoppedReason: empty_board`.

#### 3. The server-side filters are exact matches with inconsistent case rules

`?department=software`, `Software` and `SOFTWARE` all return 90 Zoox jobs,
but `?department=Soft` returns 0; `?location=Foster City, CA` returns 204
while `?location=foster city, ca` returns 0. A typo or a case slip is a
silent empty run. The Actor never sends them; `departments` / `locations`
are substring, case-insensitive filters on the walked board.

#### 4. The description is four pieces, not one

`description` is only the opening paragraph; the titled sections
(Requirements, Benefits, "In this role, you will:") are `lists[]` of HTML
`<li>` strings; `additional` is the closing paragraph; `salaryDescription`
another. The Actor assembles all of them into `description` (text) and
`descriptionHtml`, and keeps the raw pieces in `sections`. Section titles
carry trailing whitespace on real boards; stripped.

### Other things measured

- `createdAt` is an integer of epoch milliseconds; kept raw as
  `createdAtMs` and converted to ISO UTC in `createdAt`.
- `workplaceType` is lowercase `remote` / `hybrid` / `onsite`.
- `allLocations` is the full list where a posting is open in several
  offices (Binance: 243 of 298 postings).
- Postings are ~10 KB each; a 100-posting page is ~1 MB. No throttling or
  anti-bot layer seen.

### Output

- **`JOB`** — `jobId`, `title`, `url`, `applyUrl`, `department`, `team`,
  `commitment`, `workplaceType`, `location`, `allLocations`, `country`,
  `createdAt`, `createdAtMs`, `salaryMin`, `salaryMax`, `salaryCurrency`,
  `salaryInterval`, `salaryDescription`, `description`, `descriptionHtml`,
  `sections`, `site`, `region`, `resultPosition`.
- **`BOARD_SUMMARY`** — `site`, `region`, `boardUrl`, `totalJobsOnBoard`
  (null when the walk stopped early), `jobsReturned`, `pagesWalked`,
  `rowsWithSalary`, `stoppedReason` (`exhausted`, `max_items`,
  `empty_board`, `max_pages`), `filteredOut`, `departmentsOnBoard`,
  `locationsOnBoard`.
- **`ERROR`** — `invalid_input`, `site_not_found`, `payload_shape_changed`,
  `fetch_failed`, with detail.

### Known limits

- No total exists on the API; `totalJobsOnBoard` is the count walked and is
  null when `maxItems` stopped the walk.
- The order Lever returns is not by date; sort on `createdAt` yourself.
- Salary is what the company chose to publish (`rowsWithSalary` says how
  many).

# Actor input Schema

## `sites` (type: `array`):

Lever site slugs or job-board URLs: 'zoox', 'binance', https://jobs.lever.co/zoox, https://jobs.eu.lever.co/<site> for EU-hosted boards (a job URL under the site works too). Slugs are case-sensitive on Lever's side and are sent as given. An unknown slug is reported as site\_not\_found; a site with no public postings gets a summary with 0 jobs.

## `searchTerms` (type: `array`):

Keep jobs whose title, department, team or description contains any of these (case-insensitive). Applied locally after the board is walked. Empty = all jobs.

## `departments` (type: `array`):

Keep jobs whose department or team name contains any of these. departmentsOnBoard in the summary row lists the vocabulary. Lever's own server-side filters are exact-match with inconsistent case rules and answer a typo with an empty board; they are not used.

## `locations` (type: `array`):

Keep jobs whose location, any of its locations, or country code contains any of these ('foster city', 'remote', 'SG').

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

Keep only jobs whose workplaceType is remote.

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

On by default: the full description assembled from Lever's opening, titled sections, closing and salary paragraphs as text (description) and HTML (descriptionHtml), plus the raw sections. Same requests either way; rows are ~10x smaller without it.

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

Overall cap on JOB rows across every site. Boards are walked in pages of 100 postings (~1 MB each) until a short page; the cap stops the walk early.

## `maxConcurrency` (type: `integer`):

Requests in flight at once, across sites. Request starts are always at least 1 second apart (Lever's robots.txt Crawl-delay).

## `minRequestInterval` (type: `integer`):

Politeness delay between request starts. Values under 1 are raised to 1: api.lever.co publishes Crawl-delay: 1.

## `proxyConfiguration` (type: `object`):

Optional. A public API published for embedding; no anti-bot layer. Enable Apify's free datacenter proxy only if a cloud run reports fetch\_failed.

## Actor input object example

```json
{
  "sites": [
    "zoox"
  ],
  "remoteOnly": false,
  "includeDescription": true,
  "maxItems": 1000,
  "maxConcurrency": 3,
  "minRequestInterval": 1,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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 = {
    "sites": [
        "zoox"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/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 = { "sites": ["zoox"] }

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/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 '{
  "sites": [
    "zoox"
  ]
}' |
apify call scrapyx/lever-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/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/uPiasHr7pew2kQS2c/builds/oaHXi2Zp4OfCsBMt7/openapi.json
