# ATS Job Feeds: Greenhouse, Lever, Ashby, Workable & more (`chatuporn/ats-job-feeds`) Actor

Normalized job postings from company job boards on Greenhouse, Lever, Ashby, SmartRecruiters, Workable and Recruitee. Paste career-page URLs; get one clean schema with salary, remote, locations and dates. Only-new mode tracks changes between runs.

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

## Pricing

from $0.01 / 1,000 results

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

## ATS Job Feeds: Greenhouse, Lever, Ashby, Workable & more

**One clean job feed from many companies' career pages.** Paste the job-board URLs of the companies you follow. The Actor reads their public boards on **Greenhouse, Lever, Ashby, SmartRecruiters, Workable and Recruitee** and returns every posting in **one schema**: title, department, locations, remote flag, employment type, salary (when the company publishes it), dates, apply link and, if you want it, the description as clean text or Markdown.

Schedule it with **Only new** on and you get a feed of jobs posted since the last run, for job boards, recruiting and sales-intelligence tools, market research, or alerts.

### Why use this

- **Six ATSs, one output.** No more one Actor per ATS with different field names. `salaryMin`, `remote`, `postedAt` mean the same thing for every company.
- **Paste whatever you have.** `https://jobs.lever.co/spotify`, `greenhouse:airbnb`, a company's own careers page (the embedded board is found for you), or just `notion` (every ATS is tried).
- **Official public endpoints.** The Actor reads the JSON feeds the ATS vendors publish for embedding job lists on career sites. No login, no browser, no proxies, so it is fast and cheap.
- **Change tracking.** *Only jobs not seen in earlier runs* remembers each job ID per company. The run summary also counts jobs that were removed from each board.
- **Filters that behave.** Keywords match whole words (`java` does not match `javascript`), locations match whole words (`US` does not match `Australia`), plus remote only, departments, employment types and posting age.
- **One bad company never fails the run.** Each company gets a status (`ok`, `not_found`, `unsupported`, `error`, `invalid`) in the run summary.

### Input

| Field | What it does |
|---|---|
| `companies` | Required. URLs, `ats:token`, careers pages or names. One per line. |
| `keywords`, `keywordsMatch` | Keep jobs with any of these words, in the title only or in title and description. |
| `excludeKeywords` | Drop jobs whose title contains any of these words. |
| `locations` | Keep jobs in any of these places (whole words). Add `Remote` to also keep remote jobs. |
| `remoteOnly` | Only jobs marked remote. |
| `departments` | Department or team contains any of these texts. |
| `employmentTypes` | `full_time`, `part_time`, `contract`, `internship`, `temporary`. |
| `postedWithinDays` | Only jobs first published in the last N days. |
| `onlyNew` | Only jobs not returned before (state kept per company). |
| `includeDescription`, `descriptionFormat` | Add the description as `text`, `markdown` or the original `html`. |
| `maxJobsPerCompany` | Newest N jobs per company. 0 = all. |

#### Accepted company formats

| ATS | URL examples | Token form |
|---|---|---|
| Greenhouse | `boards.greenhouse.io/airbnb`, `job-boards.greenhouse.io/discord`, `boards.greenhouse.io/embed/job_board?for=figma` | `greenhouse:airbnb` |
| Lever | `jobs.lever.co/spotify`, `jobs.eu.lever.co/x` | `lever:spotify` |
| Ashby | `jobs.ashbyhq.com/openai` | `ashby:openai` |
| SmartRecruiters | `careers.smartrecruiters.com/Wix2`, `jobs.smartrecruiters.com/Ubisoft2` | `smartrecruiters:Wix2` |
| Workable | `apply.workable.com/huggingface`, `huggingface.workable.com` | `workable:huggingface` |
| Recruitee | `bunq.recruitee.com` | `recruitee:bunq` |

The token is the part of the board URL after the domain (for Recruitee, the subdomain). SmartRecruiters IDs are case-sensitive.

### Examples

**Daily feed of new engineering jobs at a watch-list of startups**

```json
{
  "companies": ["https://jobs.ashbyhq.com/openai", "greenhouse:discord", "lever:spotify", "https://apply.workable.com/huggingface"],
  "departments": ["Engineering", "Research"],
  "onlyNew": true
}
```

**Remote data jobs posted this week, with Markdown descriptions**

```json
{
  "companies": ["greenhouse:airbnb", "ashby:notion", "ashby:linear", "recruitee:bunq"],
  "keywords": ["data", "analytics", "machine learning"],
  "remoteOnly": true,
  "postedWithinDays": 7,
  "includeDescription": true,
  "descriptionFormat": "markdown"
}
```

**Salary benchmarking: everything a company publishes**

```json
{ "companies": ["https://job-boards.greenhouse.io/discord", "https://jobs.ashbyhq.com/openai"] }
```

### Output

One item per job:

```json
{
  "ats": "greenhouse",
  "company": "Discord",
  "companyToken": "discord",
  "jobId": "8847080002",
  "title": "Group Product Manager, Data/ML",
  "department": "Product Management",
  "team": null,
  "locations": ["San Francisco Bay Area"],
  "country": null,
  "remote": false,
  "workplaceType": null,
  "employmentType": null,
  "employmentTypeRaw": null,
  "salaryMin": 304000,
  "salaryMax": 380000,
  "salaryCurrency": "USD",
  "salaryPeriod": "year",
  "salaryText": "Base Salary",
  "postedAt": "2026-10-23T20:58:26Z",
  "updatedAt": "2026-10-23T20:58:26Z",
  "jobUrl": "https://job-boards.greenhouse.io/discord/jobs/8847080002",
  "applyUrl": "https://job-boards.greenhouse.io/discord/jobs/8847080002",
  "description": "Discord has a highly engaged community of millions of daily active users ...",
  "scrapedAt": "2026-11-02T14:14:27Z"
}
```

- `workplaceType` is `remote`, `hybrid`, `onsite` or `null` (not stated). `remote` is `true` only for remote jobs.
- `employmentType` is one of `full_time`, `part_time`, `contract`, `internship`, `temporary`, `other`; `employmentTypeRaw` keeps the ATS's own wording.
- `salaryPeriod` is `year`, `month`, `week`, `day` or `hour`. Salary fields are `null` when the company does not publish a structured range.
- Dates are ISO 8601 in UTC. `description` appears only when *Include job description* is on.

The **run summary** is saved as `OUTPUT` in the run's key-value store: one entry per company with its status, the number of jobs on the board, matched, new, removed and saved.

### What each ATS provides

| | Greenhouse | Lever | Ashby | SmartRecruiters | Workable | Recruitee |
|---|---|---|---|---|---|---|
| Salary | when published | when published | when published | – | – | when published |
| Remote / hybrid | when the board has a workplace field or the location says Remote | yes | yes | yes | remote only | yes |
| Employment type | when the board has the field | yes | yes | yes | yes | yes |
| Posted date | yes | yes (created) | yes | yes | yes (day) | yes |
| Updated date | yes | – | – | – | – | yes |
| Company name | yes | from the board page | from the board page | yes | yes | yes |

### Good to know

- **Pricing is per job saved.** Filters run before saving, so filtered-out jobs cost nothing. Set a maximum cost per run in Apify to cap spending.
- **Speed.** Most boards load in 1–5 seconds; a run over a handful of companies usually finishes in under 10 seconds. Lever can serve very large boards slowly, and SmartRecruiters needs one extra request per job for descriptions (about 4 jobs per second). Requests are spread out: at most two at a time per ATS host, with a 1-second gap on Lever as its robots.txt asks.
- **SmartRecruiters answers "0 jobs" for a company ID that does not exist**, so a typo shows as `ok` with 0 jobs and a hint in the summary.
- **Careers pages on the company's own domain** are detected only if the board is linked or embedded in the page's HTML. Pages that load the board with JavaScript from elsewhere may show `unsupported`; use the `ats:token` form then.
- **Not supported:** Workday, iCIMS, Taleo, SuccessFactors, BambooHR, Personio and other systems without a public job-board JSON feed.
- **Personal data.** Only the posting itself is returned. Recruiter names, recruiter emails and application-form questions that some feeds include are dropped. Descriptions are passed on as the company published them.
- **Only new** stores job IDs (no personal data) in the named key-value store `ats-job-feeds-state` in your Apify account. Delete that store to start over.

### Data source

Public job-board APIs that the ATS vendors document for building career sites: Greenhouse Job Board API, Lever Postings API, Ashby Job Postings API, SmartRecruiters Posting API, the Workable jobs widget and the Recruitee Careers Site API. Job postings belong to the companies that published them.

# Actor input Schema

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

One company per line. Paste a job-board URL (boards.greenhouse.io/x, jobs.lever.co/x, jobs.ashbyhq.com/x, careers.smartrecruiters.com/x, apply.workable.com/x, x.recruitee.com), write 'ats:token' (e.g. greenhouse:airbnb, lever:spotify, ashby:openai), paste the company's own careers page (the Actor looks for an embedded board), or just a name (every ATS is tried).

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

Keep jobs that contain at least one of these words or phrases. Whole words, case-insensitive: 'java' does not match 'javascript'. Leave empty for all jobs.

## `keywordsMatch` (type: `string`):

Where to look for the keywords. 'Title and description' also checks department and team. On SmartRecruiters the board's own search is used for descriptions.

## `excludeKeywords` (type: `array`):

Drop jobs whose title contains any of these words (whole words, case-insensitive), e.g. senior, intern, manager.

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

Keep jobs in at least one of these places, matched as whole words against the job's locations and country, e.g. London, Germany, US, New York. Add 'Remote' to also keep remote jobs anywhere.

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

Keep only jobs the ATS marks as remote (or whose location says Remote). Hybrid jobs are not remote.

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

Keep jobs whose department or team contains one of these texts (case-insensitive), e.g. Engineering, Sales, Data.

## `employmentTypes` (type: `array`):

Keep only these employment types. Jobs whose ATS does not state a type are dropped when this is set.

## `postedWithinDays` (type: `integer`):

Keep jobs first published in the last N days. Jobs without a published date are kept. Leave empty for any age.

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

Remember every job ID per company (in the named key-value store 'ats-job-feeds-state' in your account) and return only jobs that appeared since the last run. The first run returns all current jobs. Use with a schedule for a new-jobs feed.

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

Add the full job description. On SmartRecruiters this takes one extra request per job, so it is slower there.

## `descriptionFormat` (type: `string`):

Plain text, light Markdown (headings, bold, lists, links) or the original HTML from the ATS.

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

Return at most this many jobs per company, newest first. 0 means no limit. With 'Only new' on, jobs over the limit are returned in a later run.

## Actor input object example

```json
{
  "companies": [
    "https://job-boards.greenhouse.io/discord",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/linear"
  ],
  "keywordsMatch": "titleAndDescription",
  "remoteOnly": false,
  "onlyNew": false,
  "includeDescription": false,
  "descriptionFormat": "text",
  "maxJobsPerCompany": 0
}
```

# Actor output Schema

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

Every job with all fields.

## `overview` (type: `string`):

Company, title, department, locations, workplace, type, salary, posted date and link.

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

Status per company (ok, not_found, unsupported, error, invalid) with job counts.

# 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": [
        "https://job-boards.greenhouse.io/discord",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/linear"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("chatuporn/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": [
        "https://job-boards.greenhouse.io/discord",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/linear",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("chatuporn/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": [
    "https://job-boards.greenhouse.io/discord",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/linear"
  ]
}' |
apify call chatuporn/ats-job-feeds --silent --output-dataset

```

## MCP server setup

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