# ATS Jobs Scraper: Greenhouse, Lever, Ashby & Workday (`impressionable_lupine/ats-jobs-scraper`) Actor

Scrape every open job from a company's ATS by domain or board URL. Auto-detects Greenhouse, Lever, Ashby, Workday, SmartRecruiters and Workable from the careers page and pulls official public JSON feeds. Flat job schema, salary, remote flag, filters. No proxies.

- **URL**: https://apify.com/impressionable_lupine/ats-jobs-scraper.md
- **Developed by:** [Majd Hijazin](https://apify.com/impressionable_lupine) (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 $2.00 / 1,000 job scrapeds

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## ATS Jobs Scraper — Greenhouse, Lever, Ashby, Workday, SmartRecruiters

**Give it a company domain, get every open job back.** ATS Jobs Scraper finds a company's careers page, detects which applicant tracking system (ATS) it uses, and pulls all open positions from that ATS's official public job feed. You get one clean, flat dataset row per job: title, department, location, remote flag, employment type, posted date, apply URL, salary when published, and optional full description.

- **6 ATSs, one schema:** Greenhouse jobs, Lever jobs, Ashby jobs, Workday jobs, SmartRecruiters jobs and Workable jobs all come out in the same format.
- **Domain in, jobs out:** pass `stripe.com` or `crowdstrike.com`. You don't need to know the board URL or slug.
- **Official JSON feeds, no HTML guessing:** data comes from `boards-api.greenhouse.io`, `api.lever.co`, `api.ashbyhq.com`, Workday's `cxs` JSON, `api.smartrecruiters.com` and Workable's public widget API. These are the same feeds the companies' own careers pages use, so the data is complete, current and stable.
- **No proxies, no browser:** plain HTTPS. This keeps runs fast (10+ companies in under a minute) and cheap.
- **$0.002 per job.** You pay $0.01 per company only when an ATS is actually found. Domains with no supported ATS cost nothing.

Common uses: **hiring-signal lead generation** (who is hiring SDRs or data engineers right now), **job board and aggregator feeds**, **competitor hiring intelligence**, **recruiting and sourcing pipelines**, **labor-market research** and **AI agents** that need structured job data.

***

### How it works

1. **Careers page scraper.** For each domain the Actor fetches the homepage, follows careers/jobs links (including `careers.` / `jobs.` subdomains) and common paths like `/careers`. It finds ATS references in links, embeds, iframes, scripts and Next.js payloads. It obeys `robots.txt` and fetches at most one HTML page per second per host.
2. **ATS slug probe (fallback).** If the site links to no ATS (common with custom React careers pages), the Actor tries the public ATS APIs with slugs derived from the domain, e.g. `stripe.com` → `greenhouse:stripe`. A probe counts only when the board's own company name, or text in its postings, matches the domain. This keeps it from attributing another company's board.
3. **Official feed.** Jobs are read from the ATS's public JSON API, paging where the API requires it (Workday, SmartRecruiters).
4. **Normalize, filter, dedupe.** Every record is mapped to the schema below. Your filters are applied, results are sorted newest first, and `maxJobsPerCompany` is enforced before anything is saved or charged.

| ATS | Detected from | Feed used | Posted date | Salary |
|---|---|---|---|---|
| Greenhouse | `boards.greenhouse.io`, `job-boards.greenhouse.io`, embed `?for=` | `boards-api.greenhouse.io/v1/boards/{slug}/jobs?content=true` | `first_published` (exact) | parsed from pay-range text |
| Lever (US & EU) | `jobs.lever.co`, `jobs.eu.lever.co` | `api.lever.co/v0/postings/{slug}` | `createdAt` (exact) | `salaryRange` (structured) |
| Ashby | `jobs.ashbyhq.com`, embedded boards | `api.ashbyhq.com/posting-api/job-board/{slug}` | `publishedAt` (exact) | compensation tiers (structured) |
| Workday | `*.wdN.myworkdayjobs.com`, `wdN.myworkdaysite.com` | `/wday/cxs/{tenant}/{site}/jobs` | "Posted N days ago" (day precision), exact with descriptions on | parsed from description |
| SmartRecruiters | `jobs.` / `careers.smartrecruiters.com` | `api.smartrecruiters.com/v1/companies/{id}/postings` | `releasedDate` (exact) | parsed from description |
| Workable | `apply.workable.com`, `*.workable.com` | `apply.workable.com/api/v1/widget/accounts/{slug}` | `published_on` (date) | parsed from description |

### Input

| Field | Type | What it does |
|---|---|---|
| `domains` | array of strings | Company websites, e.g. `["stripe.com", "linear.app"]`. ATS is auto-detected. |
| `boardUrls` | array of strings | Known boards: any board URL, or shorthands `greenhouse:figma`, `lever:palantir`, `lever:eu/slug`, `ashby:ramp`, `workday:visa/Visa@wd5`, `smartrecruiters:ServiceNow`, `workable:huggingface`. |
| `keyword` | string | Keeps jobs whose title, department or team contain **all** the words. `"machine learning" engineer` matches the quoted phrase and the word. |
| `location` | string | Keeps jobs whose location contains the text. Use `|` for alternatives: `London|Berlin|Remote`. |
| `remote` | boolean | Keeps only remote jobs. |
| `postedWithinDays` | integer | Keeps only jobs first published in the last N days. Jobs with no date are excluded. |
| `maxJobsPerCompany` | integer (default 500) | Caps the number of jobs saved and charged per company, newest first. |
| `includeDescription` | boolean (default false) | Adds the plain-text `descriptionText`. For Workday it also adds the exact posting date and employment type. |
| `detectBySlug` | boolean (default true) | Turns the slug-probe fallback on or off. |
| `maxConcurrency` | integer (default 4) | Number of companies processed in parallel. Each host is still rate-limited. |

**Example: engineering jobs posted in the last 30 days at four companies**

```json
{
  "domains": ["stripe.com", "linear.app", "crowdstrike.com"],
  "boardUrls": ["lever:palantir"],
  "keyword": "engineer",
  "postedWithinDays": 30,
  "maxJobsPerCompany": 50
}
```

**Example: every open job at a list of target accounts (lead generation)**

```json
{ "domains": ["ramp.com", "notion.so", "datadoghq.com", "airbnb.com", "servicenow.com", "visa.com"] }
```

### Output

One dataset item per job. Every field is always present; unknown values are `null`.

```json
{
  "company": "Ramp",
  "domain": "ramp.com",
  "ats": "ashby",
  "atsSlug": "ramp",
  "jobId": "b7514711-8a29-4bd2-a97a-967be14e336f",
  "title": "Senior Backend Engineer | Token Spend Management",
  "department": "Engineering",
  "team": "Backend",
  "location": "New York, NY (HQ); Remote (Canada); Remote (US)",
  "remote": true,
  "workplaceType": "hybrid",
  "employmentType": "Full-time",
  "postedAt": "2026-10-20T13:40:20.264Z",
  "postedAtApproximate": false,
  "updatedAt": null,
  "applyUrl": "https://jobs.ashbyhq.com/ramp/b7514711-8a29-4bd2-a97a-967be14e336f/application",
  "jobUrl": "https://jobs.ashbyhq.com/ramp/b7514711-8a29-4bd2-a97a-967be14e336f",
  "descriptionText": null,
  "salaryMin": 204000,
  "salaryMax": 280000,
  "salaryCurrency": "USD",
  "salaryPeriod": "YEAR",
  "salaryText": "$204K - $280K",
  "scrapedAt": "2026-10-20T18:14:55.512Z",
  "sourceUrl": "https://api.ashbyhq.com/posting-api/job-board/ramp?includeCompensation=true"
}
```

| Field | Meaning |
|---|---|
| `company`, `domain` | Company name (from the ATS, else the website) and the domain you submitted (`null` for direct boards). |
| `ats`, `atsSlug` | `greenhouse` · `lever` · `ashby` · `workday` · `smartrecruiters` · `workable`, plus the board identifier (Workday: `tenant/site`). |
| `jobId` | The ATS's own job ID (Workday: requisition ID). Stable across runs, so you can diff runs to find new or closed roles. |
| `title`, `department`, `team` | As published. |
| `location`, `remote`, `workplaceType` | Location text (multiple locations joined with `; `). `remote` is `true`/`false`/`null`. `workplaceType` is `remote`/`hybrid`/`onsite`/`null`. |
| `employmentType` | `Full-time`, `Part-time`, `Contract`, `Internship`, or the raw value. |
| `postedAt`, `postedAtApproximate`, `updatedAt` | ISO 8601 UTC. `postedAtApproximate` is `true` when the date comes from Workday's relative label. |
| `applyUrl`, `jobUrl` | Direct application link and public job page. |
| `descriptionText` | Plain text, only when `includeDescription` is on. |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod`, `salaryText` | Structured pay from Ashby and Lever, otherwise a pay range stated in the posting (`$150,000—$200,000 USD`). `null` when the employer publishes none. |
| `scrapedAt`, `sourceUrl` | When the record was fetched and from which public ATS endpoint. |

The run also writes an **`OUTPUT` record** in the key-value store with one entry per input. Each entry has the detected boards, the detection method (`careers_page`, `api_slug_probe`, `direct_input`), job counts, timing, a clear status (`ok`, `no_matching_jobs`, `no_ats_found`, `website_unreachable`, `ats_error`, `budget_exhausted`, `invalid_input`) and any errors. The dataset has three views: **Overview**, **Salaries** and **All fields**.

### Pricing (pay per event)

| Event | Price | When it is charged |
|---|---|---|
| `company-resolved` | **$0.01** | Once per input domain where at least one supported ATS board was found and read. |
| `job-scraped` | **$0.002** | Once per job saved to the dataset, after your filters and `maxJobsPerCompany`. |

You are **never charged** for domains with no supported ATS, unreachable websites, invalid input, or ATS detection on direct `boardUrls` (those pay only per job).

Examples:

- 10 company domains, 50 jobs each: 10 × $0.01 + 500 × $0.002 = **$1.10**
- All 3,614 open jobs at 12 large employers (Stripe, OpenAI, Visa, ServiceNow…, capped at 500 each): **$7.35**
- Checking 1,000 domains for engineering roles posted this week, 120 with matches: about **$1.20 + $0.002 per matching job**

**Spending limit.** Set *Maximum cost per run* (`maxTotalChargeUsd`) in the run options. The Actor never charges past it. When the budget runs out, it saves what has been paid for, marks the remaining companies `budget_exhausted` in `OUTPUT`, and finishes successfully. It does not charge a company fee unless the budget also covers that company's jobs. If the budget can't pay for even one job, the run stops right away with `BUDGET_TOO_LOW`.

### Call it from the API, code or an AI agent

**HTTP (synchronous, returns the jobs):**

```bash
curl -X POST "https://api.apify.com/v2/acts/impressionable_lupine~ats-jobs-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domains":["linear.app","figma.com"],"keyword":"engineer","maxJobsPerCompany":20}'
```

**JavaScript:**

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('impressionable_lupine/ats-jobs-scraper').call({ domains: ['stripe.com'], postedWithinDays: 7 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

**Python:**

```python
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("impressionable_lupine/ats-jobs-scraper").call(run_input={"domains": ["crowdstrike.com"], "remote": True})
jobs = list(client.dataset(run["defaultDatasetId"]).iterate_items())
```

**MCP (Claude, Cursor, any MCP client).** Add the Apify MCP server with this Actor enabled:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com/?actors=impressionable_lupine/ats-jobs-scraper",
      "headers": { "Authorization": "Bearer <APIFY_TOKEN>" }
    }
  }
}
```

Then ask: *"Which of these 20 companies are hiring data engineers in Europe? Use ats-jobs-scraper."* The agent calls the Actor with `domains`, `keyword` and `location`, and gets the structured jobs back.

### Tested on real companies

Local end-to-end run, 20 Oct 2026, 14 domains, no proxies. 12 of the 12 companies on a supported ATS resolved, with 3,614 jobs in 48 seconds: Linear, Ramp and OpenAI (Ashby); Figma, Stripe, Airbnb and Datadog (Greenhouse); Palantir (Lever); Visa and CrowdStrike (Workday); ServiceNow (SmartRecruiters); Hugging Face (Workable). The two negative controls returned a clear free `no_ats_found`: NVIDIA (Eightfold, not supported) and Bosch (careers host disallows crawlers in robots.txt).

### Limitations

- **Supported ATSs only:** Greenhouse, Lever, Ashby, Workday, SmartRecruiters and Workable. Companies on iCIMS, Taleo, SuccessFactors, Eightfold, Phenom, BambooHR, Teamtailor, Personio or a fully in-house system come back as `no_ats_found` and are not charged. If you know a company's board URL on a supported ATS, pass it in `boardUrls`.
- **robots.txt is respected** on company websites. If a careers site disallows crawlers, detection by page is skipped, and the slug probe or a direct `boardUrls` entry is the way in.
- **Workday** returns at most 2,000 jobs per site through its public feed. Dates in listings are "Posted N days ago" (day precision; "30+ days" has no exact date) unless `includeDescription` is on. Department isn't exposed in listings.
- **Slug probing** only accepts boards whose name or postings match the domain. Companies whose ATS slug differs from their domain (e.g. `BoschGroup`) need a careers-page link or a `boardUrls` entry.
- **Salary** appears only when the employer publishes it. Text-derived ranges are conservative and need a currency symbol on both ends.
- Each run reads live data. To track new or closed roles, schedule the Actor and compare `jobId`s between runs.

### FAQ

**Is it legal to scrape ATS job boards?** The Actor reads only public job postings from the official, unauthenticated feeds the ATS vendors provide for careers pages. No login, no personal data, and `robots.txt` is honoured on company sites. Check that your use fits your jurisdiction and the sites' terms.

**How is this different from a LinkedIn or Indeed scraper?** It goes to the source. ATS feeds are where companies publish first, they include roles never syndicated to job boards, and they carry the ATS's job ID, exact posting date and the direct apply link, with no duplicates or reposts.

**Do I need the Greenhouse/Lever slug?** No. Give the domain. If you already have the board URL or slug, put it in `boardUrls` to skip detection and the $0.01 company fee.

**Why was a domain `no_ats_found`?** The company uses an ATS that isn't supported, its careers page loads jobs from an unsupported source, or robots.txt blocked the careers host. The `OUTPUT` record shows which pages were checked and why. You are not charged.

**Can I get only new jobs?** Use `postedWithinDays: 1` on a daily schedule, or diff `jobId` across runs.

**How fast is it?** About 2–10 s per company for Greenhouse, Lever, Ashby and Workable. Large Workday sites take 10–40 s because the feed pages 20 jobs at a time. Companies run in parallel.

**Does it use proxies?** No. Official feeds don't need them, and that's why the price per job can be low.

**What happens if an ATS changes its API?** Each adapter validates the response shape. A change shows up as an explicit `ATS_FORMAT_CHANGED` error for that company, never as silently wrong data.

***

Keywords: ats jobs scraper, greenhouse jobs, lever jobs, workday jobs, careers page scraper, ashby jobs scraper, smartrecruiters jobs, workable jobs, job postings API, company jobs API, hiring signals, job board scraper.

# Actor input Schema

## `domains` (type: `array`):

Company websites or domains, e.g. "stripe.com" or "https://linear.app". The Actor finds the careers page, detects the ATS (Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable) and scrapes all open jobs. You are charged company-resolved only when an ATS is found.

## `boardUrls` (type: `array`):

Skip detection and scrape a known board. Accepts URLs (https://boards.greenhouse.io/figma, https://jobs.lever.co/palantir, https://jobs.ashbyhq.com/ramp, https://visa.wd5.myworkdayjobs.com/Visa, https://jobs.smartrecruiters.com/ServiceNow, https://apply.workable.com/huggingface) or short refs like "greenhouse:figma", "lever:palantir", "lever:eu/slug", "workday:visa/Visa@wd5". No company-resolved charge for these.

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

Only jobs whose title, department or team contain all of these words (case-insensitive). Use quotes for a phrase: "machine learning" engineer.

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

Only jobs whose location contains this text. Separate alternatives with | (e.g. "London|Berlin|Remote").

## `remote` (type: `boolean`):

Keep only jobs the ATS marks as remote (or whose location says Remote / Anywhere).

## `postedWithinDays` (type: `integer`):

Only jobs first published in the last N days. Jobs with no published date are excluded when this is set. Workday dates are day-precision.

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

Upper limit of jobs saved (and charged) per company, newest first. Use it to cap cost on very large employers.

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

Adds the full plain-text description (descriptionText). For Workday and SmartRecruiters this fetches each job's detail record, which is slower but also adds exact Workday posting dates and employment type.

## `detectBySlug` (type: `boolean`):

If the careers page does not link to an ATS, try public ATS APIs with slugs derived from the domain (e.g. stripe.com -> greenhouse:stripe). A match is accepted only when the board's company name or postings match the domain.

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

How many companies are processed at once. Each host is still rate-limited individually.

## Actor input object example

```json
{
  "domains": [
    "stripe.com",
    "linear.app",
    "crowdstrike.com"
  ],
  "boardUrls": [
    "greenhouse:figma",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "keyword": "engineer",
  "remote": false,
  "maxJobsPerCompany": 20,
  "includeDescription": false,
  "detectBySlug": true,
  "maxConcurrency": 4
}
```

# Actor output Schema

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

Flat job records: company, domain, ats, jobId, title, department, location, remote, employmentType, postedAt, applyUrl, salary fields, optional descriptionText.

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

OUTPUT record: per input, detected ATS boards, status (ok, no_ats_found, website_unreachable, ats_error, budget_exhausted, invalid_input), job counts, errors and billing totals.

# 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 = {
    "domains": [
        "linear.app",
        "figma.com"
    ],
    "maxJobsPerCompany": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("impressionable_lupine/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 = {
    "domains": [
        "linear.app",
        "figma.com",
    ],
    "maxJobsPerCompany": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("impressionable_lupine/ats-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 '{
  "domains": [
    "linear.app",
    "figma.com"
  ],
  "maxJobsPerCompany": 20
}' |
apify call impressionable_lupine/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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