# Job Postings Scraper — Greenhouse, Lever, Ashby & Workday (`cleanfeed/ats-job-postings-scraper`) Actor

Scrape open roles from companies' own job boards across six applicant tracking systems: Greenhouse, Lever, Ashby, SmartRecruiters, Workable and Personio. One normalised row per role — title, department, location, apply URL and full description. Auto-detects which system each company uses.

- **URL**: https://apify.com/cleanfeed/ats-job-postings-scraper.md
- **Developed by:** [Yaniv van der Stigchel](https://apify.com/cleanfeed) (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 $0.95 / 1,000 job posting returneds

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 Postings Scraper — with the apply link aggregators lost

Open roles from the applicant tracking system each company actually runs, not
from an aggregator's copy. Pass a list of companies; get one normalised row per
role across six different systems.

**Every row carries a working `applyUrl`.** LinkedIn removed the external apply
URL from what aggregators can see, permanently — the largest LinkedIn jobs Actor
on this Store told a user in a public review reply that they are removing the
field entirely because it is "only possible post-login". Company job boards
never had that problem: the apply link is native there, always present, always
correct.

A job posting you cannot apply to is a row, not a lead.

### What it does

- Scrape job postings from Greenhouse, Lever, Ashby, SmartRecruiters, Workable and Personio
- Get a working apply link on every role
- Get every open role at a list of companies in one dataset
- Track hiring activity as a buying signal
- Monitor competitor headcount growth on a schedule
- Export job postings to JSON or CSV

### Three things aggregators cannot give you

**The apply URL.** Covered above. It is native on a company's own board.

**Deduplication.** One row per role, keyed on the board's own job id, so the
same role does not arrive three times across three sources — and you are not
charged three times for it.

**The company name, on every row.** Obvious, and routinely missing from
aggregator output.

### Why this is not one more job scraper

Six systems publish the same concept — an open role — in six different shapes,
at six different URLs, using different names for the same field. Greenhouse
nests locations under `offices`; Lever puts everything in `categories`; Ashby
alone states whether a role is remote; Personio returns XML. A buyer wanting
every open role at 500 companies has to reconcile all of it.

**Auto-detection is the point.** You will not know which system each company on
your list uses, and finding out by hand is the work you are paying to avoid.
Pass a bare slug and each board is tried in turn until one answers.

Every row carries `atsPlatform`, so you can see where it came from — and it is
useful in itself, because knowing a company runs Greenhouse tells you how to
apply programmatically.

### You only pay for roles

| `errorCode` | Meaning | Charged |
|---|---|---|
| — (`success: true`) | A role | Yes |
| `no-open-roles` | Board found and empty — not hiring publicly | No |
| `no-board-found` | No public board on any supported system | No |
| `unparseable-company` | Input was not a slug or careers URL | No |

Those two failures are different answers and the distinction matters. **No
roles** means the company was located and is not hiring. **No board** means we
could not find where they publish — they may use an unsupported system, or host
roles on their own site.

### Input

| Field | Required | Description |
|---|---|---|
| `companies` | yes | Slugs (`stripe`) or careers URLs (`https://jobs.lever.co/spotify`) |
| `platform` | no | Default `auto`. Set it only if every company uses the same system |
| `maxJobsPerCompany` | no | Default 1000. Your cost ceiling |
| `maxConcurrency` | no | 1–15, default 5 |

```json
{
  "companies": ["stripe", "https://jobs.lever.co/spotify", "https://jobs.ashbyhq.com/ramp"],
  "platform": "auto",
  "maxJobsPerCompany": 500
}
```

A careers URL skips detection, so it is both faster and cheaper than a bare slug
when you already know the board.

### Output

Every row has the same fields whether it succeeded or failed, so you can select
columns without branching.

| Field | Type | Description |
|---|---|---|
| `success` | boolean | True when the row carries a role |
| `company` | string | Company slug |
| `atsPlatform` | string | Which system the role came from |
| `jobId` | string | Board's id — stable, deduplicates across runs |
| `title` | string | Role title |
| `jobUrl` | string | Canonical page |
| `applyUrl` | string | Where an application is submitted |
| `department` | string | As the board states it |
| `team` | string | Where published |
| `location` | string | Primary location |
| `allLocations` | array | Every location, where the board says so |
| `isRemote` | boolean | Only when stated; null when not |
| `employmentType` | string | Full-time, contract, internship |
| `publishedAt` | string | ISO 8601 |
| `updatedAt` | string | ISO 8601, where published |
| `descriptionText` | string | Full description as plain text |
| `errorCode` | string | Null on success |
| `errorMessage` | string | Human-readable explanation |

```json
{
  "success": true,
  "company": "ramp",
  "atsPlatform": "ashby",
  "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "title": "Security Engineer, Cloud",
  "jobUrl": "https://jobs.ashbyhq.com/ramp/34413f8d",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d/application",
  "department": "Engineering",
  "team": "Backend",
  "location": "New York, NY (HQ)",
  "allLocations": ["New York, NY (HQ)", "Remote (US)"],
  "isRemote": false,
  "employmentType": "FullTime",
  "publishedAt": "2026-08-14T09:12:00Z",
  "updatedAt": null,
  "descriptionText": "About the role\n\nYou will secure our cloud estate...",
  "errorCode": null,
  "errorMessage": null
}
```

### Use it from an AI agent (MCP)

This Actor is callable as a tool through the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp), so Claude, ChatGPT, Cursor and VS Code can run it directly.

Add the server to your MCP client:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com",
      "headers": {
        "Authorization": "Bearer <YOUR_APIFY_TOKEN>"
      }
    }
  }
}
```

Then ask for what you want in plain language — for example *“list every open role at this company”* — and the agent calls `cleanfeed/ats-job-postings-scraper` with the right input. Every output field is described in the dataset schema, so the agent knows what it is getting back before it runs anything.

### Call it from code

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("cleanfeed/ats-job-postings-scraper").call(run_input={
    "companies": ["stripe"],
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["success"]:
        print(item)
```

#### JavaScript

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });

const run = await client.actor('cleanfeed/ats-job-postings-scraper').call({
    companies: ["stripe"],
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.filter((i) => i.success));
```

#### cURL

```bash
curl -X POST "https://api.apify.com/v2/acts/cleanfeed~ats-job-postings-scraper/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
  -H 'Content-Type: application/json' \
  -d '{"companies": ["stripe"]}'
```

### Related actors

| If you need | Use |
|---|---|
| Contact details for those same companies | [Verified Website Contacts](https://apify.com/cleanfeed/verified-website-contacts) |
| What those companies say on video | [YouTube Channel Transcript Scraper](https://apify.com/cleanfeed/youtube-channel-transcript-downloader) |

### Limitations

- **`isRemote` is null unless the board states it.** Only Ashby, Lever and Workable publish it. Inferring it from the word "remote" in a location gives confident wrong answers on "Remote-friendly office, Berlin", so the gap is reported instead.
- **Not every company is on a supported board.** Workday and Taleo are widely used in large enterprises and are not covered; many companies host roles on their own site. Those return `errorCode: no-board-found`.
- **Slugs differ per system.** A company can be `stripe` on Greenhouse and `stripe-inc` on Lever. Pass the careers URL when auto-detection misses.
- **Salary is not included.** Most of these boards do not publish it, and the few that do use inconsistent formats. A field that is null most of the time and wrong some of the rest is worse than absent.
- **SmartRecruiters' list endpoint carries no description**, so `descriptionText` is null for those rows.
- Unlisted Ashby roles are excluded — `isListed: false` means the company chose not to publish them.

### FAQ

#### Which applicant tracking systems are supported?

Greenhouse, Lever, Ashby, SmartRecruiters, Workable and Personio. Pass a bare company slug and each is tried in turn until one answers.

#### How do I find a company's slug?

Open their careers page — the slug is in the URL, so `boards.greenhouse.io/stripe` gives `stripe`. Pasting the whole URL also works and skips detection, which is faster.

#### What if a company has no open roles?

You get one row with `errorCode: no-open-roles` and are not charged. That is a real finding: a company that has stopped hiring is a signal in itself, and it is a different answer from `no-board-found`.

#### Can I get roles from LinkedIn or Indeed?

No, and deliberately. Those are aggregators with their own terms. This reads the boards companies publish themselves, which is also fresher — a role appears here before it propagates to an aggregator.

#### Why is salary missing?

Most of these boards do not publish it in their API, and the few that do use inconsistent formats. A field that is null most of the time and wrong some of the rest is worse than no field at all.

#### Does it need a proxy?

No. These are public job boards on ordinary infrastructure.

### Notes

Only public job boards that companies publish themselves are read. No
aggregators, no login, no scraping of a careers page behind an application
flow. A company with no public board is reported as such rather than guessed at.

# Actor input Schema

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

Company slugs (stripe, spotify) or careers-page URLs. A URL tells us which system to use; a bare slug is tried against every supported board until one answers.

## `platform` (type: `string`):

Leave on auto-detect unless every company on your list uses the same system. Auto tries each board in turn and stops at the first one with roles.

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

You are charged per role returned, so this is your cost ceiling.

## `maxConcurrency` (type: `integer`):

Companies processed in parallel. Higher is faster; lower is gentler on the boards.

## Actor input object example

```json
{
  "companies": [
    "stripe",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "platform": "auto",
  "maxJobsPerCompany": 1000,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

One row per role: title, department, location, employment type, apply URL and full description text.

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

Companies processed, roles delivered, and a breakdown of which ATS each company uses.

# 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",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/ramp"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("cleanfeed/ats-job-postings-scraper").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",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/ramp",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("cleanfeed/ats-job-postings-scraper").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",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/ramp"
  ]
}' |
apify call cleanfeed/ats-job-postings-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,cleanfeed/ats-job-postings-scraper"
        }
    }
}

```

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/hS2N5zQCQLCtSI7lw/builds/nBEdHu8DNSjS1pgFy/openapi.json
