# Company Jobs Scraper — 12 ATS, $0.002/job (`plainapi/company-jobs-ats-api`) Actor

You pay only for jobs actually returned. Nothing found, nothing charged. Every open role from a company's own board across 12 ATS — Greenhouse, Workday, Ashby, Lever, SmartRecruiters, Workable, BambooHR and more. Reads the official public API each careers page already calls.

- **URL**: https://apify.com/plainapi/company-jobs-ats-api.md
- **Developed by:** [Shun Furu](https://apify.com/plainapi) (community)
- **Categories:** Jobs, Agents, MCP servers
- **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 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?

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, Workday, Ashby, Lever and 8 more

Get every open job from any company's careers page. Give it a domain — `stripe.com` — and it
works out which applicant tracking system (ATS) that company uses, reads the **official public
job-board API**, and returns clean, normalised job data.

**Live, not a database.** Every run reads the company's own job board at that moment. There is no
index to be out of date and no "sorry, that company isn't in our data" — if the company has a
public board, you get today's jobs.

No browser. No cookies. No proxies. No blocking.

```
Input:   ["stripe.com", "openai.com", "monzo.com"]
Output:  one row per open job — title, location, department, remote flag, URL, dates
```

> **Not affiliated.** This Actor is an independent tool. It is not affiliated with, endorsed by, or
> sponsored by Greenhouse, Workday, Ashby, Lever, SmartRecruiters, Rippling, Workable, Recruitee,
> BambooHR, Breezy HR, Teamtailor, Personio, or any of the
> companies whose job boards it reads. All product names and trademarks belong to their respective
> owners and are used here only to describe which public job boards this Actor can read.

***

### Two things no other job Actor does

#### 1. Find out the cost before you pay — `countOnly`

Set `countOnly: true` and the Actor resolves every company, counts the jobs that match your
filters, and **writes nothing**. No rows means no charge, so the run is free. The count and the
estimated cost land in the run's `SUMMARY` record and in the log:

```
Done. 2577 jobs match across 3/3 companies.
Running this for real would cost about $5.15. This run was free — nothing was written.
```

Use it to size a company list before committing to it. It answers the one question pay-per-event
pricing normally cannot: *what is this run going to cost me?*

#### 2. Pay for new jobs only — `onlyNewSinceLastRun`

Built for scheduled runs. The Actor remembers the job IDs it has already returned to you, and on
the next run gives you **only the postings that appeared since**. A daily run over 50 companies
stops being 50 full boards every day and becomes just the new openings.

The first run has nothing to compare against, so it returns everything; the saving starts on the
second run. The memory is per-account and per-board, and lives in a named key-value store on your
own account.

***

### Why not just scrape the careers page?

Most job scrapers render a careers page in a headless browser and parse the HTML. That breaks every
time a company redesigns its site, it gets blocked, and it is slow.

This Actor never touches HTML for job data. Every company on a supported ATS has a **public JSON
(or XML) endpoint** — the very same one its own careers page calls to draw the job list. This Actor
reads that endpoint directly.

| | HTML scrapers | Job databases | This Actor |
|---|---|---|---|
| Breaks on site redesign | Yes | No | No — the API contract is stable |
| Blocked by bot protection | Often | No | No — the endpoints are public and unauthenticated |
| Needs proxies | Usually | No | No |
| Data freshness | Live | **Hours to a day behind** | **Live** |
| Company missing entirely | — | **Possible — if it is not in their index** | Not possible — it reads the company's own board |
| Cost per company | Browser compute | Subscription | One HTTP request |

**You do not need to know which ATS a company uses.** Pass the company domain and all twelve
providers are probed in parallel; whichever answers wins. If none do, the Actor reads the company's
careers page and looks for the job board embedded in it.

***

### Input

Every field is optional. Run it with the defaults and it works.

| Field | Type | Default | What it does |
|---|---|---|---|
| `companies` | array of strings | `["stripe.com", "openai.com"]` | Company domains, careers-page URLs, or explicit `provider:slug` |
| `keyword` | string | — | Keep only jobs matching any of these words (title, department, team, description) |
| `location` | string | — | Keep only jobs whose location contains any of these words |
| `remoteOnly` | boolean | `false` | Keep only jobs the ATS flags as remote |
| `includeDescription` | boolean | `false` | Include the full job description as plain text |
| `countOnly` | boolean | `false` | Count matching jobs and return nothing — **free** |
| `onlyNewSinceLastRun` | boolean | `false` | Return only jobs not seen in an earlier run |
| `maxJobsPerCompany` | integer | `0` (no limit) | Cap results per company |
| `maxTotalJobs` | integer | `0` (no limit) | Cap results for the whole run — use this to cap cost |

#### The `companies` field accepts three shapes

```jsonc
[
  "stripe.com",                                    // a domain — the ATS is detected
  "https://jobs.ashbyhq.com/ramp",                 // a careers-page URL
  "greenhouse:airbnb"                              // an explicit board
]
```

For **Workday**, pass the careers-site URL —
`https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite`. A Workday tenant cannot be derived
from a company domain, so this Actor asks for the URL rather than pretending to guess.

#### Example 1 — remote engineering jobs at a shortlist of companies

```json
{
  "companies": ["stripe.com", "openai.com", "monzo.com", "linear.app"],
  "keyword": "engineer, scientist",
  "remoteOnly": true,
  "includeDescription": true
}
```

#### Example 2 — check the cost of a large list before paying for it

```json
{
  "companies": ["stripe.com", "cloudflare.com", "figma.com", "notion.so", "airtable.com"],
  "countOnly": true
}
```

Returns nothing, charges nothing, and tells you what the real run would cost.

#### Example 3 — a daily watch that only bills you for new postings

```json
{
  "companies": ["stripe.com", "anthropic.com", "vercel.com"],
  "onlyNewSinceLastRun": true,
  "maxTotalJobs": 500
}
```

Schedule it daily. Day one returns every open job; from day two you get — and pay for — only the
jobs that have appeared since.

***

### Output

One row per job. The fields are identical across all twelve platforms, so results from different
companies can be pooled with no post-processing.

| Field | Type | Always present | Description |
|---|---|---|---|
| `company` | string | yes | The value you passed in |
| `companySlug` | string | yes | The board identifier this company uses on its ATS |
| `ats` | string | yes | `greenhouse` | `workday` | `ashby` | `lever` | `smartrecruiters` | `rippling` | `workable` | `recruitee` | `bamboohr` | `breezy` | `teamtailor` | `personio` |
| `atsLabel` | string | yes | The same, formatted for display |
| `jobId` | string | yes | The ATS's own identifier for the posting |
| `title` | string | yes | Job title |
| `url` | string | yes | Public link to the posting |
| `location` | string | yes | As published by the company; formatting varies by ATS |
| `remote` | boolean | null | yes | `true` when the ATS flags it remote or the location says so; `null` when the ATS does not report it |
| `department` | string | yes | Empty on platforms that do not publish it |
| `team` | string | yes | Empty on platforms that do not publish it |
| `employmentType` | string | yes | Full-time, contract, etc. Not published by every ATS |
| `salary` | string | yes | As published, e.g. `4500–6000 EUR/month`. Empty on the platforms that do not publish it |
| `publishedAt` | string | null | yes | ISO date, when the ATS publishes one |
| `updatedAt` | string | null | yes | ISO date, when the ATS publishes one |
| `postedLabel` | string | Workday only | Workday's relative label, e.g. `Posted Yesterday` |
| `description` | string | with `includeDescription` | Plain-text job description |
| `scrapedAt` | string | yes | When the row was produced (ISO 8601, UTC) |

```json
{
  "company": "stripe.com",
  "companySlug": "stripe",
  "ats": "greenhouse",
  "atsLabel": "Greenhouse",
  "jobId": "8130725",
  "title": "Account Executive, AI Startups",
  "url": "https://stripe.com/jobs/search?gh_jid=8130725",
  "location": "San Francisco",
  "remote": null,
  "department": "Startups - Account Executives (NA)",
  "team": "",
  "employmentType": "",
  "publishedAt": "2026-08-19T14:02:07-04:00",
  "updatedAt": "2026-08-19T14:02:07-04:00",
  "scrapedAt": "2026-08-22T09:48:35.371Z"
}
```

#### The run summary

Every run writes a `SUMMARY` record to the key-value store: which ATS each company resolved to, how
many jobs matched your filters, and **which companies could not be resolved, with the list of
boards that were tried**. Failures are never written to the dataset, so a company that cannot be
read costs you nothing and still tells you why.

***

### Supported platforms

| ATS | Endpoint used | Description text | Departments |
|---|---|---|---|
| Greenhouse | `boards-api.greenhouse.io/v1/boards/{slug}/jobs` | with `includeDescription` | yes |
| Workday | `{tenant}.wd{n}.myworkdayjobs.com/wday/cxs/…/jobs` | with `includeDescription` | — |
| Ashby | `api.ashbyhq.com/posting-api/job-board/{slug}` | yes | yes |
| Lever | `api.lever.co/v0/postings/{slug}` | yes | yes |
| SmartRecruiters | `api.smartrecruiters.com/v1/companies/{slug}/postings` | — | yes |
| Rippling | `api.rippling.com/platform/api/ats/v1/board/{slug}/jobs` | — | yes |
| Workable | `apply.workable.com/api/v1/widget/accounts/{slug}` | yes | yes |
| Recruitee | `{slug}.recruitee.com/api/offers/` | yes | yes |
| BambooHR | `{slug}.bamboohr.com/careers/list` | — | yes |
| Breezy HR | `{slug}.breezy.hr/json` | — | yes |
| Teamtailor | `{slug}.teamtailor.com/jobs.json` | yes | — |
| Personio | `{slug}.jobs.personio.de/xml` | yes | yes |

**Salary** is returned where the platform publishes it — currently Recruitee and Breezy HR.

***

### Pricing

You pay for jobs, not for attempts.

| | Price |
|---|---|
| Starting a run | **$0.00001** — one US cent per 1,000 runs |
| Each job returned | **$0.002** — $2 per 1,000 jobs |

**What you are not charged for**

- Companies that could not be resolved — no rows are produced, so there is nothing to bill.
- Error messages and diagnostics — they go to the log and to `SUMMARY`, never to the dataset.
- `countOnly` runs — they write nothing at all.
- With `onlyNewSinceLastRun`, jobs you have already been given.

Cap a run with `maxTotalJobs`, or with Apify's own per-run charge limit.

***

### Reliability

The Actor ships with a live smoke test covering all seven platforms plus the domain-detection and
careers-page-discovery paths, checked against known-good companies and failing if the number of
returned jobs drops below a floor. It runs against the real APIs, so upstream changes are caught
before users hit them — which is the point of building on stable public APIs rather than on HTML.

```
✓ greenhouse        576 jobs    601ms
✓ ashby             754 jobs    263ms
✓ lever             383 jobs   1155ms
✓ rippling          734 jobs    664ms
✓ smartrecruiters   136 jobs    605ms
✓ workday         1,182 jobs   4486ms
✓ workable           38 jobs    358ms
✓ recruitee          15 jobs    355ms
✓ bamboohr            4 jobs   1141ms
✓ breezy              3 jobs    188ms
✓ teamtailor         38 jobs    689ms
✓ domain detection  576 jobs   4014ms
✓ site discovery      1 job    8550ms
```

***

### FAQ

**Do I need to know which ATS a company uses?**
No. Pass the domain. All seven are probed in parallel and the one that answers is used. If none
answer, the Actor reads the company's careers page and looks for an embedded job board. The one
exception is Workday — see below.

**Why does Workday need a URL when the others don't?**
A Workday board lives at `{tenant}.wd{n}.myworkdayjobs.com`, and neither the tenant nor the pod
number can be derived from a company domain. Every way of deriving it was tested and none worked,
so the Actor asks for the URL instead of failing silently.

**What happens if a company can't be resolved?**
It is reported in `SUMMARY` with the list of boards that were tried, and it produces no rows — so
it costs you nothing.

**How do I keep the cost down?**
Three ways: run with `countOnly: true` first to see the price; set `maxTotalJobs`; or use
`onlyNewSinceLastRun` for scheduled runs so you are billed only for new postings.

**How fresh is the data?**
It is read at the moment you run it. There is no cache and no index.

**Can it handle very large boards?**
Yes. Large Workday employers publish thousands of jobs — NVIDIA about 2,000, Salesforce about
1,530. The Actor reads the total up front and fetches the remaining pages in parallel, bringing a
2,000-job board down to roughly 16 seconds.

**Does it collect any personal data?**
No. Job postings are company business information. No candidate or employee data is touched, and
nothing behind a login is accessed.

**Is reading these boards allowed?**
The Actor only calls public, unauthenticated endpoints that the companies' own careers pages call.
No bot protection is bypassed, no rate limits are circumvented, and it backs off on `429`.

**Why is `department` empty for some companies?**
Not every ATS publishes it, and some boards leave it blank. For Greenhouse the Actor makes an extra
call to fill departments in where the listing endpoint omits them.

**Why is `publishedAt` empty on Workday?**
Workday's listing endpoint returns a relative label (`Posted Yesterday`, exposed as `postedLabel`)
rather than a date. Enable `includeDescription` and the Actor reads each job's detail page, which
carries a real date, the employment type and the description — at the cost of one extra request per
job.

**Can I get salary data?**
Where the platform publishes it, yes — the `salary` field is filled in for Recruitee and Breezy HR
boards. Most other ATS platforms do not expose salary publicly, so the field is empty for them.

**Can I feed it a spreadsheet of companies?**
Not yet — pass the list in `companies`. If this matters to you, open an issue on the Actor. Issues
decide what gets built next.

***

### Limitations — stated up front

- Only companies on one of the twelve supported platforms are found. Taleo, iCIMS, Jobvite,
  SuccessFactors, Gem and bespoke careers pages are **not covered**.
- Slug detection from a bare domain is a heuristic (`acme.com` → `acme`, plus a few variants such
  as stripping a trailing `hq`). It is right for most companies; when it is not, pass
  `provider:slug` explicitly — the slug is visible in the URL of the company's careers page.
- SmartRecruiters does not include description text in its public listing endpoint.
- `location` is free text exactly as the employer wrote it. It is not normalised.

# Actor input Schema

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

Company domains (stripe.com), careers-page URLs (https://job-boards.greenhouse.io/stripe), or explicit board slugs (greenhouse:stripe). The ATS is detected automatically.

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

Keep only jobs whose title, department or description contains any of these words (space or comma separated). Leave empty for all jobs.

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

Keep only jobs whose location contains any of these words. Leave empty for all locations.

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

Keep only jobs flagged as remote by the ATS, or whose location mentions remote.

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

Fetch and include the full job description as plain text. Slower and produces larger results.

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

0 means no limit.

## `maxTotalJobs` (type: `integer`):

0 means no limit. Use this to cap cost on large runs.

## `countOnly` (type: `boolean`):

Count how many roles match, without returning any. Nothing is written to the dataset, so the run is free. Use it to see exactly what a real run would cost before paying for it. The count and the estimated cost are written to the run's SUMMARY record and to the log.

## `onlyNewSinceLastRun` (type: `boolean`):

Return only roles that were not seen in a previous run of this Actor on your account. Built for scheduled runs: you pay for new postings instead of re-paying for the same board every time. The first run has nothing to compare against, so it returns everything; the saving starts from the second run. Ignored when countOnly is on.

## Actor input object example

```json
{
  "companies": [
    "stripe.com",
    "openai.com",
    "ramp.com"
  ],
  "remoteOnly": false,
  "includeDescription": false,
  "maxJobsPerCompany": 0,
  "maxTotalJobs": 0,
  "countOnly": false,
  "onlyNewSinceLastRun": false
}
```

# Actor output Schema

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

Every open job found, normalised to the same fields across all supported ATS platforms.

## `jobsCsv` (type: `string`):

The same rows as CSV, for spreadsheets.

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

Which ATS each company resolved to, how many roles matched, and which companies could not be resolved. Useful for checking a run is complete.

# 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.com",
        "openai.com",
        "ramp.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("plainapi/company-jobs-ats-api").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.com",
        "openai.com",
        "ramp.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("plainapi/company-jobs-ats-api").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.com",
    "openai.com",
    "ramp.com"
  ]
}' |
apify call plainapi/company-jobs-ats-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,plainapi/company-jobs-ats-api"
        }
    }
}

```

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/5uu5X1aaMibudcUpP/builds/HUNWcx6DCaGIgTr9A/openapi.json
