# ATS API — Job Board, Job Listings & Applicant Tracking | $3/1K (`kenko404/ats-job-feed`) Actor

Unofficial — not affiliated with any ATS vendor. ATS aggregator and job board API: give company names, domains or careers page URLs, it detects the applicant tracking system (Greenhouse, Lever, Ashby, Workday, Workable, Personio, Rippling) and normalizes every open role into one schema.

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

## Pricing

from $3.00 / 1,000 job listings

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/platform/actors/running/actors-in-store#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 Jobs Scraper — Greenhouse, Lever, Ashby, Workday | $3/1K

> **Unofficial.** This Actor is not affiliated with, endorsed by, or sponsored
> by Greenhouse, Lever, Ashby, Workday, Workable, Personio or Rippling. All
> product names and trademarks belong to their respective owners and are used
> here only to describe which job boards this Actor can read. It reads each
> vendor's own public feed, subject to that vendor's `robots.txt`.

A **job listings API** for company career pages. Give it company names. Get back
every open role, normalized.

```json
{ "companies": ["stripe", "linear.app", "https://jobs.lever.co/spotify"] }
```

You do not have to know which applicant-tracking system a company uses. The
Actor works it out, reads the board, and returns one row shape no matter how
many different systems a run touched.

Use it as a **Greenhouse job board API**, a **Lever jobs API**, an **Ashby jobs
API**, a **Workday job board API** or a **Workable jobs API** — or point it at a
mixed list and let it sort out which is which.

### Why this exists

Job data on the market is scraped HTML. That breaks every time a career page is
redesigned, and it needs proxies and anti-bot handling to collect at all.

This reads **the JSON and XML feeds the ATS vendors publish themselves** —
`boards-api.greenhouse.io`, `api.lever.co`, `api.ashbyhq.com` and four more.
Nothing here logs in, uses a proxy, solves a challenge, spoofs a user agent, or
parses a layout. The user agent is `atsfeed/1.0` and says where it came from.

Six of the seven are documented, unauthenticated vendor endpoints. Workday is
the exception and is labelled as one: it is an undocumented career-site endpoint,
read from a career-site URL **you** supply, not a published API. It is included
because it is where a large share of enterprise roles actually live — but you
should know which of these you are depending on.

### What it will not read

Every host is checked against its own `robots.txt` before the first request, and
a published `Crawl-delay` sets the spacing. If a vendor changes its mind about
who may read a feed, this Actor stops on its own rather than waiting for someone
to notice.

That check is why two systems are **not** supported:

- **SmartRecruiters** — `api.smartrecruiters.com/robots.txt` allows exactly one
  agent, `LinkedInBot`, and disallows everything else. We are everything else.
- **Breezy** — its `/json` endpoint is undocumented and its terms speak against
  automated collection.

Requests are also capped per host — at most 4 in flight and 5 per second, and
half that into a single Workday tenant. A whole 2,000-role Workday board takes
about 2½ minutes rather than 8 seconds. That is deliberate: a job feed that gets
its source annoyed is worth nothing next month.

### Supported systems

| ATS | Addressed by | Descriptions | Endpoint |
|---|---|---|---|
| Greenhouse | slug, `boards.greenhouse.io/…` | yes | documented |
| Lever | slug, `jobs.lever.co/…` | yes | documented |
| Ashby | slug, `jobs.ashbyhq.com/…` | yes | documented |
| Workable | slug, `apply.workable.com/…` | yes | documented |
| Personio | slug, `*.jobs.personio.de` | yes | documented |
| Rippling | slug, `ats.rippling.com/…` | — | documented |
| Workday | full careers URL (`*.myworkdayjobs.com/…`) | — | **undocumented** |

Workday is addressed by URL rather than name because its host encodes the data
centre and its path encodes both tenant and site — none of which can be derived
from a company name.

### Input

Everything but `companies` is optional.

| Field | Meaning |
|---|---|
| `companies` | Names, domains, board URLs, or `provider:slug`. |
| `keywords` / `excludeKeywords` | Title filters. Whole-word, so `go` does not match `Google`. Multi-word entries match as phrases. |
| `locations` / `countries` / `departments` | Further filters. `countries` takes ISO-2 codes. |
| `remoteOnly` | Remote only. Hybrid is excluded, and so are roles the board says nothing about. |
| `postedWithinDays` | Recency. Undated roles are excluded — they cannot be shown to be recent. |
| `searchDescriptions` | Match keywords against the body text too. |
| `includeDescription` / `includeDescriptionHtml` | Add the description as plain text and/or original HTML. |
| `maxJobsPerCompany` / `maxJobsTotal` | Cost ceilings. Large Workday tenants list 20,000+ roles. |

### Output

One row per role:

```
provider  company  companySlug  jobId  title  department  team
employmentType  location  locations[]  country  workplaceType  isRemote
postedAt  updatedAt  salaryText  salaryMin  salaryMax  salaryCurrency
salaryPeriod  url  applyUrl  description  descriptionHtml  scrapedAt
```

Measured coverage over **1,287 live roles from 11 companies across all 7
systems**:

| Field | Present |
|---|---|
| `title`, `url`, `location` | 100% |
| `country` | 99.3% |
| `postedAt` | 88.7% |
| `department` | 77.0% |
| `workplaceType` | 49.6% |
| `employmentType` | 41.6% |
| `salaryMin` | 13.1% |

The lower rows are not gaps in this Actor — they are fields the boards
themselves leave blank. Rippling publishes no dates at all; most companies
publish no pay. **A field is `null` when the source did not say.** Nothing is
inferred to fill a column, because a guessed salary is worse than an empty one.

`department` is 100% on every system except Workday, which drags the average
down on its own. Workday postings carry no category at all, only a
`jobFamilyGroup` search facet. Taking a **whole** Workday board fills
`department` for 100% of its rows by replaying the search once per category —
NVIDIA's 2,000 postings, fully attributed. Taking a slice does not, because
labelling even 300 rows means sweeping all 2,617 postings, and the run log says
so out loud when it declines.

The 0.7% of rows with no `country` are rows whose location names no place:
`"Remote"`, `"N/A"`, `"LOCATION"`.

Four judgement calls worth knowing about:

- `workplaceType` is `null`, not `"onsite"`, when a board is silent. Silence is
  not evidence of an office.
- A role open in several places is `"remote"` if any one of those places reads
  remote. The order a board lists its offices in is its ordering, not a
  statement about the role, so reading only the first would decide `remoteOnly`
  at random.
- A bare two-letter code in a location is read as a US state (`Austin, TX` → US)
  because US boards write `City, ST` constantly while non-US boards spell the
  country out (`Berlin, Germany`). So `Berlin, DE` resolves to US — spell the
  country out and it resolves correctly.
- A city with one dominant reading resolves its own country (`London` → GB,
  `Bengaluru` → IN). This is only consulted *after* subdivision codes, so the US
  homonyms still win where a board qualifies them: `London, KY` → US.

### Pricing

Pay per job returned: **$3 per 1,000 roles**, flat — the same rate on every
plan, with no volume tier that quietly makes the number in the title wrong for
whichever tier you happen to be on. Plus a $0.005 run start fee. `maxJobsTotal`
is a hard ceiling, so a run cannot cost more than you allowed it to — including
after a platform migration mid-run.

### Notes

- **De-duplication**: companies commonly run two boards at once (a Greenhouse
  board and a Workday mirror). The same role from both collapses to one row.
- **Missing companies**: if a company self-hosts its careers page or uses an ATS
  not listed above, the run reports it under `SUMMARY.failures` rather than
  failing. Pass a board URL directly if auto-detection misses it.
- **Moved boards**: a slug whose board has been closed usually redirects to the
  vendor's marketing site. That is reported as a miss, not retried.
- **Resumable**: progress is checkpointed, so a migrated run neither re-fetches
  nor re-charges for boards already delivered.
- **Downstream use**: `apply.workable.com` publishes `Content-Signal:
  ai-train=no`. It does not restrict this Actor, but it is a stated preference
  about what may be done with the rows afterwards, and you should know it exists.

### Common questions

**How do I scrape company career pages without breaking every month?**
Don't parse the page. Every system here publishes a machine-readable feed behind
the careers page, and that feed changes far less often than the design does.
This Actor reads the feed.

**Is there a free job board API?**
The vendor feeds are free and unauthenticated — you can call
`boards-api.greenhouse.io` yourself. What you pay for here is the part that is
tedious rather than hard: working out which of seven applicant tracking systems
a company is on, addressing each one correctly, obeying seven different
`robots.txt` files and rate limits, and flattening seven incompatible payloads
into one row shape with real country and salary parsing.

**Can I get a job posting API across many companies at once?**
Yes — pass a list. Boards are fetched concurrently within the per-host limits,
duplicates across a company's two boards collapse to one row, and
`maxJobsTotal` caps what the run can cost you.

**Which applicant tracking system API does a given company use?**
Pass the company name or domain and read the `provider` column. Detection is the
first thing the run does, and it is reported per company in `SUMMARY`.

### Development

```bash
npm install
npm test          # 66 tests, no network
npm run demo      # live run against real boards
```

# Actor input Schema

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

Company names, domains, or job-board URLs. The Actor works out which ATS hosts each board. Accepts: `stripe`, `linear.app`, `https://boards.greenhouse.io/airbnb`, `lever:spotify`, or a full Workday careers URL.

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

Keep only roles whose title matches one of these. Whole-word matching, so `go` will not match `Google`. Leave empty for all roles.

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

Drop roles whose title or department matches any of these, e.g. `intern`, `contract`.

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

Keep roles whose location matches one of these, e.g. `London`, `Berlin`, `New York`.

## `countries` (type: `array`):

Keep roles in these countries, e.g. `US`, `GB`, `DE`. Resolved from the feed's own country field where present, otherwise from the location text.

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

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

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

Keep only roles whose workplace type reads as remote — from the board’s own field where it publishes one, otherwise from the location text. A role open in several places counts if any one of them reads remote: the order a board lists its offices in says nothing about the role. Hybrid is excluded, and so is a role whose location says nothing, since it cannot be shown to be remote.

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

Keep only roles published in the last N days. Roles with no publication date are excluded, since they cannot be shown to be recent. Leave empty for all.

## `searchDescriptions` (type: `boolean`):

Match `keywords` against the full job description as well as the title. Slower — descriptions must be fetched.

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

Add the full job description as readable plain text.

## `includeDescriptionHtml` (type: `boolean`):

Add the original HTML description. Large — expect 5-20 KB per role.

## `providers` (type: `array`):

Only probe these applicant-tracking systems. Leave empty to auto-detect across all of them. If you already know your companies are on one ATS, naming it here costs one request per company instead of seven — the same rows for a fraction of the compute.

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

Cap on delivered roles per company, applied after your filters and newest-first — so with `keywords` set you get the N most recent matches, not the matches that happen to fall in the first N roles the board lists.

## `maxFetchPerCompany` (type: `integer`):

How much of a board to read before filtering. Only matters for Workday, the one ATS here that pages — the other six return the whole board in a single request. Raise it if a large Workday tenant is hiding matches beyond the first 1,000 roles.

## `maxJobsTotal` (type: `integer`):

Hard ceiling on the run, so cost is bounded no matter how many roles the targets have.

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

Companies processed in parallel.

## Actor input object example

```json
{
  "companies": [
    "stripe",
    "https://jobs.lever.co/spotify"
  ],
  "keywords": [],
  "excludeKeywords": [],
  "locations": [],
  "countries": [],
  "departments": [],
  "remoteOnly": false,
  "searchDescriptions": false,
  "includeDescription": false,
  "includeDescriptionHtml": false,
  "providers": [],
  "maxJobsPerCompany": 1000,
  "maxFetchPerCompany": 1000,
  "maxJobsTotal": 5000,
  "concurrency": 6
}
```

# 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": [
        "stripe",
        "linear.app",
        "ramp"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kenko404/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 = { "companies": [
        "stripe",
        "linear.app",
        "ramp",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("kenko404/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 '{
  "companies": [
    "stripe",
    "linear.app",
    "ramp"
  ]
}' |
apify call kenko404/ats-job-feed --silent --output-dataset

```

## MCP server setup

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