# Greenhouse, Lever, Ashby, Workable - Career Site Jobs API (`thedeadpoet/greenhouse-lever-ashby-workable-ats-jobs-api`) Actor

Scrape every open job from a company's Greenhouse, Lever, Ashby or Workable careers page into one normalised schema. Give it a mixed list of career sites, get one job-listings dataset back. No API key, no proxy, no browser.

- **URL**: https://apify.com/thedeadpoet/greenhouse-lever-ashby-workable-ats-jobs-api.md
- **Developed by:** [Pablo D](https://apify.com/thedeadpoet) (community)
- **Categories:** Jobs, Lead generation, Open source
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 job listing 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?

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

## Greenhouse, Lever, Ashby & Workable Jobs API — every career site job listing in one schema

**You have a list of companies and you need their open job listings.** Their careers pages run on four different applicant tracking systems, each with a different API and different field names. This Actor takes the mixed list and returns **one normalised job-listings dataset** — same 22 fields for every company, whichever ATS they use.

No API key. No proxy. No browser. No login. Just company career sites in, structured job listings out.

```
greenhouse:stripe  ─┐
lever:palantir     ─┤
ashby:ramp         ─┼──►  one dataset, 22 identical fields per job
workable:blueground┘
```

### What problem this solves

Most job scrapers on the Apify Store cover **one** ATS. Tracking 200 companies therefore means running four different Actors, learning four different output shapes, and writing the reconciliation yourself. Greenhouse calls it `updated_at`, Lever calls it `createdAt`, Ashby calls it `publishedAt`, Workable calls it `published_on`. This Actor does that reconciliation once, correctly, and gives you `postedAt` and `updatedAt`.

It reads each vendor's **own public job-board API** — the endpoint they publish so that aggregators can consume it — so the data is the company's source of truth, not a re-scrape of a re-scrape.

### Use cases

- **Recruiting and talent intelligence** — track hiring across a portfolio of companies and see which teams are growing. Schedule a daily run and diff the dataset for new-role alerts.
- **Build a niche job board or newsletter** — climate tech, Berlin, remote design — from the companies' own boards rather than a second-hand aggregator.
- **Sales and hiring-intent signals** — job postings are a leading indicator. A company hiring five Salesforce admins is buying Salesforce; a company hiring three SREs is scaling infrastructure.
- **Market and labour research** — department mix, seniority mix, remote share, salary bands where the ATS publishes them.
- **Feed an LLM or RAG index** with fresh, structured, clearly-sourced job text.

### Scrape Greenhouse job boards

Greenhouse boards live at `job-boards.greenhouse.io/<token>`. Pass the URL or `greenhouse:<token>`. Greenhouse is the richest source here: it is the only one of the four that publishes `updatedAt`, and it returns `companyName`.

### Scrape Lever job boards

Lever boards live at `jobs.lever.co/<token>`. Pass the URL or `lever:<token>`. Lever's `robots.txt` asks for a one-second crawl delay and this Actor enforces it — `requestDelaySeconds` cannot go below 1 for Lever. Lever does not publish the company's display name, so `companyName` is `null` (`companySlug` and `boardUrl` still identify the company).

### Scrape Ashby job boards

Ashby boards live at `jobs.ashbyhq.com/<token>`. Pass the URL or `ashby:<token>`. Ashby is the best source for **salary**: it is the only one of the four that reliably publishes a compensation range, which arrives in `salaryText` (for example `"$211.4K - $290.6K"`).

### Scrape Workable job boards

Workable boards live at `apply.workable.com/<token>/`. Pass the URL or `workable:<token>`. Workable returns flat `city`/`state`/`country` fields plus a `locations` array; all of it is normalised into `location`, `locations` and `country`.

Per-job Workable short links (`apply.workable.com/j/<code>`) are **not** board tokens and are rejected with a clear error rather than silently scraped as a board called `j`.

### Input

Pass boards in whatever form you already have them — a careers URL, a `provider:token` pair, or a bare token:

```json
{
  "companies": [
    "https://job-boards.greenhouse.io/stripe",
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/ramp",
    "https://apply.workable.com/blueground/",
    "greenhouse:figma",
    "lever:leverdemo",
    "ashby:ramp"
  ],
  "includeDescription": true,
  "remoteOnly": false,
  "titleKeywords": ["engineer", "data"],
  "postedWithinDays": 30,
  "maxJobsTotal": 1000,
  "requestDelaySeconds": 1
}
```

A bare token (`stripe`) uses the **Default ATS provider** setting.

#### How do I find a company's ATS board token?

It is the path segment in the careers URL:

| ATS | Careers URL | Token |
|---|---|---|
| Greenhouse | `https://job-boards.greenhouse.io/`**`stripe`** | `stripe` |
| Lever | `https://jobs.lever.co/`**`palantir`** | `palantir` |
| Ashby | `https://jobs.ashbyhq.com/`**`ramp`** | `ramp` |
| Workable | `https://apply.workable.com/`**`blueground`**`/` | `blueground` |

If the careers page sits on the company's own domain, the ATS is almost always visible in the **Apply** link. If you have domains rather than tokens, run [Website Tech Stack & Email Finder](https://apify.com/thedeadpoet/website-tech-stack-email-finder-wappalyzer-builtwith) first — it returns `atsProvider` and `atsToken` for a domain, which drop straight into this Actor's `companies` input.

Boards that were live when this Actor was last verified, if you want something to try it on:
`greenhouse:monzo`, `lever:leverdemo`, `ashby:ramp`, `workable:blueground`, `workable:skroutz`, `workable:orfium`, `workable:epignosis`.

### Output — one row per open job

Every field is present for every ATS, `null` where that ATS does not publish it:

```json
{
  "source": "ashby",
  "companySlug": "ramp",
  "companyName": null,
  "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "title": "Security Engineer, Cloud",
  "url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-.../application",
  "location": "New York, NY (HQ)",
  "locations": ["New York, NY (HQ)", "Remote (Canada)"],
  "country": "USA",
  "department": "Engineering",
  "team": "Backend",
  "employmentType": "FullTime",
  "isRemote": true,
  "workplaceType": "Hybrid",
  "postedAt": "2026-04-07T17:12:35.753000+00:00",
  "updatedAt": null,
  "salaryText": "$211.4K - $290.6K",
  "descriptionText": "About Ramp ...",
  "descriptionHtml": "<p>About Ramp ...</p>",
  "boardUrl": "https://jobs.ashbyhq.com/ramp",
  "scrapedAt": "2026-09-10T10:22:41.010000+00:00"
}
```

The run also writes a `RUN_SUMMARY` record to the key-value store with boards processed, jobs pushed and any board that failed — the quickest way to validate a long list of tokens.

### Filtering

`titleKeywords`, `excludeTitleKeywords`, `locationKeywords`, `remoteOnly`, `postedWithinDays`, `maxJobsPerCompany`, `maxJobsTotal`.

Filtering happens **after** the fetch, so it does not reduce the number of requests — but `maxJobsTotal` caps your dataset size and therefore your cost, because this Actor charges **per job returned**.

### Honest limitations

- **Four ATS platforms, not all of them.** SmartRecruiters and Recruitee are deliberately **not** supported: their `robots.txt` says `Disallow: /` for generic clients, so this Actor does not touch them. Workday, Taleo, iCIMS and SAP SuccessFactors have no comparable open board API and are out of scope.
- **`companyName`** is published by Greenhouse and Workable only; it is `null` for Lever and Ashby.
- **`updatedAt`** is published by Greenhouse only.
- **Salary** is published by Ashby, and occasionally Lever. Most boards do not expose it at all.
- **Only public, listed jobs.** Confidential and unlisted postings never appear in these APIs, and this Actor does not try to find them.
- **Board tokens must be right.** A wrong token returns HTTP 404; turn on *Emit an item for failed boards* to get those back as dataset rows instead of log lines.
- **`includeDescription` typically multiplies dataset size by 10–30×.** Turn it off if you only need titles, locations and links.

### How the data is obtained

Each source is the ATS vendor's own **public, unauthenticated job-board API**:

- `boards-api.greenhouse.io/v1/boards/{token}/jobs`
- `api.lever.co/v0/postings/{token}?mode=json`
- `api.ashbyhq.com/posting-api/job-board/{token}`
- `apply.workable.com/api/v1/widget/accounts/{token}`

No login, no captcha solving, no proxy rotation, no browser automation. `robots.txt` was checked for every host.

### FAQ

**Is there a Greenhouse jobs API I can just call?**
Yes — `boards-api.greenhouse.io` is public. This Actor calls it for you, plus the Lever, Ashby and Workable equivalents, and normalises all four into one schema so you do not write four parsers.

**How do I scrape a company's careers page?**
If the careers page is hosted by one of these four ATS platforms, use this Actor with the board token — that is the reliable route. If the company hosts its own HTML careers page, this Actor is not the right tool; nothing here renders JavaScript or parses arbitrary markup.

**Can I get job listings from many companies at once?**
Yes, that is the point. Pass hundreds of boards in `companies`. Roughly one request per company; a 50-company list finishes in about a minute, and a measured 25-board run returned 4,287 jobs in 65 seconds.

**Does this scrape LinkedIn, Indeed or Glassdoor?**
No. It reads company-owned ATS boards only. That is a deliberate trade: fewer sources, but first-party data, no anti-bot arms race and no proxy bill.

**How do I find which ATS a company uses?**
Run [Website Tech Stack & Email Finder](https://apify.com/thedeadpoet/website-tech-stack-email-finder-wappalyzer-builtwith) over the company's domain. It returns `atsProvider`, `atsToken` and `atsBoardUrl`, which feed directly into this Actor.

**Do I need an Apify Proxy or residential IPs?**
No. These are public JSON APIs and this Actor exposes no proxy input at all.

**What does it cost?**
It charges per job listing returned. `maxJobsTotal` is the cap that controls your bill; a run that returns nothing costs nothing.

### Changelog

See `CHANGELOG.md`.

### Support

Found a bug, or need a field this Actor does not return yet? Open an issue on the Actor's **Issues** tab, or email **pablodevigoalertas@gmail.com**. Issues are read first.

# Actor input Schema

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

One entry per company. Accepted formats:
• A board URL - https://boards.greenhouse.io/stripe, https://job-boards.greenhouse.io/stripe, https://jobs.lever.co/palantir, https://jobs.ashbyhq.com/ramp, https://apply.workable.com/acme/
• An explicit pair - greenhouse:stripe, lever:palantir, ashby:ramp, workable:acme
• A bare token - stripe (then set "Default ATS provider" below)

## `defaultProvider` (type: `string`):

Used only for entries that are a bare token with no URL and no provider prefix.

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

Adds descriptionText and descriptionHtml to every job. Makes the dataset much larger; turn it off if you only need titles, locations and links.

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

Case-insensitive. A job is kept if its title contains at least one of these strings. Leave empty to keep every title.

## `excludeTitleKeywords` (type: `array`):

Case-insensitive. Jobs whose title contains any of these are dropped.

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

Case-insensitive match against the job location string, e.g. "Berlin", "United Kingdom", "Remote".

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

Keep only jobs the ATS flags as remote, or whose location text contains "remote".

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

0 = no date filter. Uses the posting date the ATS reports; jobs with no date are always kept.

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

0 = no limit.

## `maxJobsTotal` (type: `integer`):

0 = no limit. A hard stop so a large company list cannot run away with your budget.

## `requestDelaySeconds` (type: `number`):

Politeness delay between board requests. Lever's robots.txt asks for a 1 second crawl delay, so values below 1 are raised to 1 for Lever.

## `emitErrorItems` (type: `boolean`):

If a board returns 404 or an error, push a row with the error instead of silently skipping it. Useful when you are validating a large list of board tokens.

## Actor input object example

```json
{
  "companies": [
    "https://job-boards.greenhouse.io/stripe",
    "lever:palantir",
    "ashby:ramp",
    "workable:blueground"
  ],
  "defaultProvider": "greenhouse",
  "includeDescription": true,
  "titleKeywords": [],
  "excludeTitleKeywords": [],
  "locationKeywords": [],
  "remoteOnly": false,
  "postedWithinDays": 0,
  "maxJobsPerCompany": 0,
  "maxJobsTotal": 0,
  "requestDelaySeconds": 1,
  "emitErrorItems": false
}
```

# Actor output Schema

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

One row per open job, normalised across Greenhouse, Lever, Ashby and Workable.

# 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://job-boards.greenhouse.io/stripe",
        "lever:palantir",
        "ashby:ramp",
        "workable:blueground"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("thedeadpoet/greenhouse-lever-ashby-workable-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://job-boards.greenhouse.io/stripe",
        "lever:palantir",
        "ashby:ramp",
        "workable:blueground",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("thedeadpoet/greenhouse-lever-ashby-workable-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://job-boards.greenhouse.io/stripe",
    "lever:palantir",
    "ashby:ramp",
    "workable:blueground"
  ]
}' |
apify call thedeadpoet/greenhouse-lever-ashby-workable-ats-jobs-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,thedeadpoet/greenhouse-lever-ashby-workable-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/JTvoEtYu05eJIBZEa/builds/20rH6NpUaGAg5H5bk/openapi.json
