# ATS Jobs API: Greenhouse, Lever, Ashby & Workable (`tidewell/ats-jobs-api`) Actor

Get every open job from company career sites on Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee, Personio and BambooHR. Paste a careers URL, board slug, domain or company name; get clean, normalised job rows with salary, remote flag and apply URL.

- **URL**: https://apify.com/tidewell/ats-jobs-api.md
- **Developed by:** [Tidewell Software](https://apify.com/tidewell) (community)
- **Categories:** Jobs, Lead generation, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 job returneds

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 API: Greenhouse, Lever, Ashby & Workable career-site jobs

Get every open job from company career sites, straight from the applicant tracking system (ATS) behind them. Paste a careers-page URL, an ATS board slug, a company website or just a company name. The actor detects the ATS, reads its public job-board API and returns one clean, normalised row per job: title, department, location, remote flag, employment type, posted date, salary when published, and the apply link.

**Supported ATS:** Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee, Personio and BambooHR.

**$3 per 1,000 jobs.** No start fee and no monthly rental. Boards that are not found or fail cost nothing.

Docs, examples, MCP setup and an honest comparison with other actors: [plugin.benjaminbritton.co.uk/ats-jobs-api/](https://plugin.benjaminbritton.co.uk/ats-jobs-api/)

### Why use this ATS jobs API

- **One schema for eight ATSes.** Greenhouse jobs, Lever jobs, Ashby jobs and Workable jobs all come out in the same fields, so you can merge them without per-source code.
- **Data from the source.** Rows come from each company's own job board, not an aggregator, so postings are current and links go to the real application form.
- **Give it a URL, a domain or a name.** `notion.so`, `Allica Bank` or `https://jobs.lever.co/palantir` all work. The actor finds the board for you.
- **Salary included.** Structured pay ranges from Greenhouse pay transparency, Ashby compensation, Lever salary range, Recruitee and BambooHR, plus ranges parsed from the job ad text (flagged `salary_source: "description"`).
- **Built for feeds and alerts.** Filter by keyword, location, remote, department and posting age. Turn on *Only jobs not seen in earlier runs* so each scheduled run returns, and charges for, only new postings.
- **Fast and light.** Most boards take one request. 2,400 jobs from 20 companies took about 6 seconds in testing.
- **No personal data.** Recruiter names, emails, phone numbers and application questions are never copied into the output.

### Use cases

- **Niche job boards:** fill a climate, crypto, AI or remote-only job board from a list of target companies, refreshed daily.
- **Job alerts and AI job agents:** watch a set of companies and get only new roles that match a candidate's keywords and location.
- **Sales and recruiting signals:** see which companies are hiring for a role right now (for example, the first sales hire or a RevOps lead) and reach them while the need is fresh.
- **Labour-market and competitor research:** track hiring volume by department, location and seniority, and how published salary ranges change over time.
- **Enrichment:** answer "which ATS does this company use?" and "how many open roles do they have?" for a list of domains.

### How to use it

1. Add companies to **Companies**, one per line. Mix formats freely:
   - Careers or job-board URLs: `https://boards.greenhouse.io/airtable`, `https://jobs.ashbyhq.com/1password`, `https://apply.workable.com/fe-fundinfo`, `https://careers.smartrecruiters.com/ServiceNow`, `https://bunq.recruitee.com`, `https://acme.jobs.personio.de`, `https://acme.bamboohr.com/careers`
   - Explicit `ats:slug`: `greenhouse:stripe`, `lever:spotify`, `smartrecruiters:BoschGroup`
   - Company websites: `1password.com`, `bunq.com`. The actor looks for the job board linked from the site, then tries likely slugs.
   - Company names: `Matillion`, `Allica Bank`. The actor tries the likely slug on every supported ATS, keeps the board with the most open jobs and checks that the board's company name matches yours.
2. Optionally set filters and turn on **Include job description**.
3. Run the actor and download the dataset as JSON, CSV, Excel or XML, or read it through the Apify API.

#### Input example: all open engineering jobs in London or remote at three companies

```json
{
  "companies": [
    "https://boards.greenhouse.io/airtable",
    "https://jobs.lever.co/palantir",
    "ashby:1password"
  ],
  "keywords": ["engineer*", "developer"],
  "excludeKeywords": ["intern*"],
  "locations": ["London", "United Kingdom", "Remote"],
  "postedWithinDays": 30,
  "includeDescription": true
}
```

#### Input example: a daily feed of new jobs only

```json
{
  "companies": ["notion.so", "Monzo", "greenhouse:stripe", "workable:legatics"],
  "onlyNewJobs": true,
  "stateKey": "my-daily-feed"
}
```

Schedule it in Apify Console. Each run returns only postings not returned before for the same `stateKey`.

#### Input fields

| Field | What it does |
|---|---|
| `companies` (required) | URLs, `ats:slug`, domains or company names, one per line |
| `defaultAts` | ATS to use for bare slugs and names. `auto` tries every ATS |
| `keywords` | Keep jobs whose title, department or team contains any keyword (whole-word, `*` wildcard) |
| `excludeKeywords` | Drop jobs whose title contains any of these |
| `searchInDescription` | Match keywords against the full description too |
| `locations` | Keep jobs whose location contains any of these, e.g. `London`, `Germany`, `GB`, `Remote` |
| `remoteOnly` | Keep only remote jobs |
| `departments` | Keep jobs whose department or team contains any of these |
| `postedWithinDays` | Keep jobs first published in the last N days |
| `includeDescription` | Add `description_html` and `description_text` |
| `maxJobsPerCompany`, `maxJobs` | Caps (newest jobs first) |
| `onlyNewJobs`, `stateKey` | Incremental mode for feeds and alerts |
| `maxConcurrency` | Parallel requests (a polite limit of 4 per ATS host always applies) |

### Output

One dataset row per job. The dataset has three views: **Jobs**, **Salaries** and **All fields**.

```json
{
  "company": "AlphaSense",
  "ats": "greenhouse",
  "board_slug": "alphasense",
  "job_id": "8929518002",
  "title": "Enterprise Account Executive, Corporate",
  "department": "Corporate Sales",
  "team": null,
  "location": "New York, New York, United States; Remote - United States; United States",
  "locations": ["New York, New York, United States", "Remote - United States", "United States"],
  "country": null,
  "remote": true,
  "workplace_type": "remote",
  "employment_type": null,
  "posted_at": "2026-11-04T00:31:43Z",
  "updated_at": "2026-11-04T00:31:44Z",
  "salary_min": 105000,
  "salary_max": 140000,
  "salary_currency": "USD",
  "salary_period": "year",
  "salary_text": "USD 105,000-140,000 (Base Compensation Range)",
  "salary_source": "structured",
  "apply_url": "https://job-boards.greenhouse.io/alphasense/jobs/8929518002",
  "job_url": "https://job-boards.greenhouse.io/alphasense/jobs/8929518002",
  "description_html": "<div class=\"content-intro\"><h2><strong>About AlphaSense ...",
  "description_text": "About AlphaSense:\n\nAlphaSense is the AI platform redefining market intelligence ...",
  "source_url": "https://boards-api.greenhouse.io/v1/boards/alphasense/jobs/8929518002",
  "scraped_at": "2026-11-08T00:49:02Z"
}
```

| Field | Notes |
|---|---|
| `company`, `ats`, `board_slug`, `job_id` | `job_id` is the ATS's own ID and stays the same across runs, so use `ats + board_slug + job_id` as a unique key |
| `title`, `department`, `team` | As published. Not every ATS has a team field |
| `location`, `locations`, `country` | Primary location, all listed locations, and a country code or name when the ATS provides one |
| `remote`, `workplace_type` | `remote` is true when the ATS flags the job as remote or the location says Remote / Anywhere. `workplace_type` is `remote`, `hybrid` or `onsite` when published |
| `employment_type` | e.g. Full-time, Contract, Internship |
| `posted_at`, `updated_at` | ISO 8601 UTC. Workable and BambooHR give a date only (`YYYY-MM-DD`) |
| `salary_*` | Min, max, currency, period (`year`, `month`, `hour` and so on), the original text, and whether it came from a structured pay field or the ad text |
| `apply_url`, `job_url` | Application form and public job page |
| `description_html`, `description_text` | Only when `includeDescription` is on |
| `source_url`, `scraped_at` | The public endpoint the row came from, and when |

A `RUN_SUMMARY` record in the run's key-value store lists every input, the ATS and board it resolved to, how it was resolved, the jobs found and returned, and any board that was not found.

### Use from Claude / ChatGPT (MCP)

Run this actor from an AI assistant through Apify's hosted MCP server. This server URL loads only this actor:

```
https://mcp.apify.com/?tools=tidewell/ats-jobs-api
```

- **Claude (claude.ai and Claude Desktop):** Settings → Connectors → Add custom connector. Name it `Apify`, paste the URL, then sign in to Apify.
- **Claude Code:** `claude mcp add apify "https://mcp.apify.com/?tools=tidewell/ats-jobs-api" -t http`
- **ChatGPT:** add a custom MCP server named `Apify` with the URL above and OAuth. In the desktop app: Settings → MCP servers → Add server → Streamable HTTP.
- **Cursor, VS Code and other MCP clients:** `{"mcpServers": {"apify": {"url": "https://mcp.apify.com/?tools=tidewell/ats-jobs-api"}}}`. Clients without OAuth can add the header `Authorization: Bearer <APIFY_TOKEN>`.

Then ask in plain language, for example: *"Use tidewell/ats-jobs-api to list every open engineering job at Stripe, Notion and Monzo posted in the last 14 days, with salary and apply link."* The actor appears as the tool `tidewell--ats-jobs-api`. Runs are billed to your Apify account at the prices below.

Full setup guide, examples and comparisons: [plugin.benjaminbritton.co.uk/ats-jobs-api/](https://plugin.benjaminbritton.co.uk/ats-jobs-api/)

### Pricing

Pay per event: **$0.003 per job returned** ($3 per 1,000 jobs).

- No actor-start fee and no monthly rental.
- Filters run before charging, so you pay only for jobs that pass your filters.
- Boards that are not found, have no matching jobs, or fail cost nothing.
- Set a **maximum cost per run** in the run options. The actor stops cleanly at your limit and never returns rows you are not charged for.

Example: one daily run over 50 companies that returns about 40 new jobs a day with `onlyNewJobs` costs about $0.12 a day.

### FAQ

**Which ATS does a company use?**
Put its website or name in `companies`. The `ats` column and `RUN_SUMMARY` tell you which ATS and board it resolved to. If the company is not on a supported ATS, `RUN_SUMMARY` says so and you are not charged.

**Is this scraping? Is it allowed?**
The actor reads the public job-board endpoints that Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee, Personio and BambooHR provide so companies can show their jobs on their own sites and job boards can list them. It does not log in, bypass protections or collect personal data. It sends polite, rate-limited requests with an identifying user agent. You remain responsible for how you use the data, for example linking back to the original posting.

**Do you collect recruiter or candidate data?**
No. Hiring-manager names, recruiter emails, mailbox addresses, phone numbers and application questions are deliberately left out. The output contains job postings only.

**Why is a field empty for some jobs?**
Each ATS publishes different fields. Greenhouse rarely publishes employment type, Workable's public widget has no salary, and Personio and BambooHR descriptions are whatever the company entered. A field is `null` when the source does not provide it. We never invent values.

**How accurate is salary?**
`salary_source: "structured"` values come from the ATS's own pay fields. `"description"` values are parsed from ranges written in the ad, such as "$120,000 - $150,000" or "£45k-£55k". Check `salary_text` for the original wording.

**Some company names did not resolve. What can I do?**
Paste the careers-page URL or an `ats:slug` instead. Those are exact. Name guessing only accepts a board whose company name matches your input and that has open jobs, so it errs towards "not found" over a wrong match.

**Is Workday supported?**
Not yet. Workday career sites do not have a simple public board API. Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee, Personio and BambooHR are supported now.

**Can I run it on a schedule or via API?**
Yes. Use Apify Schedules, the Apify API, or the Make, Zapier and n8n integrations. Combine `onlyNewJobs` with a schedule for a job-alert feed.

**How fast is it?**
In local tests, 20 boards across all eight ATSes (about 2,400 jobs) took about 6 seconds, and 150 boards took about 10 seconds. SmartRecruiters needs one extra request per job for descriptions, so very large SmartRecruiters boards with descriptions on take longer.

### Keywords

greenhouse jobs, lever jobs, ashby jobs, workable jobs, smartrecruiters jobs, career site jobs, ats jobs api, job postings api, careers page scraper, job board api, company jobs api, recruitee jobs, personio jobs, bamboohr jobs, remote jobs feed, salary data

# Actor input Schema

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

One per line. Any of: a careers or job-board URL (https://boards.greenhouse.io/airtable, https://jobs.lever.co/palantir, https://jobs.ashbyhq.com/1password, https://apply.workable.com/fe-fundinfo, https://careers.smartrecruiters.com/ServiceNow, https://bunq.recruitee.com, https://acme.jobs.personio.de, https://acme.bamboohr.com/careers); an explicit 'ats:slug' such as greenhouse:stripe; a company website such as notion.so (the actor finds the job board linked from the site); or a company name such as Monzo (the actor tries the likely board slug on every supported ATS).

## `defaultAts` (type: `string`):

When an input line is a bare slug or company name, 'Auto' tries every supported ATS and keeps the board with the most open jobs. Pick one ATS to skip the guessing.

## `keywords` (type: `array`):

Keep jobs whose title, department or team contains any of these words (whole-word, case-insensitive; use \* as a wildcard), e.g. 'engineer', 'data', 'account executive', 'develop\*'.

## `excludeKeywords` (type: `array`):

Drop jobs whose title contains any of these words (whole-word; \* is a wildcard), e.g. 'intern\*' (intern, internship), 'senior', 'manager'.

## `searchInDescription` (type: `boolean`):

Also match keywords against the full job description (for skills like 'Python' or 'Kubernetes'). For SmartRecruiters this needs one extra request per job, so it is slower.

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

Keep jobs whose location text contains any of these (case-insensitive), e.g. 'London', 'United Kingdom', 'GB', 'Berlin', 'Remote'.

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

Keep only jobs the ATS marks as remote, or whose location says Remote / Anywhere / Work from home.

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

Keep jobs whose department or team contains any of these (case-insensitive), e.g. 'Engineering', 'Sales', 'Marketing'.

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

Keep jobs first published in the last N days. Jobs with no published date are dropped when this is set.

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

Add description_html and description_text to each row. Off by default to keep rows small. Salary is still extracted from descriptions when it is not published as a structured field.

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

Stop after this many matching jobs from one board. Leave empty for no limit.

## `maxJobs` (type: `integer`):

Stop the whole run after this many jobs. Leave empty for no limit. You can also cap spend with the run's maximum cost setting.

## `onlyNewJobs` (type: `boolean`):

Skip jobs this actor already returned in a previous run with the same state key. Job IDs are kept for 365 days in your named key-value store 'ats-jobs-state'.

## `stateKey` (type: `string`):

Name for the seen-jobs memory, so separate feeds don't share it (e.g. 'uk-data-jobs'). Letters, digits and hyphens.

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

Total parallel HTTP requests. A per-host limit of 4 also applies, so ATS servers are never hammered.

## Actor input object example

```json
{
  "companies": [
    "https://boards.greenhouse.io/airtable",
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/1password",
    "https://apply.workable.com/fe-fundinfo"
  ],
  "defaultAts": "auto",
  "keywords": [],
  "searchInDescription": false,
  "remoteOnly": false,
  "includeDescription": false,
  "onlyNewJobs": false,
  "stateKey": "default",
  "maxConcurrency": 8
}
```

# Actor output Schema

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

One row per job posting.

## `runSummary` (type: `string`):

Per-input summary including boards that were not found.

# 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": [
        "https://boards.greenhouse.io/airtable",
        "https://jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/1password",
        "https://apply.workable.com/fe-fundinfo"
    ],
    "keywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidewell/ats-jobs-api").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": [
        "https://boards.greenhouse.io/airtable",
        "https://jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/1password",
        "https://apply.workable.com/fe-fundinfo",
    ],
    "keywords": [],
}

# Run the Actor and wait for it to finish
run = client.actor("tidewell/ats-jobs-api").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": [
    "https://boards.greenhouse.io/airtable",
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/1password",
    "https://apply.workable.com/fe-fundinfo"
  ],
  "keywords": []
}' |
apify call tidewell/ats-jobs-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,tidewell/ats-jobs-api"
        }
    }
}
```

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/QcRQWefj1IuvOleXP/builds/Qy8MynJLcZilvIZt8/openapi.json
