# Job Scraper That Never Gets Blocked - Company ATS (`datalayer/ats-jobs-api`) Actor

Reads company job boards at the source - Greenhouse, Lever, Ashby, SmartRecruiters - instead of scraping LinkedIn. No login, no cookies, no account to get blocked. One schema across all four, versioned so your pipeline does not break, plus a change feed of roles opened and closed.

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

## Pricing

from $1.40 / 1,000 jobs

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Job Scraper That Never Gets Blocked — straight from the company's ATS

Pull every open role from any company's **Greenhouse**, **Lever**, **Ashby**, **SmartRecruiters**, **BambooHR** or **Recruitee** board, in one unified format. No login. No cookies. No browser automation. Nothing that can get an account blocked, because there is no account.

Optional change tracking tells you which roles **appeared**, **closed** or were **edited** since your last run — the difference between a job list and a hiring signal.

### Why not just scrape LinkedIn or Indeed?

Because those break, and they break in the two ways that cost you real time:

**They get blocked, and they put your account at risk.** Anything scraping a logged-in surface is fighting anti-bot systems that change weekly. This reads the same public JSON endpoints companies publish so applicants can find their jobs. There is nothing to detect and nothing to ban.

**Their output moves under you.** The most-used LinkedIn job scraper on this store is rated 3.9, and its top review says it plainly: *one week it strictly matches job titles and returns 250 postings, the next it returns 5,000 irrelevant ones.* That makes it unusable in a pipeline — every run becomes a cleaning job.

This actor takes the opposite position, and puts it in writing below.

### The schema contract

Every row carries `schemaVersion`. While that number stays the same:

- fields are **added**, never renamed, retyped or removed
- filters mean the same thing every run — `titleFilter` is a case-insensitive regular expression applied to the job title, and nothing else
- a field that cannot be determined returns `null`, never a guess

If a breaking change is ever genuinely necessary, the version increments and the change is documented. Your pipeline gets to keep working.

### The problem this solves

Most companies publish open roles through Greenhouse, Lever, Ashby, SmartRecruiters, BambooHR or Recruitee, and each publishes a completely different shape. Greenhouse calls a link `absolute_url`, Ashby calls it `jobUrl`, Lever calls it `hostedUrl`. Dates arrive as ISO strings in some and epoch milliseconds in another. Departments are nested arrays in one, flat strings in the next. SmartRecruiters paginates and keeps descriptions on a separate call entirely.

Every other job scraper handles exactly one provider and hands you its raw shape. Track companies across all six — and you will, because companies don't coordinate their ATS choices — and you're left writing and maintaining that join yourself.

This does it for you. **One schema, one table, every provider.**

### What you get

| Field | Notes |
|---|---|
| `schemaVersion` | The output contract this row was produced under |
| `company`, `companySlug`, `ats` | Who, and which system published it |
| `title`, `department`, `team` | Normalised across all six providers |
| `location`, `isRemote` | See the note on remote detection below |
| `employmentType` | Full-time, contract and so on, where published |
| `postedAt`, `updatedAt` | Always ISO 8601, including Lever's epoch timestamps |
| `jobUrl`, `applyUrl` | Public posting and direct application links |
| `description` | Full text, HTML stripped |
| `compensation` | Salary range where the company discloses one |

#### Honest remote detection

`isRemote` returns `true`, `false`, or **`null` when it genuinely cannot be determined.**

Ashby, SmartRecruiters and Recruitee publish real flags. Greenhouse and Lever only hint at it in free text, so a location reading "London" tells you nothing about whether the role is remote. Most scrapers return `false` in that case, which is a guess dressed as data — and it quietly breaks anyone filtering on it.

Null means unknown. Filtering with `remoteOnly` excludes unknowns rather than assuming.

### Change tracking

Turn on `trackChanges` and schedule the actor daily. Each run compares against the previous snapshot and flags every role as `appeared`, `closed` or `edited`.

That turns a static list into a feed:

- A company opening four sales roles in a week is expanding a team
- A company quietly closing its entire platform team is a different signal entirely
- Roles that reappear after closing usually mean a failed hire

Nobody else on the store sells this, and it is the reason to run this on a schedule rather than once.

### Input

```json
{
  "companies": ["greenhouse:stripe", "lever:plaid", "ashby:ramp", "smartrecruiters:Visa", "bamboohr:jobandtalent"],
  "trackChanges": true,
  "remoteOnly": false
}
```

**Finding a company's slug** — it's the last part of their careers URL:

| ATS | Careers URL | Slug |
|---|---|---|
| Greenhouse | `boards.greenhouse.io/stripe` | `stripe` |
| Lever | `jobs.lever.co/plaid` | `plaid` |
| Ashby | `jobs.ashbyhq.com/ramp` | `ramp` |
| SmartRecruiters | `jobs.smartrecruiters.com/Visa` | `Visa` (case-sensitive) |
| BambooHR | `jobandtalent.bamboohr.com/careers` | `jobandtalent` |
| Recruitee | `personio.recruitee.com` | `personio` |

A bare slug like `"stripe"` defaults to Greenhouse. If you get the provider wrong, the error tells you exactly which URL to check rather than just returning nothing.

**Filters** — `remoteOnly`, `titleFilter` (accepts a regular expression, e.g. `engineer|developer`), `maxJobsPerCompany`, and `includeDescription` to shrink the payload when you only need titles and links.

### Common use cases

- **Recruiting and talent mapping** — monitor hiring across a target list of companies
- **Sales prospecting** — a company hiring ten engineers is buying developer tools
- **Competitive intelligence** — track headcount direction by team
- **Job aggregators** — one integration instead of three
- **Investment research** — hiring velocity as a growth signal

### Reliability

**One company failing never fails the run.** If you're tracking two hundred companies and one board is unreachable, you get one hundred and ninety-nine, plus a machine-readable record of which one failed and why. You're only charged for roles actually returned.

These are the providers' own public JSON endpoints — no login, no session, no browser automation — which makes this one of the most stable data sources available. Wrong slugs produce a clear error naming the URL to check, rather than a silent empty result.

That last point matters more than it sounds. SmartRecruiters answers an unknown company identifier with `HTTP 200` and zero results rather than a 404, so a typo looks exactly like a company that isn't hiring. This actor tells you which of the two it is.

### FAQ

**Which job boards does it read?** Company career sites running Greenhouse,
Lever, Ashby, SmartRecruiters, BambooHR or Recruitee — the ATS platforms behind
most tech and scale-up hiring. You name companies, it finds their jobs at the
source.

**Why is this better than scraping LinkedIn or Indeed?** No login, no
anti-bot wall, no account to get blocked, and no selector to break when a page
redesigns. ATS APIs are public JSON the companies themselves publish.

**Will the output format change and break my pipeline?** Every row carries
`schemaVersion`. Fields are added, never renamed or removed, without that
number changing. That is the contract.

**Can I get only remote jobs?** Yes — and remote detection is honest: a job is
flagged remote from the ATS's own fields, not guessed from the title.

**Can I export to CSV or feed an ATS/CRM?** Yes — CSV, Excel, JSON or JSONL
per run, or pull the dataset via the API on a schedule.

**How fresh are the results?** As fresh as the company's own careers page —
the ATS is where postings appear first, usually before the aggregators.

### Notes

Data comes from publicly published job boards that companies operate to attract applicants. This actor is unofficial and is not affiliated with Greenhouse, Lever, Ashby, SmartRecruiters, BambooHR or Recruitee. Job descriptions remain the property of the publishing company — use them in line with applicable laws and terms.

# Actor input Schema

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

One entry per company board, as "ats:slug". Supported: greenhouse, lever, ashby, smartrecruiters, bamboohr and recruitee — for example "greenhouse:stripe", "smartrecruiters:Visa", "bamboohr:jobandtalent". A bare slug defaults to Greenhouse. The slug is the part of the company's careers URL that identifies them, and it is case-sensitive for SmartRecruiters.

## `titleFilter` (type: `string`):

Keep only roles whose title matches this text or regular expression, for example: engineer|developer

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

Keep only roles confirmed as remote. Roles where remote status cannot be determined are excluded rather than guessed.

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

Turn off to get a much smaller dataset when you only need titles, locations and links.

## `trackChanges` (type: `boolean`):

Compare against the previous run and flag roles that appeared, closed or were edited. Schedule this daily to build a hiring signal feed.

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

Cap the number of roles returned per company. Leave empty for all.

## `proxyConfiguration` (type: `object`):

Optional. These are public JSON endpoints and generally do not require a proxy.

## Actor input object example

```json
{
  "companies": [
    "greenhouse:stripe",
    "lever:plaid",
    "ashby:ramp",
    "smartrecruiters:Visa",
    "bamboohr:jobandtalent",
    "recruitee:personio"
  ],
  "titleFilter": "",
  "remoteOnly": false,
  "includeDescription": true,
  "trackChanges": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Every role returned by this run as clean JSON.

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

The same roles as a CSV download.

## `runSummary` (type: `string`):

Counts per company, change totals, and any board that could not be reached.

# 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": [
        "greenhouse:stripe",
        "lever:plaid",
        "ashby:ramp",
        "smartrecruiters:Visa",
        "bamboohr:jobandtalent",
        "recruitee:personio"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("datalayer/ats-jobs-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": [
        "greenhouse:stripe",
        "lever:plaid",
        "ashby:ramp",
        "smartrecruiters:Visa",
        "bamboohr:jobandtalent",
        "recruitee:personio",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("datalayer/ats-jobs-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": [
    "greenhouse:stripe",
    "lever:plaid",
    "ashby:ramp",
    "smartrecruiters:Visa",
    "bamboohr:jobandtalent",
    "recruitee:personio"
  ]
}' |
apify call datalayer/ats-jobs-api --silent --output-dataset

```

## MCP server setup

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