# ATS & Career Page Jobs Scraper (Greenhouse, Lever, Workday) (`scrapebench/ats-career-page-jobs-scraper`) Actor

Give it a company domain, get every open job from the ATS behind its career page — Greenhouse, Lever, Ashby, Workday and SmartRecruiters in one schema.

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

## Pricing

$0.001 / job posting

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 & Career Page Jobs Scraper (Greenhouse, Lever, Workday)

### Pain points

- Several job-board Actors are published one per applicant-tracking system, so a mixed list of employers means running two or three of them and reconciling different output shapes.
- Most want the ATS board slug rather than the company, which you have to look up per employer before you can start.
- Career pages increasingly render the job list in JavaScript, so a generic page crawler comes back empty or partial.
- Boards get large — a single Workday tenant tested held 2,000 open roles — so a per-company cap and a visible total matter more than they sound.

### What we solve

- One input — a company domain, a careers URL, or an explicit board like 'greenhouse:stripe' — resolves to the right ATS automatically.
- One output schema across all five systems, so Greenhouse and Workday rows sort and filter together.
- Hundreds of companies in a single run, with each company's outcome written to the run log.
- A 'posted on or after' filter turns the same run into a weekly new-openings feed.
- A company we cannot resolve returns a free, unbilled row naming the exact input that would work — never a silent zero.

### Summary

Submit a company domain — acme.com — and this Actor works out which applicant-tracking system runs that company's career page, then returns every open job from it. Greenhouse, Lever, Ashby, Workday and SmartRecruiters all land in one schema: title, department, team, location, remote flag, employment type, posting date, requisition ID and the apply link. Submit hundreds of domains in a single run. Every row says how the board was matched to the company (`detected_by`) and cites the job URL the board itself served, so a result is auditable rather than assembled. Built for recruiters sourcing roles, sales teams treating a new posting as a hiring signal, and job-board aggregators who would otherwise run one Actor per ATS and merge the output by hand.

### Who it's for

- Recruiters and sourcers tracking open roles across a target list of employers
- Sales and RevOps teams using new job postings as a hiring/expansion signal
- Job-board and aggregator operators ingesting employer boards at scale
- Talent-market and compensation analysts measuring hiring by department and location

### How to use

Set the input, run the actor, and collect results from the run's dataset (export to JSON/CSV/Excel, or pull via the Apify API). Example input:

```json
{
  "companies": [
    "stripe.com",
    "notion.so",
    "lever:gopuff"
  ],
  "maxResults": 100,
  "postedAfter": "2026-08-01"
}
```

See **Inputs** below for every available field.

### What you get

One row per record:

| Field | Description |
|---|---|
| `company` | The value you submitted — domain, careers URL or explicit board |
| `company_name` | Employer name as the board states it (Greenhouse and SmartRecruiters publish it; Ashby, Lever and Workday do not, so it is null there) |
| `ats` | greenhouse, lever, ashby, smartrecruiters or workday |
| `ats_slug` | The board identifier on that ATS |
| `detected_by` | How the board was matched: explicit, careers\_page, or slug\_match |
| `job_id` | The board's own job identifier — stable across runs, so it de-duplicates |
| `title` | Posted job title |
| `department / team` | Department and team or job function, where the board publishes them |
| `location / locations` | Primary location plus any additional locations on the same posting |
| `remote` | The board's own remote flag where it publishes one (Ashby, SmartRecruiters); otherwise read from the board's own location wording, e.g. 'Remote - US' or 'Hybrid - London'. Null when the board says nothing either way |
| `employment_type` | Full time, contract, intern and so on, where published |
| `posted_at` | Publish timestamp — Greenhouse, Lever, Ashby and SmartRecruiters |
| `posted_text` | The board's own wording when it gives no date, e.g. Workday's 'Posted Today' |
| `updated_at` | Last-modified timestamp, where published |
| `apply_url` | Where a candidate applies |
| `requisition_id` | The employer's internal requisition reference, where published |
| `description` | Full job text — only when 'Include the full job description' is on |
| `source_url` | The job page as the board serves it |

Sample:

```json
{
  "source_url": "https://job-boards.greenhouse.io/vercel/jobs/6136160004",
  "company": "vercel.com",
  "company_name": "Vercel",
  "ats": "greenhouse",
  "ats_slug": "vercel",
  "detected_by": "slug_match",
  "job_id": "6136160004",
  "title": "Account Executive, Commercial",
  "department": "Account Executive",
  "team": null,
  "location": "Hybrid - London",
  "locations": [
    "Office - London"
  ],
  "remote": null,
  "employment_type": null,
  "posted_at": "2026-08-06T12:50:10-04:00",
  "posted_text": null,
  "updated_at": "2026-08-18T18:06:19-04:00",
  "apply_url": "https://job-boards.greenhouse.io/vercel/jobs/6136160004",
  "requisition_id": null,
  "description": null
}
```

### Inputs

| Field | Required | Type | Default | Description |
|---|---|---|---|---|
| `company` | no | string | `"stripe.com"` | A company domain (acme.com), a careers URL, or an explicit board such as 'greenhouse:stripe' or 'lever:gopuff'. The Actor works out which applicant-tracking system the company uses. Kept forever alongside 'companies' — saved tasks and API callers send it. |
| `companies` | no | array | `["stripe.com", "notion.so", "greenhouse:figma"]` | Look up many companies in ONE run instead of starting a run per company. Takes the same values as 'Company'. Combined with 'Company' if you fill both, and duplicates are removed. Billing is unchanged: you are charged per job returned, not per company submitted. |
| `maxResults` | no | integer | `1000` | Cap on jobs returned PER COMPANY, not per run — so a batch cannot silently return only the first rows overall and read as 'these companies aren't hiring'. When a board holds more, the run log states the board's own total next to what was returned. |
| `postedAfter` | no | string | — | Keep only jobs published on or after this date (YYYY-MM-DD) — this is what turns the Actor into a weekly new-openings feed. Jobs whose board states no publish date (Workday says 'Posted Today' rather than a date) are KEPT, because dropping them would hide every Workday job and read as 'not hiring'. |
| `includeDescription` | no | boolean | `false` | Off by default: descriptions are the bulk of the payload (one large board is ~16 KB per job) and most buyers filter on title, team and location first. Turn it on when you need the text. |
| `proxyConfiguration` | no | object | `{"useApifyProxy": false}` | Optional. These are public vendor APIs and the plain path is the fastest one, so a proxy is genuinely not needed for the boards themselves. Enable it only if a company's own careers page blocks the detection step. |

### Pricing (Pay Per Event)

You pay per result (`job-posting`) — **no charge for empty runs**. Example: **1000 job postings** at *$0.00125/result* ≈ **$1.25**.

Scheduled price change: $0.001 per job ($1.00 per 1,000) until 21 September 2026, $0.00125 ($1.25 per 1,000) from that date. You are charged per job returned, not per company submitted — 1,000 jobs cost the same whether they come from one company or fifty. Marker rows explaining an unresolved company are free. Apify platform usage (compute) is billed separately per your plan.

### Use cases

- Hiring-signal alerts — schedule a daily run over your account list with 'Posted on or after' set to yesterday, and get only the roles opened since the last run.
- Sourcing a target list — submit 200 employer domains and get every open engineering role across all of them in one dataset.
- Aggregator ingestion — pull whole boards by ATS and reconcile them on the stable per-board job ID rather than on titles.
- Market research — count openings by department and location across a peer group to see where a sector is hiring.

### Why this actor

- Takes a company domain, so you do not have to look up an ATS board slug per employer first.
- Includes Workday, which several ATS Actors leave out and which holds some of the largest boards — one tenant tested held 2,000 open roles.
- Refuses to guess: a board is only used when you named it, when the company's own site links it, or when the vendor states a company name that matches the domain — so you are never handed another company's jobs.
- Every row carries the board's own job URL, job ID and how the board was matched (`detected_by`), so a result is auditable and de-duplicates cleanly across runs.
- $1.25 per 1,000 jobs from 21 September 2026 ($1 until then), charged per job returned rather than per company submitted — below both comparable Actors benchmarked on 2026-09-06, at $1.50 and ~$2 per 1,000.

### Limitations & updates

Covers the five applicant-tracking systems listed; a company on any other system (or hosting its own board) will not resolve, and returns a free marker row saying so. Automatic detection from a domain worked for 11 of 14 companies tested on 2026-09-06 — the misses were career pages rendered entirely in JavaScript, which expose no ATS link to read; those still work if you pass the board or careers URL directly. Workday in particular needs its careers URL rather than a bare domain, because its tenant, pod and site name cannot be derived from a company domain. Fields are returned only where the board publishes them: employer name comes from Greenhouse and SmartRecruiters but not Ashby, Lever or Workday; posting dates come from all but Workday. Data is whatever the employer's board shows at the moment of the run — this Actor adds no candidate data, no salary estimates and no personal information.

### FAQ

**Which applicant-tracking systems are supported?**

Greenhouse, Lever, Ashby, SmartRecruiters and Workday. Each is read from that vendor's own public job-board endpoint, so there is no login, no API key and no browser rendering involved.

**How does it know which ATS a company uses?**

Three ways, tried in that order. If you name the board — 'greenhouse:stripe', or a careers URL — that wins. Otherwise it looks for an ATS link on the company's own careers page, which proves the board belongs to them. Failing that it derives a likely board name from the domain and accepts it only if the vendor states a company name that matches. Every row records which of the three applied, in `detected_by`.

**What happens when a company cannot be resolved?**

You get one free, unbilled row explaining why and naming the input that would work, for example 'lever:acme'. It is never a silent empty result, because 'we could not find the board' and 'this company is not hiring' are different answers and only one of them is our fault. On a 14-company test on 2026-09-06, 11 resolved from the domain alone and the three that did not were companies whose careers page is entirely JavaScript.

**Why is posted\_at empty on some rows?**

Workday publishes a relative phrase — 'Posted Today' — rather than a date. Rather than invent a timestamp, those rows leave `posted_at` null and keep the board's own wording in `posted_text`. The other four systems publish a real timestamp. For the same reason, the 'Posted on or after' filter keeps undated rows instead of dropping them, which would hide every Workday job.

**Can I get only jobs posted since my last run?**

Yes. Set 'Posted on or after' to a date and schedule the Actor; each run returns the roles opened since then. Rows carry the board's own stable job ID, so de-duplicating across runs is exact rather than title matching.

**Do I pay per company or per job?**

Per job returned. Submitting 50 companies that collectively have 1,000 open roles costs the same as one company with 1,000 roles. Companies that resolve to nothing, and boards with no open jobs, are free.

**How many jobs can one company return?**

Whole boards, and they get large — on 2026-09-06 the boards in the example input held 617, 132 and 765 open roles, and one Workday tenant tested held 2,000. Use 'Maximum jobs per company' to cap it; the cap applies per company, not per run, and the run log always states the board's own total next to what was returned so a cap can never read as completeness.

**Why is `remote` sometimes null instead of false?**

Because the board did not say. Ashby and SmartRecruiters publish a real remote flag; Greenhouse and Workday publish none, so for those the value is read from the board's own wording — 'Remote - US' gives true, 'Hybrid - London' gives false. A posting that just says 'Dublin' says nothing about remote work, and this Actor returns null there rather than asserting false. Filtering for remote roles gives the same answer either way; the difference only shows when you filter for non-remote, and there null is the honest value.

**What is not in the output?**

Salary, skills, requirements and seniority. Not an oversight: on 2026-09-06 the three boards checked published pay data on 0 of 617 Greenhouse jobs, 0 of 132 Ashby jobs and 0 of 40 Lever jobs. Those columns would be present and empty, so this Actor does not carry them rather than implying data it cannot get. Everything returned comes from the board; nothing here is inferred by a model.

**Is a proxy needed?**

No. These are public vendor endpoints and the direct path is the fastest one, so the proxy input is off by default. Turn it on only if a particular company's own careers page blocks the detection step.

### Which actor to choose

Other Actors in the portfolio that pair with this one:

- **Business Email & Phone Contact Details Scraper** — You have the employers and now need contact details for outreach rather than their open roles.
- **Multi-State Corporate & Business Entity Registry** — You want newly formed companies to build the target list this Actor then checks for hiring.

### Guides & use cases

Written up on **[scrapebench.dev](https://scrapebench.dev)** — the bench that runs and verifies this actor against the live source every night:

- **How-to:** [How to run ATS & Career Page Jobs Scraper (Greenhouse, Lever, Workday)](https://scrapebench.dev/guides/how-to-ats-career-page-jobs-scraper/)
- **Use case:** [Every Open Job at a Company, From Its Career Page](https://scrapebench.dev/use-cases/open-jobs-at-a-company-by-domain/)
- **Use case:** [New Job Postings This Week Across a Company List](https://scrapebench.dev/use-cases/new-job-postings-this-week/)
- **Use case:** [Open Roles From a Workday Careers Site](https://scrapebench.dev/use-cases/workday-employer-open-roles/)

More actors, coverage and nightly verification results: **[scrapebench.dev](https://scrapebench.dev)**

### Works with AI assistants (MCP)

Callable as an MCP tool, so Claude, Cursor, VS Code Copilot and other MCP clients can run it directly. Grab the config from the [MCP tab](https://apify.com/scrapebench/ats-career-page-jobs-scraper/api/mcp) on this page — Apify hosts the server and keeps that snippet current, and OAuth signs you in on first connect, so no API token goes in your config file.

Then just ask:

> "List every open job at stripe.com and notion.so, with department and location."

Those two resolve straight from the domain. For a company whose careers page is JavaScript-only, name the board instead — 'lever:gopuff' — or paste its careers URL. Runs started this way bill exactly like any other run.

# Actor input Schema

## `company` (type: `string`):

A company domain (acme.com), a careers URL, or an explicit board such as 'greenhouse:stripe' or 'lever:gopuff'. The Actor works out which applicant-tracking system the company uses. Kept forever alongside 'companies' — saved tasks and API callers send it.

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

Look up many companies in ONE run instead of starting a run per company. Takes the same values as 'Company'. Combined with 'Company' if you fill both, and duplicates are removed. Billing is unchanged: you are charged per job returned, not per company submitted.

## `maxResults` (type: `integer`):

Cap on jobs returned PER COMPANY, not per run — so a batch cannot silently return only the first rows overall and read as 'these companies aren't hiring'. When a board holds more, the run log states the board's own total next to what was returned.

## `postedAfter` (type: `string`):

Keep only jobs published on or after this date (YYYY-MM-DD) — this is what turns the Actor into a weekly new-openings feed. Jobs whose board states no publish date (Workday says 'Posted Today' rather than a date) are KEPT, because dropping them would hide every Workday job and read as 'not hiring'.

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

Off by default: descriptions are the bulk of the payload (one large board is ~16 KB per job) and most buyers filter on title, team and location first. Turn it on when you need the text.

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

Optional. These are public vendor APIs and the plain path is the fastest one, so a proxy is genuinely not needed for the boards themselves. Enable it only if a company's own careers page blocks the detection step.

## Actor input object example

```json
{
  "company": "stripe.com",
  "companies": [
    "stripe.com",
    "notion.so",
    "greenhouse:figma"
  ],
  "maxResults": 1000,
  "includeDescription": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Every open job found on each company's board: title, team, location, posting date and the apply link.

# 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 = {
    "company": "stripe.com",
    "companies": [
        "stripe.com",
        "notion.so",
        "greenhouse:figma"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapebench/ats-career-page-jobs-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 = {
    "company": "stripe.com",
    "companies": [
        "stripe.com",
        "notion.so",
        "greenhouse:figma",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("scrapebench/ats-career-page-jobs-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 '{
  "company": "stripe.com",
  "companies": [
    "stripe.com",
    "notion.so",
    "greenhouse:figma"
  ]
}' |
apify call scrapebench/ats-career-page-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapebench/ats-career-page-jobs-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/3DIOJnAPhKeljFQwR/builds/m1aAl5seAgLyQMzQh/openapi.json
