# Multi-ATS Jobs Feed: Greenhouse, Lever & Ashby Jobs API (`hardwoodlogic/multi-ats-jobs-feed`) Actor

One clean feed of job postings from public Greenhouse, Lever and Ashby boards. Salary when posted, remote flag, delta mode for new jobs only, 21-studio games preset. No personal data.

- **URL**: https://apify.com/hardwoodlogic/multi-ats-jobs-feed.md
- **Developed by:** [HardwoodLogic](https://apify.com/hardwoodlogic) (community)
- **Categories:** Jobs, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.50 / 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?

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

## Multi-ATS Jobs Feed: Greenhouse, Lever & Ashby Jobs API

### What it does

Multi-ATS Jobs Feed turns company job boards hosted on **Greenhouse, Lever and Ashby** into one normalized dataset. Paste board URLs, job URLs, `ats:slug` shortcuts, bare company slugs, or a company careers page that embeds one of these boards — the Actor detects the ATS, calls that ATS's public job-board API, and returns one row per open posting with the same fields regardless of source: company, title, department, team, employment type, locations, remote/hybrid/onsite, **salary range when the employer publishes it**, posted date and apply link.

No browser, no login, no proxies. Built-in retries with backoff, per-host rate limiting that respects each ATS's published crawl-delay, duplicate merging, and a **delta mode** that returns (and charges for) only postings you haven't seen before — ideal for scheduled runs.

### Who it's for

- **Recruiting tools & ATS integrators** — pull open roles from target companies into your CRM, sourcing tool or Slack alerts.
- **Job aggregators & niche job boards** — one schema across three ATSs, with stable posting IDs and apply links back to the source.
- **AI agents (via Apify MCP)** — a simple, well-documented input and a typed dataset make it easy for an agent to answer "what is Company X hiring for?" in one tool call.
- **Game-industry job seekers & studios watchers** — the one-click **Game studios** preset covers 21 studio boards (Riot Games, Epic Games, Roblox, Bungie, PlayStation, Naughty Dog, Rockstar Games, Kabam, thatgamecompany and more).

### Sources

| ATS | Public API used | Docs |
|---|---|---|
| Greenhouse | `GET https://boards-api.greenhouse.io/v1/boards/{board}/jobs?content=true&pay_transparency=true` (+ `/v1/boards/{board}` for the company name) | [Job Board API](https://developers.greenhouse.io/job-board.html) |
| Lever | `GET https://api.lever.co/v0/postings/{site}?mode=json` (EU boards: `api.eu.lever.co`) | [Lever Postings API](https://github.com/lever/postings-api) |
| Ashby | `GET https://api.ashbyhq.com/posting-api/job-board/{board}?includeCompensation=true` | [Ashby Job Postings API](https://developers.ashbyhq.com/docs/public-job-posting-api) |

Other ATSs (Workday, SmartRecruiters, Workable, Recruitee…) are reported as unsupported inputs and skipped. If you paste a company careers page, the Actor reads that one page to find an embedded Greenhouse/Lever/Ashby board.

**Game studios preset (21 boards):** Greenhouse — Riot Games, Epic Games, Roblox, Bungie, PlayStation (Sony Interactive Entertainment), Naughty Dog, Housemarque, Rockstar Games, 2K, Gearbox, Scopely, KRAFTON, Wildlife Studios, Outfit7, Nordeus · Lever — Kabam, Jam City, Avalanche Studios Group · Ashby — Voodoo, Moon Active, thatgamecompany. Roughly 1,400–1,450 open postings when verified on 2026-10-27.

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `sources` | list of strings | — (Console prefill: Riot Games, Kabam, thatgamecompany) | Board URLs, job URLs, `greenhouse:`/`lever:`/`ashby:` slugs, bare slugs (tried Greenhouse → Lever → Ashby), or careers pages. The three sample boards are used only when the input is completely empty; they are never added to a preset or `boards` run. |
| `preset` | `none` / `gameStudios` | `none` | Adds the 21 game-studio boards. |
| `boards` | JSON array | — | Advanced: `[{"ats":"greenhouse","slug":"riotgames","name":"Riot Games"}]`; `name` overrides the company name. |
| `keyword` | string | — | Comma-separated; keeps postings matching any term in title, department or team. |
| `excludeKeyword` | string | — | Comma-separated; drops postings matching any term. |
| `location` | string | — | Comma-separated plain-text match on location strings (not geographic: "Canada" won't match "Montreal"). |
| `remoteOnly` | boolean | `false` | Only fully remote roles (hybrid is not remote). |
| `postedSince` | string | — | A date (`2026-10-01`) or span (`7 days`, `2 weeks`, `1 month`). |
| `onlyNew` | boolean | `false` | Delta mode: return only postings not returned in earlier runs. |
| `stateKey` | string | `default` | Separate delta memory per schedule or client. |
| `includeDescription` | boolean | `false` | Adds a ≤500-character plain-text snippet with contact details scrubbed (best-effort). |
| `dedupeSimilar` | boolean | `true` | Merge identical re-posts of the same job. |
| `maxResults` | integer | `0` (no limit) | Stop after this many rows. |
| `maxConcurrencyPerHost` | integer 1–5 | `2` | Parallel requests per API host. |
| `minRequestIntervalMs` | integer 0–10,000 | `250` | Minimum gap between requests to one host. Lever hosts always wait at least 1,000 ms (Lever's robots.txt sets `Crawl-delay: 1`); a larger value raises that floor. |
| `maxRetries` | integer 0–10 | `4` | Retries on 429/5xx/timeouts (honors `Retry-After`). |
| `requestTimeoutSecs` | integer 5–120 | `30` | Per-request timeout. |
| `maxConcurrentBoards` | integer 1–20 | `6` | Boards fetched in parallel. |

### Example input

```json
{
  "preset": "gameStudios",
  "sources": ["https://jobs.lever.co/kabam", "ashby:thatgamecompany"],
  "keyword": "engineer, programmer",
  "remoteOnly": true,
  "postedSince": "30 days"
}
```

### Example output (one row)

```json
{
  "company": "Kabam",
  "ats": "lever",
  "boardSlug": "kabam",
  "sourceJobId": "6f9175f3-bbf9-4ab7-bdb7-bfbcf85274b4",
  "title": "Concepteur de jeux (Contrat)",
  "department": "Design",
  "team": "Currently Hiring",
  "employmentType": "Contractor",
  "location": "Montreal",
  "locations": ["Montreal"],
  "remote": false,
  "workplaceType": "hybrid",
  "salaryMin": 40,
  "salaryMax": 45,
  "salaryCurrency": "CAD",
  "salaryPeriod": "HOUR",
  "postedAt": "2026-10-02T18:21:15.180Z",
  "updatedAt": null,
  "applyUrl": "https://jobs.lever.co/kabam/6f9175f3-bbf9-4ab7-bdb7-bfbcf85274b4/apply",
  "postingUrl": "https://jobs.lever.co/kabam/6f9175f3-bbf9-4ab7-bdb7-bfbcf85274b4",
  "scrapedAt": "2026-10-27T16:14:05.498Z"
}
```

(Sample row from the Actor README (unchanged in v0.2.1); contains no personal data. `descriptionSnippet` appears only when `includeDescription` is on.)

The dataset has two views: **Overview** and **With salary**. Each run also writes a summary to the `OUTPUT` record (per-board status, totals, errors, HTTP stats). A wrong slug is reported and skipped; the run fails only if every board fails.

### FAQ

**Is this legal?**
The Actor only calls the public, unauthenticated job-board endpoints that Greenhouse, Lever and Ashby document for publishing jobs (Greenhouse: "Job Board data is publicly available, so authentication is not required for any GET endpoints"; Lever: "all job postings in the `published` state are publicly viewable. These jobs may be scraped by third parties"). It uses no logins, private APIs or access-control bypasses, never returns candidate data, and skips unlisted Ashby postings. Job content belongs to the employers: link back to `applyUrl`, and check that your use (for example republishing on a job board, or using text to train AI models) fits each employer's and ATS's terms and your local law. This is not legal advice.

**Does it collect personal data?**
No by design. It reads an allow-list of job fields and never outputs recruiter, hiring-manager or custom-metadata fields. Description snippets are off by default; when on, emails, phone numbers, handles, profile links, URLs and names after contact phrases are removed. That scrubbing is best-effort, so leave snippets off if you need a hard guarantee.

**What are the limits?**
Greenhouse, Lever and Ashby only; only postings the public board shows. Greenhouse has no structured remote flag (inferred from text) and publishes pay without a period (inferred as hourly or yearly for major currencies). Ashby `applyUrl` is the ATS-hosted page on `jobs.ashbyhq.com`, not the employer's own site. Location filtering is a plain text match. Careers-page detection reads one page (public http/https addresses only); boards loaded by JavaScript from another domain may be missed — paste the board URL instead. When an employer's own Greenhouse apply link is not https or does not load (currently Nordeus), `applyUrl` points to the Greenhouse-hosted application form instead. 256 MB handles the full preset; use 512 MB for boards with several thousand postings.

**Can I schedule it?**
Yes. Create an Apify Schedule (e.g. daily) and turn on **Only new postings**. Each run returns and charges only postings it hasn't returned before for that `stateKey`; closed postings drop out of memory automatically. Use a different `stateKey` per schedule or client.

**How do I integrate it?**
Use the Apify API or SDKs (JavaScript/Python) to start runs and read the dataset as JSON, CSV or Excel; connect webhooks, Make, Zapier, n8n, Google Sheets or Slack via Apify integrations; or let an AI agent call it as a tool through the Apify MCP server.

### Pricing

**$1.50 per 1,000 job postings** — you pay $0.0015 for each posting saved to your dataset. No subscription, no separate compute charges on standard runs. Postings are charged as they're saved, and the run stops cleanly at your **Max cost per run**, so you never pay more than you set.

**Example:** a run that returns 10,000 job postings costs 10,000 × $0.0015 = **$15.00**.

**Delta mode keeps schedules cheap:** with *Only new postings* on, repeat runs charge only for postings you haven't received before — unchanged postings cost nothing.

# Changelog

This Actor's version history is a separate document: https://apify.com/hardwoodlogic/multi-ats-jobs-feed/changelog.md

# Actor input Schema

## `sources` (type: `array`):

One per line. Accepts Greenhouse, Lever or Ashby board URLs (e.g. https://boards.greenhouse.io/riotgames, https://jobs.lever.co/kabam, https://jobs.ashbyhq.com/thatgamecompany), a job URL from one of those boards, an explicit "ats:slug" (greenhouse:riotgames, lever:jamcity, ashby:voodoo), a bare slug (we try Greenhouse, then Lever, then Ashby), or a company careers page that embeds one of these boards. A single string is accepted as one source. The prefill is a sample for the console only. It is not added when you send boards, a preset, or your own sources. A completely empty input still uses those three sample boards.

## `preset` (type: `string`):

Adds a curated list of boards. "Game studios" adds 21 game companies verified live on Greenhouse, Lever and Ashby (see README).

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

Explicit boards as JSON objects: \[{"ats": "greenhouse", "slug": "riotgames", "name": "Riot Games"}]. "name" is optional and overrides the company name in the output.

## `keyword` (type: `string`):

Comma-separated. Keeps postings whose title, department or team contains any of them (case-insensitive), e.g. "engineer, designer".

## `excludeKeyword` (type: `string`):

Comma-separated. Drops postings whose title contains any of them, e.g. "intern, contract".

## `location` (type: `string`):

Comma-separated plain text. Keeps postings where any location string contains any term (case-insensitive). This is not geographic: "Canada" does not match "Montreal", and "Remote" does not match a remote job whose location text does not contain that word.

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

Keep only postings whose workplace is remote. When the ATS gives a workplace type (remote, hybrid, onsite), that wins: a hybrid job is not remote. If the type is missing, fall back to the ATS remote flag, then to words like "remote" in the location.

## `postedSince` (type: `string`):

A date (2026-10-01) or a relative span (7 days, 2 weeks, 1 month). Leave empty for all open postings.

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

Skip postings already returned by a previous run with the same state key, so scheduled runs only pay for new jobs. State is kept in the named key-value store "multi-ats-jobs-feed-state".

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

Separate delta histories per feed (e.g. one per schedule). Only used with "Only new postings".

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

Adds a plain-text snippet (max 500 characters) of the job description. Emails, phone numbers, social links and contact names are removed on a best-effort basis. Off by default.

## `dedupeSimilar` (type: `boolean`):

Within one board, keep only one posting when several have the same title, department and location (common for multi-headcount requisitions). Turn off to get every requisition.

## `maxResults` (type: `integer`):

Stop after this many postings (0 = no limit). Your "Max cost per run" limit is always respected too.

## `maxConcurrencyPerHost` (type: `integer`):

Politeness limit per ATS API host.

## `minRequestIntervalMs` (type: `integer`):

Minimum delay between two requests to the same host. Greenhouse and Ashby use this value (default 250 ms); their robots.txt files set no crawl-delay. lever.co hosts wait at least 1000 ms because their robots.txt sets Crawl-delay: 1. A larger value raises that floor.

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

Retries on rate limits (429), server errors (5xx), timeouts and network errors, with exponential backoff, jitter and Retry-After support.

## `requestTimeoutSecs` (type: `integer`):

Abort and retry a request that takes longer than this.

## `maxConcurrentBoards` (type: `integer`):

How many boards are processed at once (per-host limits still apply).

## Actor input object example

```json
{
  "sources": [
    "https://boards.greenhouse.io/riotgames",
    "lever:kabam",
    "ashby:thatgamecompany"
  ],
  "preset": "none",
  "remoteOnly": false,
  "onlyNew": false,
  "stateKey": "default",
  "includeDescription": false,
  "dedupeSimilar": true,
  "maxResults": 0,
  "maxConcurrencyPerHost": 2,
  "minRequestIntervalMs": 250,
  "maxRetries": 4,
  "requestTimeoutSecs": 30,
  "maxConcurrentBoards": 6
}
```

# Actor output Schema

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

One dataset item per posting that passed the filters. Console opens the overview table.

## `summary` (type: `string`):

Totals, per-board status, invalid inputs, and why the run stopped, stored as the OUTPUT record.

# 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 = {
    "sources": [
        "https://boards.greenhouse.io/riotgames",
        "lever:kabam",
        "ashby:thatgamecompany"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("hardwoodlogic/multi-ats-jobs-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 = { "sources": [
        "https://boards.greenhouse.io/riotgames",
        "lever:kabam",
        "ashby:thatgamecompany",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("hardwoodlogic/multi-ats-jobs-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 '{
  "sources": [
    "https://boards.greenhouse.io/riotgames",
    "lever:kabam",
    "ashby:thatgamecompany"
  ]
}' |
apify call hardwoodlogic/multi-ats-jobs-feed --silent --output-dataset

```

## MCP server setup

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