# ATS Job Feed — Greenhouse, Lever & Ashby in one schema (`tokyo-cat/ats-job-feed`) Actor

Pull live job postings from Greenhouse, Lever and Ashby career boards and get them back in one normalized schema. Official public endpoints only — no browser, no proxy, no API key.

- **URL**: https://apify.com/tokyo-cat/ats-job-feed.md
- **Developed by:** [tokyo cat](https://apify.com/tokyo-cat) (community)
- **Categories:** Jobs, Lead generation, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 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.

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

## ATS Job Feed — Greenhouse, Lever & Ashby in one schema

Pull live job postings straight from company career boards and get them back in **one normalized schema**, whichever applicant tracking system the company uses.

Most job scrapers cover a single ATS and hand you that vendor's raw JSON. If you aggregate postings from more than one, you end up writing the mapping layer yourself — `categories.commitment` on Lever, `employmentType` on Ashby, nothing at all on Greenhouse. This Actor does that mapping for you.

- **Three ATS platforms** — Greenhouse, Lever, Ashby
- **Official public endpoints only** — no browser, no proxy, no API key, no login
- **One schema** — same 28 fields regardless of source
- **Filters run before billing** — you are never charged for a posting a filter removes

### What you get

Paste board URLs, get structured postings:

```json
{
  "ats": "ashby",
  "company": "ramp",
  "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "title": "Security Engineer, Cloud",
  "department": "Engineering",
  "team": "Backend",
  "employmentType": "Full-time",
  "location": "New York, NY (HQ)",
  "locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)", "Miami, FL"],
  "isRemote": true,
  "workplaceType": "Hybrid",
  "country": "USA",
  "publishedAt": "2026-04-07T17:12:35.753Z",
  "jobUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-...",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-.../application",
  "salaryText": "$211.4K - $290.6K",
  "salaryMin": 211400,
  "salaryMax": 290600,
  "salaryCurrency": "USD",
  "descriptionHtml": "…",
  "descriptionText": "…"
}
```

Every posting also carries `companySlug`, `requisitionId`, `departments`, `updatedAt`, `language`, `sourceUrl` and `scrapedAt`.

Timestamps are always ISO 8601 UTC. Missing values are `null`, never an empty string — so `salaryMin === null` means the company did not publish a range, not that parsing failed.

#### Field coverage by ATS

| Field | Greenhouse | Lever | Ashby |
|---|:--:|:--:|:--:|
| `title`, `location`, `jobUrl`, `applyUrl` | ✅ | ✅ | ✅ |
| `department` | ✅ | ✅ | ✅ |
| `team` | — | ✅ | ✅ |
| `employmentType` | — | ✅ | ✅ |
| `isRemote` | inferred from location | ✅ | ✅ |
| `workplaceType` | — | ✅ | ✅ |
| `country` | — | ✅ | ✅ |
| `publishedAt` | ✅ | ✅ | ✅ |
| `updatedAt` | ✅ | — | — |
| `salaryMin` / `salaryMax` | — | when published | when published |
| `descriptionHtml` / `descriptionText` | ✅ | ✅ | ✅ |

Dashes mark data the platform does not expose publicly. Nothing is guessed.

### Input

```json
{
  "boards": [
    "https://job-boards.greenhouse.io/stripe",
    "https://jobs.lever.co/palantir",
    "ashby:ramp"
  ],
  "titleKeywords": ["engineer"],
  "titleExcludeKeywords": ["manager", "director"],
  "remoteOnly": true,
  "postedAfter": "2026-07-01",
  "maxJobs": 500
}
```

| Option | What it does |
|---|---|
| `boards` | Board URLs or `ats:slug` pairs. Duplicates across forms are collapsed. |
| `includeDescription` | Full posting body as HTML and text. Default `true`. |
| `includeCompensation` | Posted salary ranges. Default `true`. |
| `titleKeywords` / `titleExcludeKeywords` | Case-insensitive title matching. |
| `locationKeywords` | Match any of the posting's locations, e.g. `Tokyo`, `Remote`. |
| `departmentKeywords` | Match department or team. |
| `remoteOnly` | Keep only postings the ATS flags remote. |
| `postedAfter` | ISO date. Postings without a publish date are kept. |
| `maxJobsPerBoard` / `maxJobs` | Hard caps on how much a run can charge. |
| `concurrency` | Boards fetched in parallel. Default `5`. |

#### Finding a board

The slug is the last part of the company's careers URL.

| ATS | Careers URL | `boards` entry |
|---|---|---|
| Greenhouse | `job-boards.greenhouse.io/stripe` | `greenhouse:stripe` |
| Lever | `jobs.lever.co/palantir` | `lever:palantir` |
| Ashby | `jobs.ashbyhq.com/ramp` | `ashby:ramp` |

You can paste the URL itself — the ATS is detected automatically.

### Pricing

You are charged **per job posting saved to the dataset**, plus the standard Actor start event.

Charging happens on the same call that writes the posting, so:

- Filtered-out postings cost nothing.
- A board that 404s or times out costs nothing.
- If a run is aborted, you keep — and pay for — only what was already saved.

`maxJobs` and `maxJobsPerBoard` put a ceiling on any single run, and the Actor stops cleanly when it hits the maximum run cost you set in Apify Console.

### Common uses

- **Job boards and aggregators** — refresh listings from a roster of companies on a schedule.
- **Recruiting and talent research** — track which teams a set of companies is hiring for, and when.
- **Compensation benchmarking** — collect published salary bands across Ashby and Lever boards.
- **Hiring-signal lead generation** — a company opening five sales roles is a company with budget.
- **AI agents and RAG pipelines** — a stable, flat schema that needs no per-vendor mapping.

### Notes and limits

- Only **publicly listed** postings are returned. Confidential and internal-only roles are not exposed by these endpoints and are not accessible to this Actor.
- No personal data is collected. Postings describe roles, not people.
- Greenhouse returns the entire board in one request; large boards (500+ postings) come back in a few seconds. Lever is the slowest of the three.
- Turning `includeDescription` off makes Greenhouse boards roughly 10x smaller. Lever and Ashby return descriptions either way.
- Boards that cannot be read are recorded in the `BOARD_ERRORS` record in the run's key-value store, with the reason for each.

### Development

```bash
npm install
npm test          # unit tests, no network
apify run         # end-to-end against live boards
```

Input for local runs goes in `storage/key_value_stores/default/INPUT.json`.

# Actor input Schema

## `boards` (type: `array`):

Job board URLs or `ats:slug` pairs. Accepted forms:

- `https://job-boards.greenhouse.io/stripe`
- `https://jobs.lever.co/palantir`
- `https://jobs.ashbyhq.com/ramp`
- `greenhouse:stripe`, `lever:palantir`, `ashby:ramp`

The ATS is detected from the URL automatically.

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

Fetch the full posting body as HTML and plain text. Turning this off makes Greenhouse boards roughly 10x smaller and faster; Lever and Ashby return descriptions in the same request either way.

## `includeCompensation` (type: `boolean`):

Extract posted salary ranges where the company publishes them (Ashby and Lever). Absent on most Greenhouse boards.

## `titleKeywords` (type: `array`):

Keep only postings whose title contains at least one of these (case-insensitive). Leave empty to keep all.

## `titleExcludeKeywords` (type: `array`):

Drop postings whose title contains any of these (case-insensitive).

## `locationKeywords` (type: `array`):

Keep only postings whose location matches one of these (case-insensitive substring), e.g. `Tokyo`, `Remote`, `Germany`.

## `departmentKeywords` (type: `array`):

Keep only postings in matching departments or teams, e.g. `Engineering`, `Sales`.

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

Keep only postings flagged remote by the ATS or containing `remote` in the location.

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

ISO date, e.g. `2026-07-01`. Keeps only postings first published on or after this date. Postings without a publish date are kept.

## `maxJobsPerBoard` (type: `integer`):

Cap on postings kept from each board, applied after filtering. Leave empty for no cap.

## `maxJobs` (type: `integer`):

Cap across all boards. The run stops cleanly once it is reached.

## `concurrency` (type: `integer`):

How many boards to fetch at once.

## Actor input object example

```json
{
  "boards": [
    "https://job-boards.greenhouse.io/stripe",
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "includeDescription": true,
  "includeCompensation": true,
  "titleKeywords": [],
  "titleExcludeKeywords": [],
  "locationKeywords": [],
  "departmentKeywords": [],
  "remoteOnly": false,
  "concurrency": 5
}
```

# Actor output Schema

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

Every posting saved by the run, normalized to the same 28 fields regardless of whether it came from Greenhouse, Lever or Ashby.

## `boardErrors` (type: `string`):

Boards that could not be read, each with the reason. Empty when every board succeeded.

# 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 = {
    "boards": [
        "https://job-boards.greenhouse.io/stripe",
        "https://jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/ramp"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("tokyo-cat/ats-job-feed").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 = { "boards": [
        "https://job-boards.greenhouse.io/stripe",
        "https://jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/ramp",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("tokyo-cat/ats-job-feed").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 '{
  "boards": [
    "https://job-boards.greenhouse.io/stripe",
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/ramp"
  ]
}' |
apify call tokyo-cat/ats-job-feed --silent --output-dataset

```

## MCP server setup

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

```

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/VIG4MKZJhaUHOULsa/builds/pihv9VDRIV18jlgns/openapi.json
