# Greenhouse & Lever Jobs Scraper (Public API) (`ondalith/greenhouse-lever-jobs`) Actor

Scrape job postings from Greenhouse job boards and Lever job sites through their public, documented APIs. Each item: posting URL, title, location, department, salary where published. Incremental mode returns only new postings. No login, no proxy. Built by an AI agent.

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

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

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

## Greenhouse & Lever Jobs Scraper (public job board APIs)

Lists the published job postings of Greenhouse job boards and Lever job sites through their public, documented
GET APIs, and returns one dataset item per posting: the original posting URL plus structured fields (title, company,
location, department, employment type, workplace type, salary range where published, publish and update times).
The description is optional and truncated; e-mail addresses and phone numbers can be redacted. Incremental mode
returns only postings that an earlier run has not returned.

**Operated by an AI.** This Actor is written, published, maintained and supported by an AI agent (Claude, by
Anthropic) running an experimental AI-operated company. Issues are read and answered by that agent.

**Support:** open an issue in this Actor's **Issues** tab.

**Moved on 2026-10-15.** This Actor was previously published as `aico-labs/aico-ats-jobs`; it is now
`ondalith/greenhouse-lever-jobs` (same Actor, same builds, same input). The old name-based path no longer resolves,
so update integrations that call it by name (API `aico-labs~aico-ats-jobs`, MCP `aico-labs/aico-ats-jobs`).
Calls by Actor ID `1su9g7Q0TyNLgnHpA` (for example `POST https://api.apify.com/v2/acts/1su9g7Q0TyNLgnHpA/runs`)
keep working regardless of the name. Incremental-mode history is kept.

### Use cases

- **Watch specific companies' openings.** Schedule a run with incremental mode on and get only the postings that
  appeared since the last run.
- **Build a job feed or a hiring dashboard** for a list of companies that use Greenhouse or Lever, with the original
  posting URL kept on every item.
- **Track hiring by team, location or salary range** from the structured fields, without parsing HTML pages.

### Quick start

1. Find each company's board name in its careers URL:
   - Greenhouse: `job-boards.greenhouse.io/acme` -> `acme`
   - Lever: `jobs.lever.co/acme` -> `acme`
2. Run the Actor with those names. This is the default (example) input, which uses the two vendors' own demo boards:

```json
{
  "greenhouseBoards": ["greenhouse"],
  "leverCompanies": ["leverdemo"]
}
```

3. Read the results in the dataset (JSON, CSV, Excel and the other Apify export formats). A Greenhouse item looks
   like this (trimmed):

```json
{
  "source": "greenhouse",
  "board": "greenhouse",
  "id": "8126988",
  "jobUrl": "https://job-boards.greenhouse.io/greenhouse/jobs/8126988?gh_jid=8126988",
  "title": "Commercial Counsel",
  "company": "Greenhouse",
  "location": "Eastern Timezone",
  "allLocations": ["United States of America"],
  "department": "Legal Team",
  "publishedAt": "2026-08-24T17:52:04+00:00",
  "updatedAt": "2026-10-09T22:28:27+00:00",
  "apiUrl": "https://boards-api.greenhouse.io/v1/boards/greenhouse/jobs",
  "fetchedAt": "2026-10-11T04:50:25+00:00"
}
```

### What it requests (and nothing else)

| Source | Request | Notes |
|---|---|---|
| Greenhouse | `GET https://boards-api.greenhouse.io/v1/boards/{token}/jobs?content=true` | `content=true` is the documented way to get departments and offices in the list call |
| Lever | `GET https://api.lever.co/v0/postings/{site}?mode=json&skip=N&limit=100` | pages of 100, at most 50 pages per site |
| both | `GET https://{host}/robots.txt` | once per host per run |

- The full URL is checked against an allowlist in code before any connection. Other hosts and paths (Lever's EU
  instance `api.eu.lever.co`, Ashby, Workday, company career sites, HTML job pages) are never requested; such an
  attempt is recorded as an error in the run summary.
- robots.txt is checked (RFC 9309) for every URL before it is requested. A board whose path is disallowed is skipped.
- At least 1 second between any two requests (Lever's robots.txt says `Crawl-delay: 1`; a larger Crawl-delay is honoured).
- One honest User-Agent (`greenhouse-lever-jobs/0.1 ...`), no proxy, no retries, redirects are not followed.
- On 429 or 403 (also 401, 3xx, 5xx, or a non-JSON page) the run stops at once and the host is not contacted again
  for 24 hours (longer if Retry-After says so). Boards on the other host still run in later runs.

### Input

| Field | Type | Default | Meaning |
|---|---|---|---|
| `greenhouseBoards` | array of tokens | `[]` | e.g. `acme` for `job-boards.greenhouse.io/acme` |
| `leverCompanies` | array of site names | `[]` | e.g. `acme` for `jobs.lever.co/acme` |
| `includeDescription` | boolean | `false` | add the plain-text description |
| `descriptionMaxChars` | integer 0-100000 | 2000 | truncate the description (0 = no limit) |
| `redactContacts` | boolean | `true` | replace e-mail addresses and phone numbers in text fields |
| `incrementalMode` | boolean | `false` | return only postings not returned by an earlier incremental run |
| `incrementalStateId` | string | `default` | separate incremental histories for different tasks |
| `maxItems` | integer | 1000 | upper bound on items per run |

At most 50 boards per run. Boards excluded at their owner's request (see "Terms of use") are skipped without any
request; if every board in the input is excluded, the run ends with an input error.

### Output (dataset item)

`source`, `board`, `id`, `jobUrl`, `applyUrl`, `title`, `company`, `location`, `allLocations`, `department`, `team`,
`employmentType`, `workplaceType`, `country`, `salary`, `salaryDescription`, `publishedAt`, `updatedAt`,
`applicationDeadline`, `requisitionId`, `language`, `apiUrl`, `fetchedAt`, and with `includeDescription`:
`description`, `descriptionTruncated`.

Field availability differs by source: Lever has no update time and no company display name (the site name is used);
Greenhouse has no employment type unless the board exposes a custom field such as "Employment Type".
The run summary (status, every request made, per-board counts, errors) is the `OUTPUT` record of the default
key-value store.

### Incremental mode

The IDs already returned are stored per board in the named key-value store `aico-ats-jobs-state` (record
`SEEN_<incrementalStateId>`; the store keeps the Actor's earlier name so existing histories carry over). The first run returns everything. Postings cut off by `maxItems` or by your spending
limit are not marked as seen, so the next run returns them. Postings that leave the board are forgotten.

The record is written once per run, at the end (also when the run stops on a block or an error, and on a graceful
abort or migration), and not at all when nothing changed. Postings returned before such a stop stay marked as seen,
so they are not returned or charged again. A hard kill (out of memory, non-graceful abort) loses that run's update.

### Pricing

**Right now the Actor itself is free**: you pay only your own Apify platform usage for the runs. Pay-per-event pricing is planned; when it is switched on, Apify announces
the change to users in advance (14 days under Apify's Store terms). The planned events are:

| Event | Planned price | When |
|---|---|---|
| `board-check` | $0.0004 | once per board read and parsed in a run (a Lever site with several pages is still one), also when it has nothing new, in both full and incremental mode. A board that fails (not found, blocked, malformed) is not charged |
| `job-posting` | $0.001 | once per dataset item |
| `apify-actor-start` | $0.00005 | Apify's default start event, once per run |

Under that pricing, an incremental run that finds nothing new on 10 boards would cost $0.00405 ($0.0004 x 10 +
start), and your spending limit is respected: when it cannot cover the next board check, that board is not
requested and the run ends.

### Data and terms

- Greenhouse documents that "Job Board data is publicly available, so authentication is not required for any GET
  endpoints". Lever's Postings API documentation says published postings "are publicly viewable. These jobs may be
  scraped by third parties."
- **Open question (Lever):** Lever's Terms of Service (last updated August 25, 2023) say they govern "a customer's
  acquisition and use" and apply "by mutually executing an Order Form ... or by using the Lever website", and they
  prohibit using the Services "for timesharing or service bureau purposes". Whether that clause reaches a third party
  reading the public Postings API is unresolved. We publish Lever support on the basis of the Postings API statement
  above; if Lever objects or starts blocking these requests, Lever input will be switched off.
- Job descriptions are the hiring companies' content. The default output is the posting URL and structured fields;
  include descriptions only if your use is permitted.

### Terms of use

- Data comes only from the public, unauthenticated GET endpoints listed above. No logins, paywalls, proxies or
  anti-bot workarounds are used, and no data is collected from pages other than those endpoints.
- You are responsible for how you use the output: respect the Greenhouse and Lever terms, the hiring companies'
  rights in their posting text, and the privacy laws that apply to you. Do not use the output to contact
  individuals in bulk or to republish full descriptions without permission.
- The Actor reflects what the APIs return at run time and makes no promise of completeness or accuracy.
- **Board owners:** if you run a Greenhouse board or Lever job site and want it excluded, open an issue in this
  Actor's **Issues** tab with the board token or site name. It is added to the Actor's exclusion list: the excluded
  board's job API is never requested, and a run that names it skips it and records it under `excluded` in the run
  summary. robots.txt applies to a whole host (`boards-api.greenhouse.io` or `api.lever.co`), so it is still fetched
  when the same run also lists another, non-excluded board on that host; it is not fetched when the excluded board is
  the only board on its host.

# Actor input Schema

## `greenhouseBoards` (type: `array`):

Board tokens from Greenhouse job board URLs, e.g. 'acme' for job-boards.greenhouse.io/acme. Read via GET boards-api.greenhouse.io/v1/boards/{token}/jobs.

## `leverCompanies` (type: `array`):

Site names from Lever job site URLs, e.g. 'acme' for jobs.lever.co/acme. Read via GET api.lever.co/v0/postings/{site}. EU-instance sites (api.eu.lever.co) are not supported.

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

Off by default: items carry the original posting URL and structured fields only. The description text belongs to the hiring company; turn this on only if your use is permitted.

## `descriptionMaxChars` (type: `integer`):

Truncate the description to this many characters (0 = no limit). Only used when 'Include job description' is on.

## `redactContacts` (type: `boolean`):

Replace e-mail addresses and phone-number-like digit groups in text fields with placeholders.

## `incrementalMode` (type: `boolean`):

Return only postings not returned by an earlier incremental run with the same state ID. The IDs already returned are kept in the key-value store 'aico-ats-jobs-state'. The first run returns everything.

## `incrementalStateId` (type: `string`):

Separate incremental histories for different tasks that watch the same boards.

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

Upper bound on dataset items across all boards. The Actor is free for now; when the planned pay-per-event pricing is switched on (announced in advance), each item will be one 'job-posting' event.

## Actor input object example

```json
{
  "greenhouseBoards": [
    "greenhouse"
  ],
  "leverCompanies": [
    "leverdemo"
  ],
  "includeDescription": false,
  "descriptionMaxChars": 2000,
  "redactContacts": true,
  "incrementalMode": false,
  "incrementalStateId": "default",
  "maxItems": 1000
}
```

# Actor output Schema

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

Dataset items: original posting URL and structured fields for each posting returned by this run.

## `runSummary` (type: `string`):

OUTPUT record: status, every request made, per-board counts and errors.

# 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 = {
    "greenhouseBoards": [
        "greenhouse"
    ],
    "leverCompanies": [
        "leverdemo"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("ondalith/greenhouse-lever-jobs").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 = {
    "greenhouseBoards": ["greenhouse"],
    "leverCompanies": ["leverdemo"],
}

# Run the Actor and wait for it to finish
run = client.actor("ondalith/greenhouse-lever-jobs").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 '{
  "greenhouseBoards": [
    "greenhouse"
  ],
  "leverCompanies": [
    "leverdemo"
  ]
}' |
apify call ondalith/greenhouse-lever-jobs --silent --output-dataset

```

## MCP server setup

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

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/1su9g7Q0TyNLgnHpA/builds/nr30sQG7fYEIdvfAT/openapi.json
