# ATS Job Scraper - Greenhouse, Lever, Ashby, Workday (`porchrun/ats-jobs-scraper`) Actor

Job postings from Greenhouse, Lever, Ashby, Workday and SmartRecruiters in one normalized schema, via official public APIs. Give it a company domain and it finds the ATS for you.

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

## Pricing

from $1.00 / 1,000 job postings

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, SmartRecruiters & Workday in one schema

Pull job postings from company career boards across five applicant tracking systems and get back **one normalized schema**. No API keys, no login, no proxies.

Give it a company domain and it works out which ATS they use on its own.

### Why this one

Every other job-board Actor covers a single platform. If you track hiring across 200 companies, they're spread over Greenhouse, Lever, Ashby and SmartRecruiters — so you end up running three or four Actors and reconciling three or four different JSON shapes by hand.

This Actor does that for you:

- **One schema.** A Greenhouse job and a SmartRecruiters job come out with identical fields.
- **Automatic ATS detection.** You supply `ramp.com`; it finds `ashby:ramp`. Nobody memorises board slugs for 200 companies, and every other Actor makes you.
- **Official public APIs.** These are the endpoints the boards themselves are built on — not HTML scraping. Nothing breaks when a careers page gets redesigned, and there are no proxy costs or blocks.
- **Business data only.** Job postings, not personal profiles.

### Input

Any combination of three ways to name a board:

```jsonc
{
  "companyDomains": ["ramp.com", "stripe.com", "linear.app"],
  "boards": ["greenhouse:stripe", "ashby:ramp", "smartrecruiters:BoschGroup"],
  "boardUrls": ["https://jobs.lever.co/spotify"],

  "includeDescription": true,
  "titleKeywords": ["engineer"],
  "locationKeywords": ["London", "Remote"],
  "departments": ["Engineering"],
  "remoteOnly": false,
  "postedAfter": "2026-07-01",
  "maxJobsPerBoard": 500
}
```

`remoteOnly` keeps postings whose remote status is unstated, because most boards omit the flag — filtering them out would silently drop the majority of Greenhouse results.

### Output

```jsonc
{
  "id": "ashby:34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "platform": "ashby",
  "boardSlug": "ramp",
  "company": null,
  "title": "Security Engineer, Cloud",
  "url": "https://jobs.ashbyhq.com/ramp/34413f8d-...",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-.../application",
  "location": "New York, NY (HQ)",
  "locations": ["New York, NY (HQ)", "Remote (US)", "Remote (Canada)", "Miami, FL"],
  "remote": true,
  "department": "Engineering",
  "team": "Backend",
  "employmentType": "Full Time",
  "descriptionHtml": "<h1><strong>About Ramp</strong></h1>...",
  "descriptionText": "About Ramp\nRamp is building...",
  "compensation": { "min": 211400, "max": 290600, "currency": "USD", "interval": "year" },
  "postedAt": "2026-04-07T17:12:35.753+00:00",
  "updatedAt": null
}
```

Fields a platform doesn't publish come back `null` rather than guessed. `remote` is only `true`/`false` when the posting says so — it is never inferred from a city name.

### Coverage

| Platform | Descriptions | Pay bands | Notes |
|---|---|---|---|
| Greenhouse | in list response | — | Whole board in one request |
| Lever | in list response | yes | Requirements and benefits sections included |
| Ashby | in list response | yes | Structured pay bands, multiple locations |
| SmartRecruiters | +1 request per job | — | Paginated; handles boards of 5,000+ postings |
| Workday | +1 request per job | — | Pages at 20; see the note below |

Verified against live boards: Stripe (548 jobs), Ramp (125), Spotify (105), Ubisoft (279), Bosch (4,712), Workday (337), NVIDIA (2,000).

**Workday boards** are addressed as `workday:tenant.wdN/SiteName`, for example
`workday:nvidia.wd5/NVIDIAExternalCareerSite`. Pasting the board URL into `boardUrls`
works too and is easier. Workday's list endpoint returns placeholders rather than data —
locations come back as `"6 Locations"` and dates as `"Posted Today"` — so real locations
and publish dates require `includeDescription: true`, which fetches each posting. With it
off, this Actor returns `null` rather than passing Workday's placeholder text off as a
location.

### Detection

`companyDomains` resolves in two passes: read the company's careers pages for a board link, then fall back to deriving a slug from the domain and verifying it against each ATS. Detection succeeds on roughly three quarters of typical startup and mid-market domains. Large enterprises on Workday or SuccessFactors are not covered yet — pass those boards explicitly if they use one of the four supported systems.

### Pricing

Pay per event: a small charge to start a run, per job returned, and per company domain successfully resolved to a board. You are not charged for domains that resolve to nothing.

# Actor input Schema

## `companyDomains` (type: `array`):

Company websites — we find which ATS they use and the board slug automatically. Example: ramp.com, stripe.com. This is the easy option if you don't know their board IDs.

## `boards` (type: `array`):

Boards you already know, as platform:slug. Example: greenhouse:stripe, ashby:ramp, lever:spotify, smartrecruiters:BoschGroup, workday:nvidia.wd5/NVIDIAExternalCareerSite. Faster and cheaper than domain detection.

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

Paste any ATS board URL directly, e.g. https://jobs.lever.co/spotify or https://job-boards.greenhouse.io/stripe.

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

Fetch the full posting text as HTML and plain text. On SmartRecruiters this needs one extra request per job and will be slower.

## `titleKeywords` (type: `array`):

Case-insensitive. Example: engineer, designer.

## `locationKeywords` (type: `array`):

Case-insensitive, matched against every location on the posting. Example: London, Berlin.

## `departments` (type: `array`):

Case-insensitive. Example: Engineering, Sales.

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

Keeps postings the platform marks as remote. Postings that don't state either way are kept, since most boards omit the flag entirely.

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

ISO date, e.g. 2026-07-01. Drops anything published earlier or with no publish date.

## `maxJobsPerBoard` (type: `integer`):

Cap results per board. Leave empty for everything.

## `concurrency` (type: `integer`):

Each board is a different host, so this is safe to raise.

## Actor input object example

```json
{
  "companyDomains": [
    "ramp.com",
    "stripe.com"
  ],
  "includeDescription": true,
  "remoteOnly": false,
  "concurrency": 5
}
```

# 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 = {
    "companyDomains": [
        "ramp.com",
        "stripe.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("porchrun/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 = { "companyDomains": [
        "ramp.com",
        "stripe.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("porchrun/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 '{
  "companyDomains": [
    "ramp.com",
    "stripe.com"
  ]
}' |
apify call porchrun/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/pvHsQa0izHdfyWZD6/builds/GAQovHYjtvWCJQHxw/openapi.json
