# Career Site Jobs API — Greenhouse, Lever, Ashby & 10+ ATS (`insight.solutions/ats-jobs-api`) Actor

Scrape job postings from any company career page. One Actor covers Greenhouse, Lever, Ashby, Workable, Recruitee, SmartRecruiters, Teamtailor, Personio, Breezy, JazzHR and Workday, returning normalized JSON: title, location, remote flag, salary, apply URL and full description.

- **URL**: https://apify.com/insight.solutions/ats-jobs-api.md
- **Developed by:** [Insight Solutions](https://apify.com/insight.solutions) (community)
- **Categories:** Jobs, Business, 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 posting returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Career Site Jobs API — Greenhouse, Lever, Ashby & 10+ ATS

**Scrape job postings from any company career page.** Give this Actor a list of career boards — or just company domains — and get back every open role as normalized JSON: title, location, remote flag, salary, posted date, apply URL and full description, 27 fields in one shape across 11 applicant tracking systems.

Data comes straight from each ATS's own public job feed, so runs finish in seconds. No browser, no proxy, no API key, no login. **$2 per 1,000 job postings**, failed boards are free, and a run that returns no jobs costs nothing at all.

### Try it in 30 seconds

```json
{
  "boards": ["greenhouse:stripe", "ashby:ramp", "vandebron.nl"],
  "includeDescription": true,
  "maxJobsTotal": 100
}
```

Two of those are explicit boards; `vandebron.nl` is a bare domain the Actor resolves on its own, then remembers for later runs.

### Supported ATS platforms

Every serious ATS publishes its customers' open roles on a public, unauthenticated endpoint — that is how postings reach Google and the aggregators. Each vendor uses a different URL, response shape and name for "location". This Actor reads those endpoints directly and flattens them into one schema.

| ATS | Board identifier | Notes |
|---|---|---|
| Greenhouse | `greenhouse:stripe` | Full descriptions, departments, offices, structured pay ranges |
| Lever | `lever:leverdemo` | Full descriptions, commitment, workplace type, salary range |
| Ashby | `ashby:ramp` | Full descriptions, department/team, compensation tiers, remote flag |
| Workable | `workable:lyst` | Description, requirements, benefits, multi-location, telecommuting flag |
| Recruitee | `recruitee:vandebron` | Description + requirements, remote/hybrid/on-site, salary when published |
| SmartRecruiters | `smartrecruiters:Visa` | Paginated postings plus the full public job ad |
| Teamtailor | `teamtailor:oatly` | JSON Feed + JobPosting data: description, locations, salary, employment type |
| Personio **(beta)** | `personio:personio` | XML feed, titled sections, offices. Personio rate-limits some feeds from cloud IPs (HTTP 429); those return a free diagnostic row |
| Breezy HR | `breezy:new-incentives` | Stable feed, but it carries no posting body — `descriptionText` is `null` |
| JazzHR **(beta)** | `jazzhr:company3` | Public board page only — title, location, department, apply link. No unauthenticated feed exists, so no description |
| Workday | `workday:nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite` | Full descriptions, locations, requisition IDs. Verified live; needs the board host and site name, not a short slug |

**One bound worth knowing.** Workday and SmartRecruiters serve each posting body from a separate document, so both adapters fetch descriptions for the **first 250 postings per board**. Beyond that the posting is still returned with `descriptionText: null`; split a large board across runs by `postedAfter` if you need every body. Every other platform ships the description in the board response.

### Use cases

- **Build a jobs board or aggregator** — pull 500 companies nightly into one consistent schema instead of maintaining eleven parsers.
- **Track who is hiring for a role** — `locationFilter` plus `remoteOnly` returns every open remote role on a watchlist, apply URL included.
- **Enrich a CRM or lead list with hiring data** — pass bare company domains straight from your account list and let auto-detection find the board.
- **Benchmark salaries and compensation** — `salaryMin`/`salaryMax`/`salaryCurrency` come only from structured ATS compensation data, never parsed out of prose.
- **Feed a recruiting or job-matching AI agent** — one JSON object in, one flat array out, over the Apify API or MCP.
- **Seed a database before change detection** — pull full boards here, then hand the watchlist to [New Job Postings Monitor](https://apify.com/insight.solutions/job-postings-monitor).

### How it compares

- **One Actor for 11 ATS platforms**, where most career-board scrapers cover three to six. One input format, one output schema, one thing to maintain.
- **Auto-detection from a bare domain** — pass `stripe.com` instead of hunting for a board slug.
- **Charge-on-success, written into the pricing table.** A board that 404s produces a free diagnostic row. A run that delivers zero postings finishes **FAILED** with the reason in its status message — never a green run containing an apology — and costs nothing, start fee included.
- **Recruiter contact details stripped** from every posting body, and `robots.txt` honoured on every HTML page fetched.

We are not the cheapest per row in this category and will not pretend otherwise. We are the one that covers the whole field in a single schema and does not bill you for failures.

### Input reference

| Field | Type | Default | What it does |
|---|---|---|---|
| `boards` **(required)** | array of strings | `["greenhouse:stripe","ashby:ramp"]` | One entry per company: `ats:slug`, a career-page URL, or a bare domain for auto-detection |
| `atsPlatforms` | multi-select | all | Restricts which platforms auto-detection considers. Ignored for explicit `ats:slug` entries |
| `includeDescription` | boolean | `true` | Turn off for a much smaller, faster result |
| `descriptionFormat` | `text` | `markdown` | `html` | `text` | How the posting body is rendered |
| `locationFilter` | array of strings | none | Case-insensitive substring match against the location |
| `remoteOnly` | boolean | `false` | Keep only postings the ATS flags remote, or whose workplace-type or location text says remote. Job titles are never used for this |
| `postedAfter` | date | none | Keep postings first published on or after this date. Postings with no published date are kept |
| `maxJobsPerBoard` | integer | `1000` | Per-board ceiling |
| `maxJobsTotal` | integer | `5000` | Whole-run ceiling. The run stops cleanly; you are not charged for jobs not returned |
| `maxRunSecs` | integer | `240` | Whole-run wall-clock budget. When reached the Actor stops fetching, keeps what it has, and says so |
| `requestTimeoutSecs` | integer | `20` | Per-request timeout. A URL is abandoned once its attempts have used twice that, or 40 seconds, whichever is longer |
| `maxConcurrency` | integer | `6` | Boards fetched in parallel. Never more than one request at a time to any single host |

**Finding a board slug:** look at where the **Apply** button goes — the slug is the identifier in that URL (`jobs.lever.co/leverdemo/…` → `lever:leverdemo`, `acme.breezy.hr/p/…` → `breezy:acme`, `acme.wd5.myworkdayjobs.com/en-US/SiteName` → `workday:acme.wd5.myworkdayjobs.com/SiteName`). You can also paste the whole URL, or pass the bare domain.

### Output reference

One row per posting, always the same keys in the same order:

```jsonc
{
  "jobId": "8077887",                    // stable ID inside its ATS
  "atsPlatform": "greenhouse", "boardSlug": "stripe", "companyName": "Stripe",
  "companyDomain": "stripe.com",         // the employer's own domain — never the ATS host, never a guess
  "title": "Account Executive, Bridge", "department": "Product Sales", "team": null,
  "employmentType": "FullTime",          // FullTime|PartTime|Contract|Temporary|Internship|Volunteer|Other
  "seniority": "senior",                 // from the title; null when the title says nothing
  "locationRaw": "SF, NYC, SEA, CHI",    // exactly as the ATS published it
  "locations": [{ "city": "SF", "region": null, "country": null, "isRemote": false, "note": null }],
  "isRemote": false,
  "workplaceType": "hybrid",             // onsite|hybrid|remote, when the ATS says so
  "salaryMin": 152000, "salaryMax": 228000, "salaryCurrency": "USD",
  "salaryPeriod": "YEAR",                // YEAR|MONTH|WEEK|DAY|HOUR. Structured ATS data only, never guessed
  "descriptionText": "Who we are\n\nStripe is…",
  "descriptionHtml": null,               // used instead when descriptionFormat is "html"
  "applyUrl": "https://job-boards.greenhouse.io/stripe/jobs/8077887",
  "jobUrl": "https://stripe.com/jobs/search?gh_jid=8077887",
  "postedAt": "2026-07-22T13:15:53-04:00", "updatedAt": "2026-08-06T12:10:17-04:00",
  "scrapedAt": "2026-09-08T07:05:51.244Z",
  "source": "boards-api.greenhouse.io",
  "sourceUrl": "https://boards-api.greenhouse.io/v1/boards/stripe/jobs?content=true"
}
```

An unreadable board produces a diagnostic row instead — `ok: false` plus `input`, `atsPlatform`, `boardSlug`, `error` (reason and the URL we tried), `source`, `sourceUrl`, `checkedAt` — and **no charge**. Filter with `ok !== false` for jobs only.

Roles syndicated to two boards are deduplicated. Two genuinely different requisitions on the *same* board are never collapsed, however alike their titles look.

### Pricing

Pay per event. **$2 per 1,000 job postings** at the Free and Starter tiers, less above that.

| Event | Free | Starter (Bronze) | Scale (Silver) | Business (Gold) |
|---|---|---|---|---|
| `actor-start` — charged once per run, and only after the run has produced at least one job | $0.001 | $0.001 | $0.0005 | $0.0002 |
| `job-posting` — charged once per job row written to your dataset | **$0.002** | **$0.002** | $0.0015 | $0.001 |

**Worked example.** You pull 12 companies and get 1,000 postings back on a Starter plan:

```
actor-start      1 × $0.001  = $0.001
job-posting  1,000 × $0.002  = $2.000
                       total   $2.001
```

Three things that are unusual, and deliberate:

1. **Diagnostic rows are free.** A dead board slug, a timeout, a domain with no detectable ATS — none of it is billed.
2. **`actor-start` is only charged once real results exist.** It becomes payable once your input has been validated and the first board is about to be fetched, but it is only actually billed after at least one job row is safely in your dataset. A run that returns no postings — for any reason — finishes FAILED and costs nothing at all, not even the start fee.
3. **Caps are honoured before billing.** Hit `maxJobsTotal`, `maxRunSecs`, or your own run budget, and the Actor stops cleanly. You are never charged for a job that did not make it into your dataset.

### Integrations and use from code

One JSON object in, one flat array out — the shape agent runtimes want. The Actor runs with **limited permissions**, uses **pay-per-event** pricing and never enters Standby, so it works over the Apify MCP server and with x402 agentic payments. The **Integrations** tab pushes results to Slack, a webhook, Zapier, Make, Google Sheets, Snowflake or BigQuery.

```bash
curl -X POST "https://api.apify.com/v2/acts/insight.solutions~ats-jobs-api/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"boards":["greenhouse:stripe"],"includeDescription":false,"maxJobsTotal":100}'
```

```python
## pip install apify-client
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("insight.solutions/ats-jobs-api").call(run_input={
    "boards": ["greenhouse:stripe", "ashby:ramp"],
    "remoteOnly": True,
    "descriptionFormat": "markdown",
    "maxJobsTotal": 500,
})

for job in client.dataset(run["defaultDatasetId"]).iterate_items():
    if job.get("ok") is not False:
        print(job["companyName"], job["title"], job["locationRaw"], job["applyUrl"], sep=" | ")
```

Set `includeDescription` to `false` when you only need the role list — the payload shrinks by roughly 90% and costs the same.

### FAQ

**How fresh is the data?**
Live. Every run reads the ATS endpoint at that moment; nothing is cached except the domain-to-ATS mapping, which records only *which* board a company uses, never the postings.

**Will I get rate limited?**
These are syndication endpoints — vendors publish them so postings get crawled. The Actor sends at most one request at a time to any single host and backs off exponentially on 429 and 5xx. In normal use you will not see limits.

**What stops a run from hanging?**
`maxRunSecs`, 240 seconds by default, checked before each board and each auto-detection probe. When it runs out the Actor stops fetching, keeps and bills for everything already collected, writes a diagnostic row for each board it never reached, and finishes. Auto-detection has a tighter budget of its own: six candidate pages, one attempt each, five seconds apiece.

**Why is a company I want missing?**
It is on an ATS we do not support yet (iCIMS, Taleo, SuccessFactors and BambooHR are the usual suspects); or it uses a supported ATS under a slug that differs from its brand name — check the apply link; or it hosts its own careers page with no ATS behind it, so there is no public feed to read.

**Why are salaries often null?**
Most postings do not publish one. We fill `salaryMin`/`salaryMax` only from structured compensation data the ATS actually returns — never from "competitive salary" or numbers scraped out of description text.

**How is this different from calling the ATS APIs myself?**
For one company, do that. This exists for fifty companies on nine platforms: eleven response shapes, four location formats, two XML feeds, one POST API, HTML entities encoded twice on one of them, and recruiter emails you should not be storing.

**Does it handle pagination?**
Yes, where the platform has it — SmartRecruiters and Workday page automatically up to `maxJobsPerBoard`. The others return the whole board in one response.

**What happens if one board is down?**
The others still run. The failed one produces a diagnostic row, the run finishes `SUCCEEDED`, and you are not billed for the failure. If *every* board is down the run finishes `FAILED` and you are billed nothing at all.

### Legal and data-protection notes

- **Public endpoints only.** Every source is an unauthenticated, publicly documented job-syndication feed or a public career board page. The Actor never logs in, never accepts cookies or session tokens, and never uses an API key belonging to anyone else.
- **No personal data by design.** Recruiter *contact details* — email addresses, `mailto:` links, personal LinkedIn profile URLs — and recruiter or hiring-manager attribution lines are removed from every description before it is written to your dataset. Recruiter names exposed as structured fields by an ATS are never mapped into the output at all. What this does not do is chase a name written into free prose with no attribution marker; a regex that tried would mangle more descriptions than it cleaned, so we state the limit rather than overstate the guarantee.
- **`robots.txt` is respected on every HTML page we fetch** — the career pages read during domain auto-detection and the JazzHR board page. It is fetched once per domain per run, cached, and evaluated per RFC 9309 against our user-agent token and `*`; a `Disallow` means the page is skipped, and the board says so in its diagnostic row. The remaining sources are the vendors' own JSON and XML syndication feeds.
- **You are the controller of what you do next.** If you export this data, combine it with personal data, or contact people based on it, GDPR/CCPA obligations are yours, not ours.
- **Not affiliated with Greenhouse, Lever, Ashby, Workable, Recruitee, SmartRecruiters, Teamtailor, Personio, Breezy HR, JazzHR, Workday, or with any employer whose postings you retrieve.** All product names and trademarks belong to their respective owners and are used only to describe which public feeds this Actor reads.

### Our other Actors

| Actor | What it does |
|---|---|
| [New Job Postings Monitor](https://apify.com/insight.solutions/job-postings-monitor) | The same 11 ATS connectors with memory: only the roles that opened, closed or changed since your last run |
| [Website Contact Extractor](https://apify.com/insight.solutions/website-contact-extractor) | Business emails, phone numbers and social profiles from a list of website domains |
| [SEC EDGAR API](https://apify.com/insight.solutions/sec-edgar-api) | SEC filings, normalized XBRL financials and full-text search, by ticker or CIK |
| [Wayback Machine Toolkit](https://apify.com/insight.solutions/wayback-toolkit) | Archived URLs, archived page text, and a diff of one page between two dates |

# Actor input Schema

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

One entry per company. Use `ats:slug` (for example `greenhouse:stripe`, `lever:leverdemo`, `ashby:ramp`) when you know the platform, or just a bare company domain (`stripe.com`) and we will detect the ATS for you. Workday needs the board host and site name: `workday:nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite`.

## `atsPlatforms` (type: `array`):

Only relevant for bare-domain inputs. When set, domain auto-detection will only consider these platforms. Leave empty to try all of them.

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

Return the full posting body. Turn this off for a much smaller, faster response when you only need titles, locations and apply links.

## `descriptionFormat` (type: `string`):

How to render the posting body. `text` is plain text with the markup removed, `markdown` keeps headings and lists, `html` is the sanitized source HTML.

## `locationFilter` (type: `array`):

Keep only jobs whose location text contains one of these strings (case-insensitive). For example `Berlin`, `United Kingdom`, `NY`.

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

Keep only postings the ATS flags as remote, whose workplace-type field says remote, or whose location text says remote. Job titles are never used for this, so `Distributed Systems` and `Virtual Reality` roles are not mistaken for remote ones.

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

Keep only jobs first published on or after this date. Jobs whose ATS does not publish a date are kept.

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

Stop after this many jobs from any single board.

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

Hard ceiling for the whole run. The run stops cleanly once it is reached and you are never charged for jobs that were not returned.

## `maxRunSecs` (type: `integer`):

Whole-run wall-clock budget. When it is reached the Actor stops fetching, keeps everything it already collected, explains the stop in a diagnostic row and finishes. Raise it if you pass many boards or very large Workday boards.

## `requestTimeoutSecs` (type: `integer`):

Per-HTTP-request timeout. Raise it only if you are pulling very large Workday boards.

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

How many career boards to fetch at the same time. We never send more than one request at a time to the same host.

## Actor input object example

```json
{
  "boards": [
    "greenhouse:stripe",
    "ashby:ramp"
  ],
  "atsPlatforms": [],
  "includeDescription": true,
  "descriptionFormat": "text",
  "locationFilter": [],
  "remoteOnly": false,
  "postedAfter": "",
  "maxJobsPerBoard": 1000,
  "maxJobsTotal": 5000,
  "maxRunSecs": 240,
  "requestTimeoutSecs": 20,
  "maxConcurrency": 6
}
```

# Actor output Schema

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

Every job posting read from the requested career boards, one row per posting, plus free diagnostic rows for boards that could not be read. Delivered as JSON items in the default dataset.

# 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 = {
    "boards": [
        "greenhouse:stripe",
        "ashby:ramp"
    ]
};

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

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

```

## MCP server setup

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