# Lever Jobs Scraper & API - Deduplicated, Pay Once Per Job (`automation_craft/lever-jobs-scraper`) Actor

Scrape every open job from any Lever careers page (jobs.lever.co): title, team, location, commitment, salary range when published, full description. Deduplicated across runs, you pay once per job. Also reads Greenhouse, Ashby, Workable and SmartRecruiters.

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

## Pricing

Pay per event

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 & API - Deduplicated, Pay Once Per Job

Get every open job from any **Lever** careers page as clean JSON, CSV or Excel: title, department, team, location, remote flag, employment type, posting date, apply link and (optionally) the full description. Paste the careers URL, press Start, done. No login, no API keys, no proxies, no code.

Built to be complete and fair:

- **Deduplicated, billed once.** Duplicates inside a run are removed. Give your search a **memory name** and every later run returns only the jobs that appeared since; jobs you already received are skipped and never charged again. Schedule it daily or weekly and you have a change feed.
- **Filters run before billing.** Keywords (must contain / must not contain), locations, remote only, employment type and an exact posting-date window. You only pay for jobs that pass.
- **Exact caps, fair share.** Per-company and per-run caps are honored exactly (the top competitor's 3-star review is "I set maximum 500 but got 200"). When several companies share a cap, each gets a fair share and unused budget flows to the companies with more jobs.
- **Honest about what you get.** Every run ends with a `run-summary` item: jobs per board, how many were filtered or already known, exactly what was charged, and how often each field was populated.

The same engine also reads Greenhouse, Ashby, Workable and SmartRecruiters boards, so you can mix careers pages from other ATS platforms into the same run. Sibling listings for those platforms are linked at the bottom.

### 30-second start

1. Under **Companies / careers pages**, paste one careers URL per line, for example `https://jobs.lever.co/spotify` (any of these forms work: jobs.lever.co/<company>, jobs.eu.lever.co/<company>) or just the company token `spotify` (the site token is the last part of the careers URL, lower case).
2. Optional: tick **Include full job descriptions**, add keyword / location / date filters.
3. Give it a **Memory name** if you will run it again and only want new jobs.
4. Start. Download JSON, CSV or Excel from the Dataset tab, or read it through the API.

Prices: **$1.20 per 1,000 new jobs**, plus $0.50 per 1,000 for full descriptions (charged only for jobs that actually got one). A complete record therefore costs **$1.70 per 1,000 jobs**. Duplicates across runs, filtered-out jobs, unknown companies and empty boards are free. There is no start fee.

Worked examples: 200 new jobs across 3 companies = **$0.24**. The same 200 with descriptions = **$0.34**. A weekly re-run that finds 12 new jobs among 200 known ones = **$0.014**. A re-run that finds nothing new = **$0.00**.

### What you get from Lever

Every job record has the same fields whatever the ATS. Where a platform does not publish a field you get `null`; the Actor never guesses. This table is the exact contract:

| Field | Lever | Greenhouse | Ashby | Workable | SmartRecruiters |
|---|:-:|:-:|:-:|:-:|:-:|
| `title, url, applyUrl, jobId` | yes | yes | yes | yes | yes |
| `departments` | yes | yes | yes | yes | yes |
| `team` | yes | no | yes | yes (function) | yes (function) |
| `location and allLocations` | yes | yes (+ offices) | yes (+ secondary) | yes | one per posting |
| `remote flag` | yes | only when the location says Remote | yes | yes | yes |
| `employmentType` | yes | no | yes | yes | yes |
| `compensation` | when published | no | structured ranges | no | no |
| `publishedAt` | yes | yes | yes | yes | yes |
| `updatedAt` | no | yes | no | no | no |
| `descriptionHtml and descriptionText` | yes | yes | yes | yes | yes (one extra request per job) |
| `companyName` | no (token only) | yes | no (token only) | yes | yes |

Measured fill rates from a live run at build time (share of delivered jobs where the field was populated):

| Field populated | Lever | Greenhouse | Ashby | Workable | SmartRecruiters |
|---|:-:|:-:|:-:|:-:|:-:|
| `companyName` | 0% | 100% | 0% | 100% | 100% |
| `departments` | 100% | 100% | 100% | 100% | 76% |
| `team` | 100% | 0% | 100% | 57% | 100% |
| `location` | 100% | 100% | 100% | 100% | 100% |
| `remote` | 100% | 8% | 100% | 100% | 100% |
| `employmentType` | 100% | 0% | 100% | 100% | 100% |
| `compensation` | 0% | 0% | 100% | 0% | 0% |
| `publishedAt` | 100% | 100% | 100% | 100% | 100% |
| `description` | 100% | 100% | 100% | 100% | 100% |

### Input reference

| Input | What it does |
|---|---|
| Companies / careers pages | One entry per company: careers URL, `provider:token`, or bare token. Case, `http/https`, `www.` and trailing paths do not matter. EU-hosted Greenhouse and Lever boards are detected from `eu.` URLs. SmartRecruiters identifiers are case-sensitive. |
| ATS for bare company names | Which platform a plain token belongs to. Preset to Lever in this listing. URLs always win. |
| Include full job descriptions | Adds `descriptionHtml` and `descriptionText`. SmartRecruiters needs one extra request per job for this, which is why it is priced separately. |
| Must contain / Must not contain / Fields to check | Case-insensitive keyword filters over title, departments, team, location and (Greenhouse, Lever, Ashby, Workable) the description text. |
| Locations | Substring match over the job's location, every listed office and the country, e.g. `London`, `Remote`, `United States`. |
| Remote jobs only | Keeps jobs the ATS explicitly marks remote. Greenhouse has no remote flag, so Greenhouse jobs match only when the location text says "Remote". |
| Employment types | Substring match over the platform's employment type, e.g. `Full`, `Contract`, `Intern`. Greenhouse does not publish it. |
| Posted within / Posted after / Posted before | Exact window on `publishedAt` (the date the platform reports the job was first published). |
| Memory name | The key that makes repeat runs return only new jobs. Stored in a named key-value store in your account (`ats-jobs-memory-<name>`). |
| Also return already-known jobs | Re-sends known jobs flagged `isDuplicate: true` with `changeType` `DUPLICATE` or `UPDATED` (the posting changed since you last received it). Free. |
| Reset this memory first | Forgets everything under the memory name before running. |
| Max new jobs per company / total | Exact caps on new, charged jobs. Known duplicates do not count towards them. |

### Output

One item per job, plus `status` items when a company could not be read and one `run-summary` item at the end. Nested values (`departments`, `allLocations`, `compensation`) for developers; flat fields for spreadsheets.

| Field | Meaning |
|---|---|
| `provider`, `company`, `companyName` | Which ATS, the board token you gave, and the company name when the platform publishes it. |
| `jobId`, `url`, `applyUrl` | Stable id (always a string) and links. |
| `title`, `departments`, `team`, `location`, `allLocations`, `country` | The role and where it is. |
| `remote`, `workplaceType`, `employmentType`, `compensation` | Work arrangement and pay, exactly as the platform publishes them (`null` when it does not). |
| `publishedAt`, `updatedAt` | ISO timestamps from the platform. |
| `descriptionHtml`, `descriptionText` | Full description when requested. |
| `changeType`, `isDuplicate`, `firstSeenAt`, `lastSeenAt`, `contentHash`, `scrapedAt` | Dedup and provenance. `changeType` is `NEW`, `UPDATED` or `DUPLICATE`. |

Sample item from a live run:

```json
{
  "type": "job",
  "provider": "lever",
  "company": "spotify",
  "companyName": null,
  "jobId": "a0fa7da3-4c3c-4fa2-97bd-7d6eb01eb9e5",
  "title": "Android Engineer - Advertising",
  "departments": [
    "Engineering"
  ],
  "team": "Advertising R&D",
  "location": "New York, NY",
  "allLocations": [
    "New York, NY"
  ],
  "country": "US",
  "remote": true,
  "workplaceType": "remote",
  "employmentType": "Permanent",
  "compensation": null,
  "publishedAt": "2026-03-18T18:07:05.234Z",
  "updatedAt": null,
  "url": "https://jobs.lever.co/spotify/a0fa7da3-4c3c-4fa2-97bd-7d6eb01eb9e5",
  "applyUrl": "https://jobs.lever.co/spotify/a0fa7da3-4c3c-4fa2-97bd-7d6eb01eb9e5/apply",
  "descriptionHtml": "<div>Our mission on the Advertising Product &amp; Technology team is to build a next generation advertising platform that aligns with our unique value propositi...",
  "descriptionText": "Our mission on the Advertising Product & Technology team is to build a next generation advertising platform that aligns with our unique value proposition for audio and video. We work to scale the user...",
  "changeType": "NEW",
  "isDuplicate": false,
  "firstSeenAt": "2026-08-31T11:22:17.996Z",
  "lastSeenAt": "2026-08-31T11:22:17.996Z",
  "contentHash": "6fa234beaf46f1e87a0a57372644c37ccc1e7b1b",
  "scrapedAt": "2026-08-31T11:22:17.996Z"
}
```

The `run-summary` item reports per board: `jobsOnBoard`, `matchedFilters`, `knownFromMemory`, `deliveredNew`, `deliveredDuplicates`, `withDescription`, plus totals, `filteredOut` counts, `charges`, `fillRates` and a plain-language `hint` whenever a run delivers nothing.

### Using the API

Same input as the form.

```bash
curl -X POST "https://api.apify.com/v2/acts/automation_craft~lever-jobs-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "companies": ["https://jobs.lever.co/spotify"],
    "includeDescription": true,
    "postedWithinDays": "30",
    "dedupMemoryName": "lever-watch",
    "maxJobsTotal": 500
  }'
```

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('automation_craft/lever-jobs-scraper').call({
  companies: ['https://jobs.lever.co/spotify'],
  includeKeywords: ['engineer'],
  keywordFields: ['title'],
  dedupMemoryName: 'engineering-watch',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const jobs = items.filter((i) => i.type === 'job');
```

```python
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("automation_craft/lever-jobs-scraper").call(run_input={
    "companies": ["https://jobs.lever.co/spotify"], "remoteOnly": True, "maxJobsTotal": 200})
jobs = [i for i in client.dataset(run["defaultDatasetId"]).iterate_items() if i.get("type") == "job"]
```

Scheduling: create a Schedule in Apify (daily or weekly) with a memory name set; each run then delivers only the jobs that appeared since the previous run, and a run that finds nothing new costs nothing.

### What this Actor does NOT do

- It does not scrape job boards such as LinkedIn, Indeed or Naukri. It reads **company career boards** hosted on Greenhouse, Lever, Ashby, Workable and SmartRecruiters.
- It does not discover companies for you. You supply the careers pages; it gets every open job they list.
- It does not return fields the platform does not publish (see the coverage table), applicant data, or anything behind a login.
- It cannot see unlisted or internal-only postings.

### Fair use

This Actor reads the same public, unauthenticated job-board endpoints the companies' own careers pages use, at a polite request rate. It collects no personal data and never logs in. You are responsible for complying with applicable laws and the target sites' terms in your jurisdiction and use case.

### Support

Something missing or wrong? Open an issue on the Actor's Issues tab. Fixes usually ship within a day.

### More job data tools by Automation Craft

- [ATS Jobs Scraper & API](https://apify.com/automation_craft/ats-jobs-scraper)
- [Greenhouse Jobs Scraper & API](https://apify.com/automation_craft/greenhouse-jobs-scraper)
- [Ashby Jobs Scraper & API](https://apify.com/automation_craft/ashby-jobs-scraper)
- [Workable Jobs Scraper & API](https://apify.com/automation_craft/workable-jobs-scraper)
- [SmartRecruiters Jobs Scraper & API](https://apify.com/automation_craft/smartrecruiters-jobs-scraper)
- [LinkedIn Jobs Scraper](https://apify.com/automation_craft/linkedin-jobs-scraper): LinkedIn public job search, no login, real posted dates.
- [SEEK Job Scraper](https://apify.com/automation_craft/seek-job-scraper): Australia and New Zealand jobs, deduplicated, pay once per job.

# Actor input Schema

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

One entry per company. Paste the Lever careers URL (jobs.lever.co/<company>, jobs.eu.lever.co/<company>), or just the company token (the site token is the last part of the careers URL, lower case), e.g. "spotify". Greenhouse, Lever, Ashby, Workable and SmartRecruiters URLs are all recognised, so you can mix boards from other ATS platforms in the same run.

## `provider` (type: `string`):

Which ATS a plain company token (no URL) belongs to. Preset to Lever for this listing. URLs and provider:token entries always win.

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

Attach the full job description (HTML and plain text) to every new job. $0.50 per 1,000 jobs that actually got one. SmartRecruiters needs one extra request per job for this.

## `includeKeywords` (type: `array`):

Keep only jobs that contain at least one of these words or phrases (case-insensitive) in the fields chosen below.

## `excludeKeywords` (type: `array`):

Drop jobs that contain any of these words or phrases.

## `keywordFields` (type: `array`):

Where the keyword filters look. Description text is available for Greenhouse, Lever, Ashby and Workable at filter time; SmartRecruiters descriptions are fetched after selection, so keyword filters there check the other fields.

## `searchText` (type: `string`):

Single keyword over title, department, team and location (kept for API compatibility; "Must contain" is more flexible).

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

Keep only jobs whose location (any listed office or country) contains one of these, e.g. "London", "Remote", "United States". Case-insensitive.

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

Keep only jobs the ATS explicitly marks as remote. Greenhouse has no remote flag, so Greenhouse jobs only match when their location text says "Remote".

## `employmentTypes` (type: `array`):

Keep only jobs whose employment type contains one of these, e.g. "Full", "Part", "Contract", "Intern". Greenhouse does not publish this field.

## `postedWithinDays` (type: `string`):

Only jobs first published in this window (exact, based on the posting date the ATS publishes). Any number of days works through the API.

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

Optional exact start date/time (ISO 8601). Overrides "Posted within".

## `postedBefore` (type: `string`):

Optional exact end date/time (ISO 8601).

## `dedupMemoryName` (type: `string`):

Give this search a name, e.g. "competitor-watch-weekly". Jobs delivered under that name are remembered in your account and never delivered or charged again in later runs. Leave empty to only remove duplicates within one run.

## `emitDuplicates` (type: `boolean`):

Return jobs the memory already knows, marked isDuplicate = true and changeType = DUPLICATE or UPDATED (content changed since you last received it). Never charged.

## `resetDedupMemory` (type: `boolean`):

Forget everything stored under the memory name before running.

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

Cap on new, charged jobs per careers page. Honored exactly. Known duplicates do not count.

## `maxJobsTotal` (type: `integer`):

Overall cap on new, charged jobs for the run. Honored exactly.

## Actor input object example

```json
{
  "companies": [
    "https://jobs.lever.co/spotify",
    "https://jobs.lever.co/palantir"
  ],
  "provider": "lever",
  "keywordFields": [
    "title",
    "departments",
    "team",
    "location"
  ],
  "remoteOnly": false,
  "postedWithinDays": "any",
  "emitDuplicates": false,
  "resetDedupMemory": false,
  "maxJobsPerCompany": 50,
  "maxJobsTotal": 100
}
```

# Actor output Schema

## `jobs` (type: `string`):

No description

# 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": [
        "https://jobs.lever.co/spotify",
        "https://jobs.lever.co/palantir"
    ],
    "includeDescription": false,
    "keywordFields": [
        "title",
        "departments",
        "team",
        "location"
    ],
    "postedWithinDays": "any",
    "dedupMemoryName": "",
    "maxJobsPerCompany": 50,
    "maxJobsTotal": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation_craft/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": [
        "https://jobs.lever.co/spotify",
        "https://jobs.lever.co/palantir",
    ],
    "includeDescription": False,
    "keywordFields": [
        "title",
        "departments",
        "team",
        "location",
    ],
    "postedWithinDays": "any",
    "dedupMemoryName": "",
    "maxJobsPerCompany": 50,
    "maxJobsTotal": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("automation_craft/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": [
    "https://jobs.lever.co/spotify",
    "https://jobs.lever.co/palantir"
  ],
  "includeDescription": false,
  "keywordFields": [
    "title",
    "departments",
    "team",
    "location"
  ],
  "postedWithinDays": "any",
  "dedupMemoryName": "",
  "maxJobsPerCompany": 50,
  "maxJobsTotal": 100
}' |
apify call automation_craft/lever-jobs-scraper --silent --output-dataset

```

## MCP server setup

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