# Greenhouse, Lever & Ashby Jobs API — Verified Fresh Directory (`zon4a/ats-jobs-api`) Actor

Normalized job postings from Greenhouse, Lever and Ashby public boards. Directory-backed: no slug hunting. $1/1K jobs, no start fee, empty runs free. Public coverage & freshness stats.

- **URL**: https://apify.com/zon4a/ats-jobs-api.md
- **Developed by:** [Heorhii Kozlitin](https://apify.com/zon4a) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Features:** Limited permissions, Single-tenant, Server interface
- **Stats:** 10 total users, 6 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 job-results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## Greenhouse, Lever, Ashby & Workday Jobs API — Verified Fresh Directory | $1/1K

**Every coverage and freshness claim on this page is verifiable:** our full company
directory with per-company *last-validated* timestamps is public —
**[live coverage & freshness stats](https://george-kozlitin.github.io/ats-jobs-directory/)**.
No other jobs actor lets you check whether a company is covered *before* you pay.

Normalized job postings straight from the **public JSON endpoints** of Greenhouse,
Lever, Ashby, Workday, SmartRecruiters, Personio and Recruitee — the same data
employers publish for distribution. No browser, no proxies, no login, no LLM guesses.
One clean schema across all seven ATS.

### Why this actor

| | **This actor** | bovi | memo23 (multi-ATS) | fantastic-jobs (flagship) |
|---|---|---|---|---|
| Price per 1,000 jobs | **$1.00** | $1.50 | $2.00 | $4.00 |
| Actor-start fee | **none** | none | **$0.03/GB per run** | $0.01+ per start |
| Empty run cost | **$0 in events** (seconds of platform usage only) | $0 | start fee still charged | start fee still charged |
| Company directory | **25,000+ verified-live boards / 1.4M open jobs — public, timestamped** | 4 static preset lists | none (bring your own slugs) | 200k claimed — not verifiable, not public |
| Freshness proof | **public per-company timestamps** | none | none | marketing claims only |
| Delta mode (new/closed jobs) | **yes** | new only | no | time-window filters |
| Workday coverage | **yes** | no | yes | yes ($4/1k) |
| Salary fields | structured ATS data only (no AI guesses) | passthrough | regex | AI-extracted ("might have some errors" — their words) |
| Recruiter emails / person data | **never** (GDPR-clean by design) | no | "emails" = info@ addresses | hiring-manager names+emails |

Scheduled monitoring runs are where start fees hurt: 30 daily runs/month of a
50-company watchlist cost $0 extra here.

### Quick start (zero config)

Run with defaults: the `top-tech` preset fetches verified-live boards, capped at 50
jobs per company — a first run costs **under $0.10**. Then bring your own companies:

```json
{
  "companies": ["stripe", "lever:netflix", "https://jobs.ashbyhq.com/linear"],
  "titleKeyword": "devops",
  "remoteOnly": true
}
```

Bare slugs are looked up across Greenhouse/Lever/Ashby automatically. Board URLs work
as-is — including Workday career sites (`https://acme.wd5.myworkdayjobs.com/careers`
or explicit `workday:acme|wd5|careers`).

**Workday notes (v0.2):** postings come from the same JSON endpoint the career site
itself uses. `posted_at` is approximated from Workday's relative dates (flagged in
`parse_warnings`); descriptions are not available at list level; per-tenant scans are
capped (max 300 jobs) to keep runs fast — see `parse_warnings` for details.

### Output (one schema for all three ATS)

```json
{
  "global_id": "greenhouse:anthropic:5222180008",
  "ats": "greenhouse",
  "company_slug": "anthropic",
  "company_name": "Anthropic",
  "canonical_company_id": "anthropic",
  "title": "DevOps Engineer",
  "department": "Platform",
  "team": null,
  "location_raw": "Madrid, Spain",
  "remote_type": "remote",
  "employment_type": "full_time",
  "salary": {"min": 60000, "max": 80000, "currency": "EUR", "period": "per-year-salary"},
  "posted_at": "2026-06-20T12:00:00+00:00",
  "updated_at": "2026-07-01T14:00:00+00:00",
  "absolute_url": "https://job-boards.greenhouse.io/anthropic/jobs/5222180008",
  "source_url": "https://boards.greenhouse.io/anthropic",
  "fetched_at": "2026-07-18T14:03:00+00:00",
  "parse_warnings": []
}
```

- **`canonical_company_id`** resolves the same employer across ATS platforms and slug
  variants into one stable id (legal-form suffixes stripped, accents folded) — the join
  key to group a company's postings even when it uses Greenhouse *and* Ashby. No other
  actor reconciles company identity across ATS.
- **`fetched_at`** is the moment we confirmed this posting live from the ATS's own public
  endpoint — a verifiable fact, not an "indexed hourly" marketing claim. Cross-check any
  company against the public [freshness directory](https://george-kozlitin.github.io/ats-jobs-directory/).
- In delta mode, `new` records carry `first_seen_at` and `closed` records carry
  `days_open` (how long the req stayed open) — lifecycle data competitors can't produce
  on a first call.
- `salary` is filled **only** from structured ATS fields (Lever `salaryRange`, Ashby
  compensation tiers). We never invent numbers with AI.
- `remote_type` comes from the ATS's own flags where available (Lever
  `workplaceType`, Ashby `isRemote`), with a conservative location fallback.
- Descriptions (`description_text`/`description_html`) are off by default — enable
  `includeDescriptions` when you need them.
- Something unparseable? It lands in `parse_warnings`, never silently dropped.

### Delta mode — monitor hiring, don't re-buy the firehose

Set `onlyNewSinceLastRun: true` and the actor remembers what it has seen (per your
account): you get only **new** postings, plus `delta_status: "closed"` records when
a previously seen job disappears. Hiring-signal data at job-scraper prices.

### Directory-wide monitoring — watch a whole scope, not a list

Don't want to name companies at all? Set `directoryScope` to `all` or one ATS and the
actor pulls the live boards straight from our verified directory (most-active first,
capped by `directoryLimit`). Combine with `onlyNewSinceLastRun: true` and one scheduled
run becomes a **hiring monitor across thousands of companies** — new and closed jobs
only. This is the signal other tools sell at $20–50/1k; here it's the same $1/1k.

```json
{ "directoryScope": "ashby", "directoryLimit": 500, "onlyNewSinceLastRun": true }
```

### Honest failure modes

- A run that returns 0 jobs **always tells you why** in the status message (dead
  slugs? filters too narrow? nothing new since last run?) — and charges nothing.
- Per-board failures are listed in `RUN_SUMMARY.failedTargets` with reasons.
- Dead companies get pruned from the public directory daily — check it before running.

### Legal & fair use

This actor reads officially public, unauthenticated job-board endpoints
([Greenhouse Job Board API](https://developers.greenhouse.io/job-board.html),
[Lever Postings API](https://github.com/lever/postings-api),
[Ashby Posting API](https://developers.ashbyhq.com/docs/public-job-posting-api)) —
data employers publish precisely so it spreads. Workday postings are read from the
same public JSON endpoint the employer's career site itself uses — unauthenticated,
conservative ≤1 req/s per tenant, and any block is treated as a hard stop for that
tenant. Polite rate limits are enforced per host (Lever's robots.txt `Crawl-delay`
honored). Output contains **no personal data** — no recruiter names, no emails.
Takedown/opt-out: open an issue.

### Use with AI agents (MCP) — dedicated endpoint

This actor **is an MCP server** (standby mode, streamable HTTP):

```
https://zon4a--ats-jobs-api.apify.actor/mcp
```

Authenticate with your Apify token (`Authorization: Bearer <APIFY_TOKEN>`). Tools:

- **`search_jobs`** — companies + filters → normalized postings (charged at the same
  $1/1K as batch runs; results also land in your dataset)
- **`check_company`** — free live coverage check: which boards a company has right
  now and how many open jobs on each

Alternatively, use the official [Apify MCP server](https://mcp.apify.com) and call
`zon4a/ats-jobs-api` as a generic actor tool.

### Roadmap

- Directory-wide monitoring mode: one scheduled run watching the whole directory
- SmartRecruiters / Personio / Recruitee filters (already in our catalog)

# Actor input Schema

## `preset` (type: `string`):

Curated verified-live company bundle. Use it for a zero-config first run, or leave 'none' and provide your own companies below.

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

Board identifiers: bare slug ('stripe'), 'ats:slug' ('lever:netflix', 'workday:acme|wd5|careers'), or a board URL (https://boards.greenhouse.io/stripe, https://jobs.lever.co/netflix, https://jobs.ashbyhq.com/linear, https://acme.wd5.myworkdayjobs.com/careers). ATS is auto-detected from URLs; bare slugs are looked up across Greenhouse/Lever/Ashby.

## `titleKeyword` (type: `string`):

Case-insensitive substring match on the job title (e.g. 'devops').

## `locationKeyword` (type: `string`):

Case-insensitive substring match on raw location (e.g. 'madrid' or 'remote').

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

Keep only jobs detected as remote.

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

ISO date (YYYY-MM-DD). Jobs published/updated before this date are dropped. Note: Greenhouse boards expose updated_at only.

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

Cap per company. Default 50 keeps a first run under $0.10.

## `includeDescriptions` (type: `boolean`):

Include description_text/description_html in output (bigger items).

## `outputProfile` (type: `string`):

full = all normalized fields; compact = drops nulls and raw blocks; minimal = title/company/location/url/posted_at only.

## `includeRawJson` (type: `boolean`):

Attach the raw per-job payload under `raw` (full profile only).

## `onlyNewSinceLastRun` (type: `boolean`):

Delta mode: output only jobs not seen in previous runs of this actor (per-account memory). Closed jobs are emitted with status='closed'.

## `directoryScope` (type: `string`):

Instead of naming companies, monitor a whole scope from our verified-live directory. 'all' or one ATS. Combine with 'Only new since last run' for a directory-wide hiring monitor: one scheduled run, only new/closed jobs across thousands of companies.

## `directoryLimit` (type: `integer`):

When directoryScope is set: max companies to monitor, taken by open-job count (most active first).

## `webhookUrl` (type: `string`):

Optional. With 'Only new since last run', POST the new/closed job delta to this HTTPS URL when the run finishes — push instead of poll. Full data stays in the dataset.

## `webhookSecret` (type: `string`):

Optional. If set, the webhook body is signed with HMAC-SHA256 in the X-ATS-Signature header (sha256=...) so you can verify authenticity.

## Actor input object example

```json
{
  "preset": "top-tech",
  "companies": [],
  "remoteOnly": false,
  "maxJobsPerCompany": 50,
  "includeDescriptions": false,
  "outputProfile": "full",
  "includeRawJson": false,
  "onlyNewSinceLastRun": false,
  "directoryScope": "none",
  "directoryLimit": 250
}
```

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("zon4a/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 = {}

# Run the Actor and wait for it to finish
run = client.actor("zon4a/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 '{}' |
apify call zon4a/ats-jobs-api --silent --output-dataset

```

## MCP server setup

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