# Greenhouse & Lever ATS Job Postings Scraper (`skusol/greenhouse-lever-job-postings-scraper`) Actor

Scrape open roles from any company's Greenhouse or Lever job board through their public ATS API - no browser, no CSS selectors to break. Each posting returns as JSON with salary min/max, currency, seniority, years of experience, skills and remote status parsed by AI. First 10 items enriched free.

- **URL**: https://apify.com/skusol/greenhouse-lever-job-postings-scraper.md
- **Developed by:** [Kusol Sukhakul](https://apify.com/skusol) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 job posting extracteds

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

## Greenhouse & Lever Job Postings Scraper

Pulls open positions from any company's public Greenhouse or Lever job board
API and returns each one as structured JSON, with salary, seniority, skills
and remote status normalized by an AI step you can filter and aggregate.

### Try it free, no setup

Run it with the defaults. The first 10 postings come back with the full `ai`
block filled in, so you can see exactly what the AI step produces before
deciding whether it is worth anything to you. Postings past the tenth are
still returned in full — only the `ai` block is left empty, with `aiError`
saying why.

To enrich every posting in a run, put your own `talariaToken` in the input.
There is no signup wall on the demo and no card required to see the output.

### Sample output

One item, exactly as it lands in the dataset:

```json
{
  "url": "https://boards-api.greenhouse.io/v1/boards/discord/jobs/8659978002?content=true",
  "source": "company_ats",
  "scrapedAt": "2026-09-05T14:02:23+00:00",
  "ats": "greenhouse",
  "title": "Senior Full-Stack Software Engineer, Ads",
  "company": "Discord",
  "location": "San Francisco Bay Area",
  "description": "Discord has a highly engaged community of millions of daily active users... The US base salary range for this full-time position is $196,000 to $220,500 + equity + benefits. …",
  "jobId": "8659978002",
  "postingUrl": "https://job-boards.greenhouse.io/discord/jobs/8659978002",
  "postedAt": "2026-07-31T12:23:32-04:00",
  "ai": {
    "salary_min": 196000,
    "salary_max": 220500,
    "salary_currency": "USD",
    "salary_period": null,
    "seniority": "senior",
    "experience_years_min": 4,
    "skills": ["TypeScript", "React", "Python", "React Native", "iOS", "Android"],
    "remote_status": "onsite"
  }
}
```

Everything outside `ai` is read from the posting. Everything inside `ai` is
inferred from the posting text by the AI step. When enrichment is turned off,
fails, or times out, `ai` is `null`, an `aiError` field says why, and the
posting is returned anyway.

> Note: the whole object above, `ai` included, is verbatim from a live run
> against Discord's real, public Greenhouse board on 2026-09-05, with the
> description truncated for length — nothing here is fabricated or
> schema-shaped filler.

### How this is different from a browser-automation job scraper

This actor never renders or scrapes a career page's HTML. Greenhouse
(`boards-api.greenhouse.io`) and Lever (`api.lever.co`) are the same JSON
APIs those companies' own career pages call to display their listings —
public, undocumented-nowhere-near-obscure, no login, no anti-bot measures,
and no `robots.txt` restriction on the endpoints this actor uses. That
means: no rendering overhead, no brittle CSS selectors that break on a
redesign, and results as clean as the company's own listing page shows.

### Input

| Option | Type | Default | What it does |
|---|---|---|---|
| `startUrls` | array | Lever's public demo board | Company job-board API URLs — Greenhouse (`boards-api.greenhouse.io/v1/boards/<company>/jobs`) or Lever (`api.lever.co/v0/postings/<company>?mode=json`). |
| `maxItems` | integer | 100 | Hard cap on items returned. Never exceeded. Maximum 1000. |
| `enrich` | boolean | `true` | Turns the paid AI step on or off. |
| `enrichFields` | array | all fields | Restricts enrichment to named fields: `salary_min`, `salary_max`, `salary_currency`, `salary_period`, `seniority`, `experience_years_min`, `skills`, `remote_status`. |
| `talariaToken` | string (secret) | none | Bearer token for the enrichment service. Required when `enrich` is on. |
| `talariaBaseUrl` | string | `https://talaria.skusol.com` | Point the AI step at your own endpoint instead. |
| `requestIntervalSecs` | integer | 1 | Minimum seconds between two requests to the target API. |
| `maxRetries` | integer | 3 | Retries per page before it is skipped. |
| `respectRobotsTxt` | boolean | `true` | Stops the crawl if robots.txt disallows it. |

#### Finding a company's board URL

Most companies that use Greenhouse or Lever link their career page straight
at `boards.greenhouse.io/<company>` or `jobs.lever.co/<company>` — the
`<company>` slug there is the same one this actor's `startUrls` need. If a
career page is custom-branded, view its page source and search for
`greenhouse.io` or `lever.co`; the slug is in the embedded widget's URL.

### Use cases

1. **Compensation benchmarking.** `salary_min`, `salary_max` and
   `salary_currency` turn free-text pay ranges into numbers you can compare
   across companies, roles, and regions — without maintaining currency
   detection yourself.
2. **Hiring-signal lead generation.** A company opening several roles on a
   team is a buying signal for sales, recruiting, and market research. Run
   this against a watchlist of companies on a schedule to catch new postings.
3. **Skill-demand tracking.** Aggregate `skills` across postings from
   companies in a sector to see which tools and technologies are actually in
   demand, not which a survey says employers want.

### Pricing

Prices are set on the Apify Store listing; this table says what triggers each
event.

| Event | Charged when |
|---|---|
| `apify-actor-start` | Once, when a run starts. |
| `item-scraped` | Once per job posting after it has been written to the dataset. A posting that fails to parse is never pushed and never charged. |
| `ai-enriched-item` | Once per posting whose AI fields came back valid. Enrichment that fails, times out or is turned off is not charged. |

Two rules the actor holds to: nothing is charged before the thing it pays for
exists, and when a run reaches its maximum cost the run ends cleanly with
everything produced so far.

### Limitations

- **Coverage.** Only companies using Greenhouse or Lever's standard hosted
  job board. Companies on other ATS platforms (Workday, SmartRecruiters,
  Ashby, custom systems) aren't covered by this actor.
- **No board directory.** You supply each company's board URL yourself —
  this actor doesn't maintain or guess a list of which companies use which
  ATS.
- **Company name on Lever postings.** Lever's API doesn't include a
  company display name in the posting data itself, so `company` is derived
  from the board's URL slug (e.g. `acme-corp` → `Acme Corp`), which can
  differ slightly from the company's official name.
- **Freshness.** Each item is a snapshot at `scrapedAt`. There is no change
  detection or deduplication across runs; a posting scraped twice appears
  twice.
- **AI fields are inferred.** `ai` values come from a language model reading
  the posting. A field the posting does not state comes back `null` or
  `unknown` rather than a guess, but the values are not verified against any
  other source.
- **robots.txt.** With `respectRobotsTxt` on, a disallow ends the crawl
  instead of working around it.

### Development

```
make actor-setup   # once, needs the network
make build         # compileall and pytest
make actor-run     # local run in pay-per-event test mode
```

The test suite runs offline. It uses fake pages, a fake enrichment service,
and the Apify SDK's local pay-per-event mode, and asserts on charge counts
and ordering.

# Actor input Schema

## `startUrls` (type: `array`):

One or more company job-board API URLs from Greenhouse (https://boards-api.greenhouse.io/v1/boards/<company>/jobs) or Lever (https://api.lever.co/v0/postings/<company>?mode=json). Find a company's slug on its own /careers page - most embed one of these two.

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

Hard cap on how many job postings this run returns. Never exceeded.

## `enrich` (type: `boolean`):

Turn the paid AI step on or off. With it off, you get the scraped fields only and are never charged for enrichment.

## `enrichFields` (type: `array`):

Limit enrichment to these fields. Leave empty for all of them: salary\_min, salary\_max, salary\_currency, salary\_period, seniority, experience\_years\_min, skills, remote\_status.

## `talariaToken` (type: `string`):

Bearer token for the enrichment service. Leave this empty to try the free demo: the first 10 items of the run come back with the full AI block, the rest are returned without it. Supply a token to enrich every item.

## `talariaBaseUrl` (type: `string`):

Override only if you run your own enrichment endpoint.

## `requestIntervalSecs` (type: `integer`):

Minimum seconds between two requests to the target API.

## `maxRetries` (type: `integer`):

How many times a failed request is retried before it is skipped.

## `respectRobotsTxt` (type: `boolean`):

Stop the crawl if the target's robots.txt disallows it. Leave on.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://api.lever.co/v0/postings/leverdemo?mode=json"
    }
  ],
  "maxItems": 100,
  "enrich": true,
  "enrichFields": [],
  "talariaBaseUrl": "https://talaria.skusol.com",
  "requestIntervalSecs": 1,
  "maxRetries": 3,
  "respectRobotsTxt": true
}
```

# Actor output Schema

## `jobPostings` (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 = {
    "startUrls": [
        {
            "url": "https://api.lever.co/v0/postings/leverdemo?mode=json"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("skusol/greenhouse-lever-job-postings-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 = { "startUrls": [{ "url": "https://api.lever.co/v0/postings/leverdemo?mode=json" }] }

# Run the Actor and wait for it to finish
run = client.actor("skusol/greenhouse-lever-job-postings-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 '{
  "startUrls": [
    {
      "url": "https://api.lever.co/v0/postings/leverdemo?mode=json"
    }
  ]
}' |
apify call skusol/greenhouse-lever-job-postings-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,skusol/greenhouse-lever-job-postings-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/3NXMYEIC8tfZ2w5J8/builds/KsyQ6LLnC3Vr25dBi/openapi.json
