# Greenhouse, Lever & Ashby Jobs Scraper: Full Descriptions (`ataism/ats-jobs`) Actor

Jobs from the public Greenhouse, Lever and Ashby boards of companies you list: full descriptions, salary ranges, filters, only-new alerts. $1 per 1,000 jobs.

- **URL**: https://apify.com/ataism/ats-jobs.md
- **Developed by:** [Kaizoku Data](https://apify.com/ataism) (community)
- **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 saveds

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?

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 & Ashby Jobs Scraper: Full Descriptions

Turn the careers pages of the companies **you pick** into one clean job dataset. Paste their Greenhouse, Lever or Ashby board links (or write `greenhouse:stripe`) and get every public posting with the full description, department, team, locations, remote flag, employment type, the salary range where the company publishes one, and the posting and apply links exactly as the board publishes them.

**Price: $1.00 per 1,000 jobs ($0.001 per job), full description included.** Boards that don't exist and entries that can't be read are free.

- **Three ATSs, one format:** the same fields for Greenhouse, Lever and Ashby, so one sheet or CRM column mapping covers all of them.
- **Only new jobs:** schedule a daily run that returns (and charges for) only roles you haven't seen, remembered per company for 120 days.
- **Filters:** keywords, locations, departments or teams, remote only, posted in the last N days, max jobs per company.
- **Emails and phone numbers removed** from postings. No applicant or candidate data, and no recruiter fields are read.
- **Quick start:** press *Start*. The prefilled input lists the public boards of Duolingo (Greenhouse), Linear (Ashby) and Spotify (Lever) and stops at 20 jobs: it finishes in seconds and costs about $0.02. No keys or accounts needed.

> **Public job postings only. You list the companies.** The Actor reads only the three vendors' public, unauthenticated job-board JSON endpoints, the same data each company already shows on its own careers page. No logins, no candidate or applicant data, no internal postings. **It never searches for, crawls or discovers companies or boards.**

### How to find a company's board link (1 minute)

1. Open the company's careers page and click any job.
2. Look at the address bar or the *Apply* button link. If it contains `boards.greenhouse.io/<name>`, `job-boards.greenhouse.io/<name>`, `jobs.lever.co/<name>` or `jobs.ashbyhq.com/<name>`, that `<name>` is the board.
3. Paste the link as it is, or write `greenhouse:<name>`, `lever:<name>` or `ashby:<name>`. A typo costs nothing: it shows up in the run summary as "not found".

If the careers page stays on the company's own domain and you can't see one of those addresses, the company may use another ATS, which this Actor doesn't read.

### What you can do with it

- **Watch the companies you care about.** Daily alert for new roles at 5 to 200 target employers (customers, competitors, investors' portfolio), with **Only new jobs** on.
- **Hiring-trend and salary research** across several companies and three ATS platforms in one dataset.
- **Feed your own internal tools** (CRM, Slack, sheets, recruiting research) with consistent fields, whatever ATS the company uses.

### Input

| Field | What it does |
|---|---|
| **Companies** | Required. A list of board URLs you paste, or `vendor:name`. See below. |
| **Keywords** | Keep jobs whose title or description contains any of these words (not case sensitive). |
| **Locations** | Keep jobs whose location text contains any of these, e.g. `Berlin`, `Germany`, `Remote`. |
| **Departments / teams** | Keep jobs whose department or team contains any of these. |
| **Remote jobs only** | Lever and Ashby say whether a job is remote. Greenhouse does not, so on Greenhouse a job counts as remote only when its location or title says "remote". |
| **Posted in the last N days** (1 or more) | Uses the vendor's date. Jobs without a date are left out when this is set. |
| **Max jobs per company** / **Max jobs in total** | Hard limits (default total 20, up to 10,000). Companies are read in the order you list them. |
| **Include description** | HTML plus plain text (on by default). |
| **Only new jobs** | Skips jobs you were already delivered, remembered per company for 120 days. |
| **Only-new memory name** | Use different names for separate histories. |
| **Mode** | `scrape` (normal) or `selfTest` (monitoring; delivers and charges nothing). |

#### Naming companies

Each entry is either a board URL you copy from the company's careers page, or `vendor:name`:

| Vendor | URL forms | Short form |
|---|---|---|
| Greenhouse | `boards.greenhouse.io/<name>`, `job-boards.greenhouse.io/<name>` | `greenhouse:stripe` |
| Lever | `jobs.lever.co/<name>`, `jobs.eu.lever.co/<name>` (EU) | `lever:spotify`, `lever-eu:<name>` |
| Ashby | `jobs.ashbyhq.com/<name>` | `ashby:linear` |

The Actor only reads the vendor and board name out of what you typed. Greenhouse boards hosted on `job-boards.eu.greenhouse.io` are rejected: Greenhouse's public Job Board API is served only from `boards-api.greenhouse.io` (checked 2026-10-18; no EU API host exists). A company's own domain (for example `careers.example.com`) cannot be read: open it, copy the `boards.greenhouse.io/...`, `jobs.lever.co/...` or `jobs.ashbyhq.com/...` address, or write `vendor:name`. For a bare `lever:name` it tries Lever's global host first and Lever's EU host only if the global one says the board does not exist.

Entries that cannot be understood, and boards that do not exist (HTTP 404), are listed in the run summary and **cost nothing**. If **no** board could be read at all (every entry 404, failed or invalid), the run **fails** and names the boards, so a typo is never mistaken for "no jobs". If `lever:name` and `lever-eu:name` turn out to be the same board, it is read and delivered once.

#### Example inputs

The company lists below are examples: replace them with the companies you follow.

**1. Daily alert: new remote data roles at your target companies** (schedule it daily; with *Only new jobs* each run returns only roles you haven't seen):

```json
{
  "companies": ["greenhouse:stripe", "https://jobs.lever.co/spotify", "ashby:linear"],
  "keywords": ["data engineer", "analytics"],
  "remoteOnly": true,
  "maxItems": 200,
  "onlyNew": true
}
```

**2. Hiring-trend snapshot: engineering roles at a shortlist, light rows** (no descriptions, at most 200 jobs per company):

```json
{
  "companies": ["greenhouse:anthropic", "ashby:openai", "greenhouse:vercel", "ashby:ramp"],
  "departments": ["Engineering"],
  "includeDescription": false,
  "maxItemsPerCompany": 200,
  "maxItems": 1000
}
```

**3. Roles in Europe posted this week** (location text match, last 7 days):

```json
{
  "companies": ["greenhouse:duolingo", "lever:spotify", "ashby:ramp", "greenhouse:stripe"],
  "locations": ["London", "Berlin", "Dublin", "Remote"],
  "postedSinceDays": 7,
  "maxItems": 300
}
```

The default input is three stable public boards, one per vendor (`greenhouse:duolingo`, `ashby:linear`, `lever:spotify`), with 20 jobs in total. Boards are read in order and later ones are only fetched if the earlier ones give fewer than 20 jobs, so it is normally one request and finishes in seconds, no keys or accounts needed.

### Output

One item per job, the same fields for all three vendors:

```json
{
  "vendor": "ashby",
  "companySlug": "ramp",
  "company": "ramp",
  "jobId": "8e1c0b7f-0000-4000-8000-000000000000",
  "title": "Senior Software Engineer",
  "department": "Engineering",
  "team": "Platform",
  "location": "New York City",
  "locations": ["New York City", "Remote (US)"],
  "remote": false,
  "workplaceType": "hybrid",
  "employmentType": "FullTime",
  "compensation": { "min": 211400, "max": 290600, "currency": "USD", "interval": "year", "summary": "$211.4K - $290.6K" },
  "publishedAt": "2026-09-01T14:00:00.000Z",
  "updatedAt": null,
  "applyUrl": "https://jobs.ashbyhq.com/ramp/8e1c0b7f-.../application",
  "jobUrl": "https://jobs.ashbyhq.com/ramp/8e1c0b7f-...",
  "description": "<p>…</p>",
  "descriptionText": "…",
  "source": "Ashby public job board",
  "retrievedAt": "2026-10-18T13:38:52.452Z"
}
```

Per-vendor notes (from our 2026-10-18 recording of 7 boards, 768 jobs):

| | Greenhouse | Lever | Ashby |
|---|---|---|---|
| Company name | Yes (`company_name`) | No: `company` repeats the board name | No: same |
| Department / team | Department only, `team` is null | Both (about 90% filled) | Both (100%) |
| Remote / workplace type | `remote` is true only if the location or title says "remote", else null. `workplaceType` is null | `workplaceType` and `remote` where the company sets it (about 20% of jobs); otherwise `remote` is null | Always |
| Employment type | Not provided (null) | `commitment`, e.g. Permanent (about 85%) | Always, e.g. FullTime |
| Salary range | Only when the company publishes pay ranges (`pay_input_ranges`) | Only when the posting has a `salaryRange` | Shown when the company enables pay display (78% on our sample) |
| Dates | `publishedAt` (first published) and `updatedAt` | `publishedAt` is Lever's creation date; no updated date | `publishedAt`; no updated date |
| Links | `jobUrl` and `applyUrl` are both the posting page the board publishes (which may be on the company's own domain) | Lever-hosted posting and apply pages | Ashby-hosted posting and apply pages |

- `compensation` is a published salary **range** only, or `null`. Most postings have none.
- `remote: null` means "unknown", not "no".
- Links are passed through exactly as the vendor published them.
- Every run also saves a `RUN_SUMMARY` record: per-company counts, boards not found, invalid entries, failures, requests and why the run stopped.

Export as JSON, CSV, Excel or XML, or read it through the Apify API.

### Pricing

Pay per event:

- **$0.001 per job** saved to the dataset.
- Apify's standard tiny start fee per run.

| Jobs delivered | Cost |
|---|---|
| 100 | $0.10 |
| 1,000 | $1.00 |
| 10,000 | $10.00 |

Examples: one company with 100 open roles, **$0.10**. 20 companies, about 1,500 roles in total, **$1.50** the first time; with **Only new jobs** a daily re-run typically returns a few new roles and costs cents. Companies you list that don't exist, invalid entries and jobs filtered out are free. Cap any run with Apify's *Max total charge*; the Actor stops at the cap and remembers what it delivered.

### Use it via API, n8n or Make

Get an API token in Apify Console → *Settings* → *API & Integrations*, and keep it in an environment variable (`APIFY_TOKEN` below). Never paste it into shared code.

**cURL** (starts a run, waits for it and returns the jobs as JSON; this synchronous endpoint waits up to 5 minutes, which fits lists of up to about 100 companies; for longer lists use the clients below):

```bash
curl -X POST "https://api.apify.com/v2/acts/ataism~ats-jobs/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"companies": ["greenhouse:stripe", "ashby:linear"], "keywords": ["engineer"], "maxItems": 50}'
```

**JavaScript** (`npm install apify-client`):

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('ataism/ats-jobs').call({
    companies: ['greenhouse:stripe', 'ashby:linear'],
    onlyNew: true,
    maxItems: 200,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const job of items) console.log(job.company, job.title, job.jobUrl);
```

**Python** (`pip install "apify-client>=3"`):

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("ataism/ats-jobs").call(run_input={
    "companies": ["greenhouse:stripe", "ashby:linear"],
    "onlyNew": True,
    "maxItems": 200,
})
for job in client.dataset(run.default_dataset_id).iterate_items():
    print(job["company"], job["title"], job["jobUrl"])
```

(With apify-client versions before 3.0, `call()` returns a dict: use `run["defaultDatasetId"]`.)

**n8n** (new jobs to Slack, a sheet or your CRM):

1. Add a *Schedule Trigger* (for example daily at 08:00).
2. Add the **Apify** node, operation **Run an Actor and get dataset**, Actor `ataism/ats-jobs`, and paste your input JSON with `"onlyNew": true`.
3. Add a *Slack*, *Google Sheets* or CRM node and map `company`, `title`, `location` and `jobUrl`.

**Make:**

1. Add the **Apify** module **Run an Actor**, choose `ataism/ats-jobs`, paste the input JSON and set *Run synchronously* to *Yes*.
2. Add the **Apify** module **Get Dataset Items** and map the run's default dataset ID.
3. Add your destination (Google Sheets *Bulk Add Rows*, Slack, CRM) and map the fields. Schedule the scenario daily.

You can also schedule the Actor directly in Apify Console (*Schedules*) and export each run as JSON, CSV or Excel.

### Vendor terms and how the Actor behaves

We read each vendor's public API docs, terms and robots.txt before building this (copies and verdicts are in the Actor's repository, `docs/terms/VERDICT.md`):

- **Greenhouse**: the Job Board API docs state "Job Board data is publicly available, so authentication is not required for any GET endpoints."
- **Lever**: the Postings API docs state "all job postings in the `published` state are publicly viewable. These jobs may be scraped by third parties." robots.txt asks for a 1-second crawl delay; the Actor keeps more than 1 second between Lever requests.
- **Ashby**: the public job-posting API is documented for companies' own careers pages and Ashby's terms say nothing about third-party reading of public postings, so we treat it as permitted but not explicitly licensed. If Ashby asks us to stop, we will.

The Actor makes **one request per company**, sequentially, with a polite pause, backs off on 429 and 5xx errors (honouring `Retry-After`, but giving up on a company after about 60 s of waiting and reporting it as failed), never follows redirects off the vendor hosts, and never tries to get around a block or CAPTCHA. You remain responsible for how you use the data and for respecting each company's and vendor's terms; job postings are the employers' content. We are not affiliated with Greenhouse, Lever or Ashby.

### Privacy

Job postings only, never applicant or candidate data. Recruiters sometimes put their email or phone number in a posting, so the Actor **removes email addresses and phone numbers** from titles, descriptions and pay text (you will see `[email removed]` or `[phone removed]`; generic addresses such as an accommodations mailbox are removed too). It reads no recruiter or hiring-manager fields, so no such names are output. If you spot personal data we missed, tell us on the Issues tab and we will fix it.

### Reliability

- Reads the vendors' JSON endpoints, not web pages, so careers-site redesigns don't break it.
- A daily self-test fetches three fixed public boards, one per vendor, and checks the output format against a recorded baseline. Each vendor has a spare board. It fails on real changes (a field removed or renamed, fewer valid rows, a lower fill rate) or when at least two of the three vendors cannot be read; one vendor's transient error or a removed board is only recorded as a notice, as is a new optional field. We fix breakages fast.
- **Only new jobs** saves its memory at least every 5 jobs. If a run is killed hard (for example an out-of-memory crash), up to 4 jobs delivered just before that may be delivered, and charged, again next run. Normal aborts and migrations save everything. Only jobs that were actually delivered and charged are remembered.
- None of the three endpoints paginates: one response holds the whole board. Greenhouse descriptions are always requested (`content=true`), even if you switch descriptions off, because without them the API also drops departments and offices. A very large board (hundreds of jobs with long descriptions) is a few MB, which the default 256 MB handles.
- Timing: about 1.1 s per Lever company and 0.5 s per other company. 100 companies take a couple of minutes; raise the run timeout for hundreds.

### FAQ

**Can it find which companies use Greenhouse, Lever or Ashby?** No, by design. You list the companies. It does not search, crawl or guess board names.

**Why was my company not found?** The board name is wrong, the company uses another ATS, or the board is private/closed. Check the careers page link. Not-found boards are listed in `RUN_SUMMARY.notFound` and are free; if none of your boards can be read, the run fails.

**Why are there fewer jobs than on the careers page?** Only public, listed postings are returned; internal and unlisted ones (Ashby `isListed: false`) are excluded.

**Does Max jobs count filtered-out jobs?** No. Only delivered jobs count.

**Can it apply to jobs or read applicants?** No. Read-only, public postings.

**Why did my run stop early?** The jobs ran out, a limit was reached, or *Max total charge* was hit. `RUN_SUMMARY.stoppedBy` says which.

**Something's wrong?** Open an issue on the Actor's Issues tab. We aim to reply within 2 working days.

# Actor input Schema

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

Career-board URLs you paste (boards.greenhouse.io/<name>, job-boards.greenhouse.io/<name>, jobs.lever.co/<name>, jobs.eu.lever.co/<name>, jobs.ashbyhq.com/<name>) or vendor:name, e.g. greenhouse:stripe, lever:spotify, lever-eu:<name>, ashby:linear. One request per company. Invalid entries and boards that do not exist are listed in the run summary and cost nothing. Greenhouse boards hosted on eu.greenhouse.io are not supported. To find a board: open any job on the company's careers page and copy the boards.greenhouse.io, jobs.lever.co or jobs.ashbyhq.com address.

## `keywords` (type: `array`):

Keep jobs whose title or description contains any of these words (not case sensitive). Leave empty for all.

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

Keep jobs whose location text contains any of these (not case sensitive), e.g. "Berlin", "Germany", "Remote".

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

Keep jobs whose department or team contains any of these (not case sensitive), e.g. "Engineering".

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

Lever and Ashby state whether a job is remote. Greenhouse does not, so for Greenhouse a job counts as remote only if its location or title says "remote".

## `postedSinceDays` (type: `integer`):

Keep jobs published within this many days. Uses each vendor's published/created date; jobs without a date are left out when this is set.

## `maxItemsPerCompany` (type: `integer`):

Stop taking jobs from a company after this many. Leave empty for no per-company limit.

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

Hard limit on delivered (and charged) jobs for the whole run. Companies are read in the order you list them, and later ones are only fetched if the limit is not reached yet.

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

Add the full description as HTML and as plain text. Switch off for lighter rows.

## `onlyNew` (type: `boolean`):

Skip jobs you were already delivered on an earlier run (remembered per company in a named store). Only jobs that were delivered and charged are remembered.

## `stateKey` (type: `string`):

Use a different name to keep separate only-new histories (e.g. one per project). Only used with Only new jobs.

## `mode` (type: `string`):

"scrape" is normal. "selfTest" fetches three fixed public boards, checks the output format against a stored baseline, saves SELF_TEST_REPORT, fails only on real changes, and delivers and charges nothing. Used for daily monitoring.

## Actor input object example

```json
{
  "companies": [
    "greenhouse:duolingo",
    "ashby:linear",
    "lever:spotify"
  ],
  "remoteOnly": false,
  "maxItems": 20,
  "includeDescription": true,
  "onlyNew": false,
  "stateKey": "default",
  "mode": "scrape"
}
```

# Actor output Schema

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

Public job postings from the companies you listed, one item per job (JSON, CSV, Excel or XML).

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

Per-company counts, boards not found, invalid entries and errors for this run.

# 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": [
        "greenhouse:duolingo",
        "ashby:linear",
        "lever:spotify"
    ],
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("ataism/ats-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 = {
    "companies": [
        "greenhouse:duolingo",
        "ashby:linear",
        "lever:spotify",
    ],
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("ataism/ats-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 '{
  "companies": [
    "greenhouse:duolingo",
    "ashby:linear",
    "lever:spotify"
  ],
  "maxItems": 20
}' |
apify call ataism/ats-jobs --silent --output-dataset

```

## MCP server setup

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