# ATS Jobs Scraper — Greenhouse + Lever + Ashby (Unified) (`studious_allergy_mig/ats-jobs-scraper`) Actor

Scrapes public job postings from Greenhouse, Lever, and Ashby company boards and normalizes them into one consistent schema.

- **URL**: https://apify.com/studious\_allergy\_mig/ats-jobs-scraper.md
- **Developed by:** [Conor G](https://apify.com/studious_allergy_mig) (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 $2.00 / 1,000 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/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, SmartRecruiters, Workable, Recruitee, Personio, BambooHR

Scrape live job postings from any company that publishes its careers page on
**Greenhouse**, **Lever**, **Ashby**, **Workday**, **SmartRecruiters**,
**Workable**, **Recruitee**, **Personio**, or **BambooHR** — nine of the most
common applicant tracking systems — and get them back in one consistent,
normalized schema. Give it a list of company names or career page URLs; it
figures out which ATS (or ATSes) each company uses and pulls every open role.

### Why this actor

Most job-board scrapers on the Store are built for a single ATS. A real
target list — especially outside big US tech — spans several of these
platforms, and stitching together different output shapes from separate
actors gets old fast. This actor:

- Covers **nine platforms** in one run, one schema, one dataset.
- Talks only to each platform's **public, unauthenticated JSON (or XML, for
  Personio) API** — no browser rendering, no HTML scraping, no cookies.
  That means it's fast, cheap to run, and far less likely to break when a
  company redesigns its careers page.
- Auto-detects which platform a company is on when you give it a bare name,
  or trusts your input outright when you give it a direct board URL.
- De-duplicates postings (by platform + job ID, so a company that happens to
  be found on two platforms never produces duplicate rows) and gives you a
  clear per-company status report (found where, how many jobs, and why a
  company came back empty) so you're never left guessing.

### What it scrapes

| Platform | Endpoint used | Bare-slug auto-detect |
|---|---|---|
| Greenhouse | `boards-api.greenhouse.io/v1/boards/{slug}/jobs` | Yes |
| Lever | `api.lever.co/v0/postings/{slug}` | Yes |
| Ashby | `api.ashbyhq.com/posting-api/job-board/{slug}` | Yes |
| SmartRecruiters | `api.smartrecruiters.com/v1/companies/{slug}/postings` | Yes |
| Workable | `apply.workable.com/api/v1/widget/accounts/{slug}` | Yes |
| Recruitee | `{slug}.recruitee.com/api/offers/` | Yes |
| Personio | `{slug}.jobs.personio.de/xml` | Yes |
| BambooHR | `{slug}.bamboohr.com/careers/list` | Yes |
| Workday | `{tenant}.{shard}.myworkdayjobs.com/wday/cxs/{tenant}/{site}/jobs` | No — paste a board URL |

These are the same public feeds each platform's own embeddable job widget
uses — they're meant to be read programmatically, are not
authentication-gated, and are stable across a company's own page redesigns.

**Workday has no guessable bare-slug shape.** A Workday board URL encodes a
tenant, a site name, and a shard (`wd1`, `wd3`, `wd5`, ...) that are all
independent of each other and of the company's name, so there's nothing to
probe from a bare company name. Paste the board URL instead (e.g.
`https://intel.wd1.myworkdayjobs.com/External`) and the actor extracts all
three parts automatically.

#### Deliberately not supported

**Teamtailor.** Its public careers-page data is served through an endpoint
that requires an API key/token rather than being openly readable, unlike the
platforms above. Since this actor only talks to genuinely public,
unauthenticated endpoints — no accounts, no keys, no per-customer
credentials — Teamtailor is out of scope rather than half-supported.

**JazzHR.** Its public board (`{slug}.applytojob.com/apply`) is
server-rendered HTML with the job list baked directly into the page markup —
there is no accompanying JSON or XML endpoint behind it at a guessable URL.
JazzHR does offer a "Listings XML feed" and a REST API, but both are opt-in
per customer and gated behind a customer-specific API key, not something
reachable from a bare company slug the way the platforms above are. Scraping
the HTML itself would mean parsing markup rather than a stable data feed —
the same reason this actor doesn't touch iCIMS — so JazzHR is out of scope.

**iCIMS.** Investigated and rejected. iCIMS career sites (e.g.
`{company}.icims.com`) don't expose a stable, guessable, unauthenticated JSON
endpoint: requests to their job-search pages come back with a `302` into a
session-bootstrapping servlet and a fresh `JSESSIONID` cookie, and whatever
JSON eventually loads client-side is undocumented, varies by iCIMS module
(Recruit/Onboard/Connect/Apply) and by customer, and changes between iCIMS
platform releases. That's session-dependent, effectively browser-rendered
territory, not a public API this actor can rely on — so iCIMS is out of
scope.

### Input

| Field | Type | Description |
|---|---|---|
| `companies` | array of strings | **Required.** Each entry is either a bare slug (e.g. `"stripe"`) — every bare-slug-capable platform is probed automatically — or a full board URL naming one platform explicitly (e.g. `"https://boards.greenhouse.io/stripe"`, `"https://jobs.lever.co/palantir"`, `"https://jobs.ashbyhq.com/ramp"`, `"https://jobs.smartrecruiters.com/Visa"`, `"https://apply.workable.com/huggingface"`, `"https://helloprint.recruitee.com"`, `"https://personio.jobs.personio.de"`, `"https://flyio.bamboohr.com/careers/list"`, `"https://intel.wd1.myworkdayjobs.com/External"`). |
| `includeDescription` | boolean | When `true`, each item includes the full plain-text job description (not available for SmartRecruiters or BambooHR — see limitations). Off by default to keep runs fast and cheap when you only need title/location/link. |
| `maxJobsPerCompany` | integer | Caps how many postings are pulled per company, per platform it's found on. Default `200`. |

#### Example input — bare slugs (auto-detect across 8 platforms)

```json
{
  "companies": ["stripe", "plaid", "huggingface", "helloprint"],
  "includeDescription": false,
  "maxJobsPerCompany": 100
}
```

#### Example input — mixed, including a Workday board URL

```json
{
  "companies": [
    "stripe",
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/ramp",
    "https://jobs.smartrecruiters.com/Visa",
    "https://intel.wd1.myworkdayjobs.com/External",
    "https://personio.jobs.personio.de",
    "https://flyio.bamboohr.com/careers/list"
  ],
  "includeDescription": true,
  "maxJobsPerCompany": 100
}
```

### Output — normalized schema

Every item in the dataset — regardless of which platform it came from —
has this shape:

| Field | Type | Notes |
|---|---|---|
| `platform` | string | `"greenhouse"`, `"lever"`, `"ashby"`, `"workday"`, `"smartrecruiters"`, `"workable"`, `"recruitee"`, `"personio"`, or `"bamboohr"` |
| `companySlug` | string | The slug used to query the ATS (`{tenant}/{site}` for Workday) |
| `companyName` | string | null | Human-readable company name, when the platform exposes it (Greenhouse and Workable do; the others generally don't) |
| `jobId` | string | Platform-native job/posting ID |
| `title` | string | Job title |
| `department` | string | null | Department/function, when available |
| `team` | string | null | Sub-team, when available |
| `locations` | array of strings | All listed locations for the posting |
| `isRemote` | boolean | null | Best-effort: platform's own remote flag when present, else inferred from location text (e.g. contains "Remote") |
| `employmentType` | string | null | e.g. "Full-time", "FullTime", when the platform reports it |
| `applyUrl` | string | Absolute URL to the live application page |
| `postedAt` | string | null | ISO timestamp, when the platform reports it (always `null` for Workday and BambooHR — see limitations) |
| `updatedAt` | string | null | ISO timestamp, when the platform reports it |
| `description` | string | null | Full plain-text description, only when `includeDescription: true` (never populated for SmartRecruiters or BambooHR) |
| `scrapedAt` | string | ISO timestamp of when this actor fetched the item |

#### Sample output item

```json
{
  "platform": "ashby",
  "companySlug": "ramp",
  "companyName": null,
  "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "title": "Security Engineer, Cloud",
  "department": "Engineering",
  "team": "Backend",
  "locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)", "Miami, FL"],
  "isRemote": true,
  "employmentType": "FullTime",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
  "postedAt": "2026-04-07T17:12:35.753+00:00",
  "updatedAt": null,
  "description": null,
  "scrapedAt": "2026-07-29T07:49:48.333Z"
}
```

#### Run summary

At the end of a run, a per-company status summary is written to the
default key-value store under the key `COMPANY_SUMMARY` — which platform(s)
each company was found on, how many jobs came back from each, and a clear
message when a company wasn't found on any platform (e.g. `Company not
found on greenhouse (tried slug "plaid")`). The run only fails (non-zero
exit) when **every** company in your list failed to resolve on any
platform — a single bad slug in a long list won't sink the run.

### Use cases

- **Recruiting intel / competitive hiring analysis** — track which teams and
  roles a set of companies is actively hiring for, and how that shifts over
  time, across a company list that spans more than one region's dominant
  ATS (Workday and SmartRecruiters skew toward enterprise/EU employers that
  Greenhouse/Lever/Ashby lists usually miss).
- **Job aggregation** — build a niche job board or newsletter by pulling
  live postings from a curated list of companies without maintaining
  scrapers for each ATS separately.
- **Sales signals ("who's hiring")** — open roles are a well-known proxy for
  growth, new initiatives, and budget — useful firmographic signal for
  sales and market research.

### Pricing

This actor is billed **pay-per-result**: one dataset item = one job
posting. Enabling `includeDescription` does not change pricing, only the
amount of data returned per item.

### Limitations (please read)

- **Only the nine platforms listed above are supported.** Teamtailor,
  JazzHR, and iCIMS are deliberately excluded (see above — none of them
  expose a genuinely public, unauthenticated JSON/XML endpoint at a
  guessable URL). LinkedIn-only postings and custom in-house career pages
  are also out of scope and will simply come back "not found."
- **Bare-slug auto-detection relies on the slug matching across
  platforms.** A company's slug is sometimes a different string on
  different platforms. If auto-detection misses a platform you know a
  company uses, pass the direct board URL instead.
- **Workday requires a board URL, not a bare slug.** See above for why.
- **`companyName` is often `null`.** Only Greenhouse and Workable reliably
  return a company display name; the others' public APIs generally don't
  expose one, so those items fall back to the slug you supplied.
- **Workday multi-location jobs report a placeholder, not the locations.**
  When a Workday posting spans more than one site, Workday's list endpoint
  returns its own summary string (for example `"2 Locations"`) instead of the
  individual cities, so that's what lands in `locations`. Single-location
  Workday jobs are exact. If you need the individual sites for those postings,
  say so in the Issues tab — resolving them means an extra request per job,
  which costs you more per run, so it isn't on by default.
- **`isRemote` is best-effort.** It uses the platform's own remote flag when
  present, otherwise infers from location text — this can misclassify
  unusual location strings.
- **An empty result for a platform doesn't always mean "not on that
  platform."** Several platforms (Lever, Ashby, SmartRecruiters) return an
  empty list both when a company isn't there and when a company is there
  but currently has zero open roles; this actor can't fully distinguish the
  two from an empty response.
- **`postedAt` is always `null` for Workday.** Workday's job feed only
  exposes a relative string ("Posted Yesterday", "Posted 30+ Days Ago"),
  never a real timestamp, so there's nothing reliable to normalize into
  ISO 8601.
- **`description` is never populated for SmartRecruiters or BambooHR**,
  even with `includeDescription: true`. Neither platform's list endpoint
  returns one, and fetching per-job detail would multiply request volume by
  job count.
- **`postedAt` is always `null` for BambooHR**, for the same reason —
  BambooHR's list endpoint doesn't report a posting date; it's only present
  behind the same per-job detail call that also holds the description.
- **BambooHR locations can be sparse or missing.** Its list endpoint
  frequently reports `null` city/state for jobs that do have a location set
  in the company's BambooHR account — this is a gap in what BambooHR's own
  public feed exposes, not something this actor can recover without the
  same per-job detail call mentioned above.
- **Data reflects a single point-in-time snapshot** of each ATS's public
  feed at the moment the actor ran — postings can close or change
  immediately after.

# Actor input Schema

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

List of companies to scrape. Each entry can be a bare career-page slug (e.g. "stripe") — in which case the actor auto-detects whether the company is on Greenhouse, Lever, Ashby, SmartRecruiters, Workable, Recruitee, Personio, and/or BambooHR — or a full job board URL, in which case only that specific platform is checked. Workday has no guessable bare-slug shape, so it's only reached by pasting a board URL (e.g. "https://intel.wd1.myworkdayjobs.com/External"). Other URL examples: "https://boards.greenhouse.io/stripe", "https://jobs.lever.co/palantir", "https://jobs.ashbyhq.com/ramp", "https://jobs.smartrecruiters.com/Visa", "https://apply.workable.com/huggingface", "https://helloprint.recruitee.com", "https://personio.jobs.personio.de", "https://flyio.bamboohr.com/careers/list".

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

When enabled, each item includes the full plain-text job description. Leave off for lighter, faster runs when you only need titles/locations/links.

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

Caps how many job postings are pulled per company, per platform it's found on. Useful for large companies with hundreds of open roles.

## Actor input object example

```json
{
  "companies": [
    "stripe",
    "https://jobs.lever.co/palantir",
    "https://intel.wd1.myworkdayjobs.com/External"
  ],
  "includeDescription": false,
  "maxJobsPerCompany": 200
}
```

# 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/palantir",
        "https://intel.wd1.myworkdayjobs.com/External"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("studious_allergy_mig/ats-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 = { "companies": [
        "stripe",
        "https://jobs.lever.co/palantir",
        "https://intel.wd1.myworkdayjobs.com/External",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("studious_allergy_mig/ats-jobs-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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/palantir",
    "https://intel.wd1.myworkdayjobs.com/External"
  ]
}' |
apify call studious_allergy_mig/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=studious_allergy_mig/ats-jobs-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/Vv4VAVMhhoWsUlCyb/builds/5arNZW43acT4tEbQj/openapi.json
