# ATS Job Feeds — Greenhouse, Lever & Ashby jobs (`aimojo/ats-job-feeds`) Actor

Collect job postings from companies' public Greenhouse, Lever and Ashby job boards into one normalised dataset, with an optional mode that returns only jobs that are new since the last run.

- **URL**: https://apify.com/aimojo/ats-job-feeds.md
- **Developed by:** [AI Mojo](https://apify.com/aimojo) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.40 / 1,000 job postings

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

### Greenhouse, Lever and Ashby jobs API — one normalised job feed

ATS Job Feeds collects job postings from any company's public Greenhouse, Lever or Ashby job board and returns them in one normalised format, so a Greenhouse job and a Lever job have the same fields. Turn on **Only new jobs since the last run** to get just the job postings that appeared since your previous run. To try it, press **Start**: the prefilled companies return results in under a minute.

### How to scrape Greenhouse, Lever and Ashby job postings

1. Find each company's job-board slug (see [How to find a company's slug](#how-to-find-a-companys-slug) below).
2. Add one line per company to **Companies**, written as `<ats>:<company-slug>`, for example `greenhouse:airbnb`, `lever:palantir` or `ashby:openai`.
3. Optionally set **Maximum jobs per company**, **Only new jobs since the last run** and **Include job descriptions**.
4. Press **Start** and wait for the run to finish. The status message shows how many companies and job postings are done.
5. Open the **Output** tab to view the job postings, then download them as JSON, CSV, Excel, XML or HTML.
6. To automate it, call the actor through the Apify API, or connect it to Make, Zapier, Google Sheets, Slack or a webhook with Apify integrations.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `companies` | array of strings | none (required) | Companies whose public job boards to collect, one per line, written as `<ats>:<company-slug>`. The ATS is greenhouse, lever or ashby. |
| `maxJobsPerCompany` | integer | `0` | The most job postings to return for each company. `0` means no limit. |
| `onlyNew` | boolean | `false` | When on, returns only job postings that were not seen in previous runs with the same company list. |
| `includeDescription` | boolean | `true` | When on, each job posting includes its full description as HTML and as plain text. Turn off for smaller, faster results. |

Example input (the prefilled values):

```json
{
  "companies": ["greenhouse:airbnb", "lever:palantir", "ashby:openai"],
  "maxJobsPerCompany": 5,
  "onlyNew": false,
  "includeDescription": true
}
```

#### How to find a company's slug

Open the company's careers page and follow a link to one of its jobs. The slug is the path segment straight after the job-board host:

- Greenhouse: `boards.greenhouse.io/<slug>` (for example `boards.greenhouse.io/airbnb` → `greenhouse:airbnb`)
- Lever: `jobs.lever.co/<slug>` (for example `jobs.lever.co/palantir` → `lever:palantir`)
- Ashby: `jobs.ashbyhq.com/<slug>` (for example `jobs.ashbyhq.com/openai` → `ashby:openai`)

### Output

Each dataset item is one job posting with the same fields, whichever job board it came from. Fields the job board does not give are `null`.

| Field | Type | Description |
|---|---|---|
| `id` | string | Stable job posting ID in the form `<ats>:<company-slug>:<native-id>`. It stays the same across runs. |
| `ats` | string | The applicant-tracking system (job board) the posting came from: greenhouse, lever or ashby. |
| `companySlug` | string | The company's job-board slug, as given in the companies input. |
| `title` | string | The job posting's title. |
| `url` | string | Link to the job posting on the company's public job board. |
| `companyName` | string or null | The company's display name. |
| `applyUrl` | string or null | Link to the job application form. |
| `locations` | array of strings | Every location listed for the job posting. Empty when the job board lists none. |
| `department` | string or null | The department the job belongs to. |
| `team` | string or null | The team the job belongs to. |
| `employmentType` | string or null | The employment type as the job board states it, for example full-time or contract. |
| `workplaceType` | string or null | Whether the job is remote, hybrid or onsite. |
| `publishedAt` | string or null | When the job posting was published (ISO 8601). |
| `updatedAt` | string or null | When the job posting was last updated (ISO 8601). |
| `compensation` | string or null | The pay range or compensation summary as the job board states it. |
| `descriptionHtml` | string or null | The full job description as HTML; null when descriptions are not included. |
| `descriptionText` | string or null | The full job description as plain text; null when descriptions are not included. |
| `scrapedAt` | string | When this run collected the job posting (ISO 8601). |

Example job posting (from an Ashby job board, descriptions shortened):

```json
{
  "id": "ashby:openai:dbd5ae8a-7c01-4f17-be39-b8caa8fd95d5",
  "ats": "ashby",
  "companySlug": "openai",
  "title": "Recruiting Coordinator - Contract",
  "url": "https://jobs.ashbyhq.com/openai/dbd5ae8a-7c01-4f17-be39-b8caa8fd95d5",
  "companyName": null,
  "applyUrl": "https://jobs.ashbyhq.com/openai/dbd5ae8a-7c01-4f17-be39-b8caa8fd95d5/application",
  "locations": ["San Francisco", "New York City", "Seattle"],
  "department": "People",
  "team": "Recruiting",
  "employmentType": "Contract",
  "workplaceType": "remote",
  "publishedAt": "2026-08-28T18:45:07.651Z",
  "updatedAt": null,
  "compensation": "$57 – $70 per hour",
  "descriptionHtml": "<p><strong>About the Role</strong></p><p>We’re seeking a Contract Recruiting Coordinator to join our team…</p>",
  "descriptionText": "About the Role\n\nWe’re seeking a Contract Recruiting Coordinator to join our team…",
  "scrapedAt": "2026-10-11T12:00:00.000Z"
}
```

The run also saves a short summary (job postings per company, and which companies failed) in the **Run summary** output.

### Only new postings

With **Only new jobs since the last run** (`onlyNew`) turned on, the actor remembers the IDs of the job postings it has seen (up to the most recent 50,000) and returns only the ones it has not seen before. The memory is kept per exact company list: the same companies (in any order) share it, and a different list starts fresh. It is stored in a named key-value store in your own Apify account, so you can view or delete it at any time. The first run with a new company list returns every current job posting. Every fetched posting is remembered, including ones left out by **Maximum jobs per company**, so later runs return only postings that appeared after that run. Postings left out because the run reached your maximum charge are not remembered, so the next run offers them again.

The memory holds at most 50,000 job posting IDs per company list. When it is full, the IDs that were first seen longest ago are dropped first, even if those postings were fetched again in recent runs. If a dropped posting is still listed on the job board, a later run treats it as new and delivers it again, and you are charged for it again. This can happen only when a posting stays listed while more than 50,000 newer posting IDs (first seen after it) are seen for the same company list; if your list is that large, split it into smaller lists.

Tip: create an Apify Schedule that runs the actor daily with `onlyNew` on, and you get a daily feed of new job postings.

### Pricing

This actor uses pay-per-event pricing: you pay for each job posting delivered to your dataset, and nothing for pages or requests.

#### How much does it cost to get 1,000 job postings?

$2.00 per 1,000 job postings ($0.002 per job posting), lower on paid Apify plans.

- You're charged only for job postings delivered to your dataset. With `onlyNew` on, postings the actor still remembers are not delivered or charged again (see the 50,000-ID memory limit under **Only new postings**).
- The run stops when it reaches your **maximum charge per run** setting, so you never pay more than you set.
- Apify adds a small fee for each run start.

### Use cases

- **Job aggregators and job boards:** fill a niche job board from hundreds of company career pages in one format.
- **Recruiting and sourcing:** watch target companies for new openings in your specialty.
- **Hiring-signal sales intelligence:** spot companies that are hiring for roles that need your product.
- **Market research:** track hiring trends by company, department, location or workplace type.

### Limits & compliance

- Uses only the public job-board endpoints that Greenhouse, Lever and Ashby publish for third-party use.
- Sends at most 1 request per second to each vendor, and identifies itself with its own user agent.
- Needs no login, API key or account on any job board.
- Collects no personal data beyond what employers publish in their job postings.

### FAQ

#### Is it legal to collect job postings from Greenhouse, Lever and Ashby?

The actor reads only job postings that employers publish on public job boards, through endpoints the vendors provide for third-party use, and collects no personal data beyond what is in those postings. You are still responsible for how you use the data; if you are unsure, ask a lawyer about your use case.

#### What happens if a company slug is wrong or unknown?

The company is skipped and a message is written to the run log. The other companies still run.

#### What if a company changed its ATS?

Its old board returns no job postings and is logged. Find the new job board as described in [How to find a company's slug](#how-to-find-a-companys-slug) and update the entry, for example from `lever:acme` to `ashby:acme`.

#### How fresh is the data?

Every run reads the job boards live, so the job postings are as current as the company's board at the time of the run.

#### Where can I report an issue or request a feature?

Open the **Issues** tab on this actor's page and describe the company entries and what you expected.

# Changelog

This Actor's version history is a separate document: https://apify.com/aimojo/ats-job-feeds/changelog.md

# Actor input Schema

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

Companies whose public job boards to collect, one per line, written as "<ats>:<company-slug>". The ATS is greenhouse, lever or ashby; the slug is the company's job-board name, for example greenhouse:airbnb for boards.greenhouse.io/airbnb.

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

The most job postings to return for each company. 0 means no limit.

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

When on, returns only job postings that were not seen in previous runs with the same company list.

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

When on, each job posting includes its full description as HTML and as plain text. Turn off for smaller, faster results.

## Actor input object example

```json
{
  "companies": [
    "greenhouse:airbnb",
    "lever:palantir",
    "ashby:openai"
  ],
  "maxJobsPerCompany": 5,
  "onlyNew": false,
  "includeDescription": true
}
```

# Actor output Schema

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

No description

## `summary` (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": [
        "greenhouse:airbnb",
        "lever:palantir",
        "ashby:openai"
    ],
    "maxJobsPerCompany": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("aimojo/ats-job-feeds").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:airbnb",
        "lever:palantir",
        "ashby:openai",
    ],
    "maxJobsPerCompany": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("aimojo/ats-job-feeds").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:airbnb",
    "lever:palantir",
    "ashby:openai"
  ],
  "maxJobsPerCompany": 5
}' |
apify call aimojo/ats-job-feeds --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,aimojo/ats-job-feeds"
        }
    }
}
```

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/2hdFrl3WyJbxEhKbX/builds/k38uUUbphA8yI4ucn/openapi.json
