# Career Site Jobs Scraper - Greenhouse, Lever, Workday & 6 more (`angaba92/ats-jobs-scraper`) Actor

Scrape jobs from career pages on Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Teamtailor, Recruitee and Personio. One schema: salary, remote, department.

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

## Pricing

$2.00 / 1,000 jobs

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

## Career Site Jobs Scraper — Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Teamtailor, Recruitee & Personio

Extract open job postings from company career pages hosted on **Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Teamtailor, Recruitee and Personio** (9 ATS) in one run, with one normalized schema. Get title, locations, remote/hybrid/onsite, employment type, department, publish date, **salary ranges** and the plain-text description when available. Export CSV, JSON or Excel, or consume the dataset API. Limits, source errors and field availability can reduce coverage.

No login, no proxies, no browser. Sources include documented job-board APIs, public feeds and Workday's undocumented career-site endpoint. Technical accessibility is not permission to republish or resell listings; check source and employer terms for your intended use.

### What you can do with it

- **Job aggregators & job boards** — feed listings from hundreds of companies into your site.
- **Recruiters & sourcers** — watch target companies and catch new roles the day they are posted (`postedWithinDays`).
- **Sales prospecting** — hiring is a buying signal: find companies hiring for "data engineer", "SDR" or "security".
- **Salary & labour-market research** — salary ranges are returned as numbers (min, max, currency, interval) whenever the company publishes them.
- **Competitive intelligence** — track how competitors' teams and locations grow.

### Input

Add companies in any of these formats (mix freely):

| Format | Example |
|---|---|
| Board URL | `https://boards.greenhouse.io/airbnb`, `https://jobs.lever.co/spotify`, `https://jobs.ashbyhq.com/ramp` |
| Workday career site URL | `https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite` |
| `platform:slug` | `greenhouse:airbnb`, `lever:spotify`, `ashby:ramp` |
| SmartRecruiters | `https://jobs.smartrecruiters.com/BoschGroup` or `smartrecruiters:BoschGroup` |
| Recruitee | `https://bunq.recruitee.com` or `recruitee:bunq` |
| Personio | `https://1komma5grad.jobs.personio.de` or `personio:1komma5grad` |
| Workable | `https://apply.workable.com/huggingface` or `workable:huggingface` |
| Teamtailor | `https://career.teamtailor.com` or `teamtailor:career` (custom domains: `teamtailor:jobs.example.com`) |
| Bare slug (auto-detected) | `airbnb` — tried on Greenhouse, Lever, Ashby, Recruitee, Personio and Workable |

Optional filters: `titleKeywords`, `locations` (use `remote` to include remote roles), `departments`, `remoteOnly`, `postedWithinDays`, `maxJobsPerCompany`, `maxResults`, `includeDescription`.

```json
{
  "companies": ["greenhouse:airbnb", "jobs.lever.co/spotify", "ramp",
                "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"],
  "titleKeywords": ["engineer"],
  "postedWithinDays": 30,
  "maxResults": 500
}
```

**Finding the slug:** open the company's "Careers" page and click a job. The URL shows the platform and slug — e.g. `job-boards.greenhouse.io/airbnb/jobs/123` → `greenhouse:airbnb`. For Workday, copy the URL up to the site name (`…myworkdayjobs.com/<SiteName>`).

### Output

One row per job, same fields for every platform:

```json
{
  "platform": "ashby",
  "companySlug": "ramp",
  "boardId": "ashby:ramp",
  "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "title": "Security Engineer, Cloud",
  "url": "https://jobs.ashbyhq.com/ramp/34413f8d-…",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-…/application",
  "locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)", "Miami, FL"],
  "workplaceType": "hybrid",
  "employmentType": "full_time",
  "department": "Engineering",
  "team": "Backend",
  "postedAt": "2026-04-07T17:12:35.753000Z",
  "salary": { "min": 211400, "max": 290600, "currency": "USD", "interval": "year",
              "text": "$211.4K - $290.6K" },
  "description": "Ramp is…",
  "scrapedAt": "2026-10-08T10:58:00Z"
}
```

Normalized values: `workplaceType` ∈ `remote | hybrid | onsite | null`; `employmentType` ∈ `full_time | part_time | contract | temporary | internship | null`; dates are ISO 8601 UTC.

Fields the company does not publish are `null` — they are never guessed. Field coverage depends on the platform and the company:

| Field | Greenhouse | Lever | Ashby | Workday | SmartRecruiters | Workable | Teamtailor | Recruitee | Personio |
|---|---|---|---|---|---|---|---|---|---|
| Title, URL, locations, posted date | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Description | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | when published in feed |
| Department | ✅ | ✅ | ✅ | — | ✅ (function) | ✅ | ✅ | when set | ✅ |
| Workplace type (remote/hybrid) | when set | ✅ | ✅ | when in location | ✅ | remote flag | ✅ | ✅ | when in office list |
| Employment type | — | ✅ | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ |
| Salary range | when published | when published | when published | — | — | — | — | when published | — |

A run summary is saved to the key-value store record `OUTPUT`: per company, the detected platform, jobs found, jobs delivered and any error (e.g. `board not found (404)`). An upstream source error is isolated to its company so other companies can proceed. Invalid input and billing errors may stop the run.

#### Source response integrity

For **Greenhouse and Ashby**, a successful HTTP response must contain an explicit `jobs` array. `jobs: []` is a valid empty board; a missing array, JSON null, wrong container, missing usable ID/title or a field structure that the normalizer cannot parse is a source error, **not proof of zero vacancies**. The fetched board is normalized before its rows are billed: if validation fails, no jobs from that board are delivered. This conservative policy may withhold valid jobs alongside a malformed record. Already delivered jobs from other boards remain available.

Read per-company `OUTPUT.error` and top-level `OUTPUT.status` before interpreting results. Mixed success is `partial`; if every board fails and no jobs are delivered, the run fails. A malformed response during bare-slug autodetection does not silently advance to another platform; HTTP 404 can still advance. Empty boards can legitimately stop autodetection. Use an explicit platform/URL to disambiguate a company slug. Ashby postings with `isListed: false` are excluded, and unknown listing-flag types are rejected. HTTP 429/5xx, invalid JSON and transport errors retain their existing retries; a structurally unexpected JSON body is reported without extra shape retries. These new shape checks do not certify every field or the other seven ATS adapters.

#### Identity, duplicates and limits

Every result includes `boardId`, the resolved board namespace. Use `(boardId, jobId)` as its stable key. Within a run, an auto-detected slug and an explicit URL for the same board do not deliver or charge the same job twice. Different ATS platforms and different Workday sites remain separate namespaces; migration copies across ATS platforms are not merged. Teamtailor custom-domain aliases are not automatically resolved to the vendor domain.

`OUTPUT.duplicatesExcluded` (also per company) counts duplicate candidate rows excluded before billing. Only the successfully charged prefix enters the delivered-ID set; jobs excluded by `maxJobsPerCompany` remain eligible through a later alias. `maxResults` and the charge budget apply to the whole run. Board fetching and delivery are sequential, so there is no concurrent identity reservation within a run. Deduplication is not persistent across runs: a scheduled run may deliver and charge a listing again. This is a current-listings extractor, not a new-jobs-only monitor.

With omitted input `{}`, the Actor uses Airbnb on Greenhouse, Spotify on Lever and Ramp on Ashby. Explicit invalid targets never switch to demo data. An empty `companies` array is rejected; valid boards with no matching jobs may legitimately produce zero rows. If all boards fail, the run fails with per-board errors in `OUTPUT`.

### Pricing

**$2 per 1,000 jobs** (pay per result). You pay only for jobs written to the dataset — filtered-out jobs are free. Set *Maximum cost per run* in the run options to cap spend; the Actor stops cleanly when the cap is reached.

### FAQ

**Which companies are supported?** Any company whose career site runs on Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Teamtailor, Recruitee or Personio — US tech, large enterprises (Workday, SmartRecruiters) and European SMEs and scale-ups (Workable, Teamtailor, Recruitee, Personio).

**What permissions do I need?** Public access does not establish commercial reuse rights. Each ATS and each employer has its own terms. Greenhouse and Ashby document their public job APIs for an organization's own career pages; that documentation is not a third-party resale license. Their customer agreements have customer/service-use restrictions, and we have not established how those agreements apply to this public-API use or obtained a blanket redistribution permission. Workday's site terms restrict automated extraction and commercial reuse, with the scope for employer career sites unresolved in our review. SmartRecruiters' API robots.txt disallows generic bots; the Actor does not claim robots-compatible access for all sources. Check and obtain any necessary permissions for your use case, especially republishing listings. No agreements or permissions are accepted on your behalf.

**Why did a company return 0 jobs?** Either the slug is wrong (see `OUTPUT` → `board not found`), the company has no open roles, or your filters excluded everything.

**Workday and SmartRecruiters are slower** — they need one request per job for the full description. Use `maxJobsPerCompany` or filters to keep runs short.

Found a bug or need another ATS (BambooHR, JazzHR, Breezy…)? Open an issue on the Actor page.

**Will aliases be billed twice?** Not for the same resolved `(boardId, jobId)` within one run. Separate runs are billed separately, and unknown custom-domain aliases or cross-ATS copies may remain distinct.

**How do I export or automate?** Open the run dataset and export CSV, JSON or Excel; use its API URL with n8n, Make, Zapier or your own pipeline. Use `boardId` plus `jobId` for downstream upserts, and read `OUTPUT` before treating a partial run as complete.

# Actor input Schema

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

One entry per company. Accepts board URLs, platform:slug or bare slugs auto-detected on Greenhouse, Lever, Ashby, Recruitee, Personio and Workable. Prefer explicit platforms to disambiguate. Greenhouse/Ashby malformed responses are source errors, not empty boards; inspect OUTPUT for partial coverage.

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

Keep only jobs whose title contains any of these words (case-insensitive). Empty = all jobs.

## `locations` (type: `array`):

Keep jobs whose location contains any of these strings, e.g. 'London', 'United States'. Use 'remote' to include remote jobs.

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

Keep jobs whose department or team contains any of these strings, e.g. 'Engineering'.

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

Keep only jobs the board marks as remote.

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

Keep only jobs posted in the last N days. 0 = no limit. Jobs without a publish date are kept.

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

Include plain-text job description.

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

Limit unique new jobs delivered per input company entry. 0 = no per-entry limit. Aliases exclude jobs already delivered in this run; fetching may itself be capped for Workday and SmartRecruiters.

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

Stop after this many unique jobs across all companies in this run (resolved boardId + jobId). 0 = no item limit. Separate runs may deliver and charge the same jobs again.

## Actor input object example

```json
{
  "companies": [
    "greenhouse:airbnb",
    "jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "titleKeywords": [],
  "locations": [],
  "departments": [],
  "remoteOnly": false,
  "postedWithinDays": 0,
  "includeDescription": true,
  "maxJobsPerCompany": 0,
  "maxResults": 1000
}
```

# Actor output Schema

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

No description

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

No description

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

// Run the Actor and wait for it to finish
const run = await client.actor("angaba92/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": [
        "greenhouse:airbnb",
        "jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/ramp",
    ] }

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

```

## MCP server setup

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