# ATS Jobs Aggregator: Greenhouse, Lever, Ashby, SmartRecruiters (`devsolal/ats-jobs-aggregator`) Actor

- **URL**: https://apify.com/devsolal/ats-jobs-aggregator.md
- **Developed by:** [David-Solal Chalmeau](https://apify.com/devsolal) (community)
- **Categories:** Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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 Aggregator: Greenhouse, Lever, Ashby & SmartRecruiters

Get every open job at companies you name, in one flat table, from each company's own public job-board API. Give it a list of companies (`ashby:ashby`, `greenhouse:stripe`, `lever:ro`, `smartrecruiters:smartrecruiters`) and get one row per job with title, department, location, remote flag, employment type, posting dates, apply URL and a plain-text description. Built for job aggregators, market research, competitor hiring tracking and LLM agents that need clean structured rows rather than scraped HTML.

### What you get per job

| Field | Notes |
|---|---|
| `company`, `ats` | The company you asked for and the ATS it came from |
| `jobId` | The board's own job id (stable across runs) |
| `title`, `department`, `location` | Department and location are only as precise as the ATS provides |
| `remote` | `true` / `false` / `null`. `null` means the ATS does not publish remote status (Greenhouse) — it is never guessed |
| `employmentType` | e.g. `Full-time`, `FullTime`. Only some ATS provide it (null otherwise) |
| `postedAt`, `updatedAt` | ISO 8601, or `null` when the ATS does not publish the field (Lever/Ashby/SmartRecruiters have no `updatedAt`; Greenhouse has both) |
| `applyUrl`, `jobUrl` | Direct application link and job-page link |
| `descriptionText` | Plain text, HTML stripped, max 2000 characters. `null` for SmartRecruiters (its public list endpoint carries no description) |
| `sourceUrl`, `fetchedAt` | The exact API URL that produced the row, and when it was fetched |

### Example input

```json
{
  "companies": ["ashby:ashby", "greenhouse:stripe", "lever:ro", "smartrecruiters:smartrecruiters"],
  "keywords": ["engineer"],
  "locations": ["Berlin", "Remote"],
  "remoteOnly": false,
  "maxJobsPerCompany": 200,
  "maxCompanies": 20
}
```

`companies` accepts `"ats:slug"` strings or `{"ats": "greenhouse", "slug": "stripe"}` objects. Keywords match the title or department (case-insensitive), locations match as substrings, and `remoteOnly: true` keeps only jobs the ATS explicitly marks remote.

### Example output

Real rows from a run of the input above (descriptions truncated here for readability):

```json
{
  "company": "ashby",
  "ats": "ashby",
  "jobId": "7458d4e9-da2e-47bd-98cb-adfda43d42b2",
  "title": "Engineering Manager - EU",
  "department": "Engineering",
  "location": "Remote - European Union",
  "remote": true,
  "employmentType": "FullTime",
  "postedAt": "2024-03-04T14:29:08.532Z",
  "updatedAt": null,
  "applyUrl": "https://jobs.ashbyhq.com/ashby/7458d4e9-da2e-47bd-98cb-adfda43d42b2/application",
  "jobUrl": "https://jobs.ashbyhq.com/ashby/7458d4e9-da2e-47bd-98cb-adfda43d42b2",
  "descriptionText": "Hi 👋 I'm Colin https://uk.linkedin.com/in/colinhoweuk, Director of Engineering, Europe. How do you feel about software e...",
  "sourceUrl": "https://api.ashbyhq.com/posting-api/job-board/ashby?includeCompensation=false",
  "fetchedAt": "2026-10-28T05:26:19.043Z"
}
```

```json
{
  "company": "ro",
  "ats": "lever",
  "jobId": "3910baa9-f961-4380-ad0a-01deec07f7a2",
  "title": "Analytics Engineer, Conversational Analytics",
  "department": "Data",
  "location": "New York, NY",
  "remote": false,
  "employmentType": "Full-time",
  "postedAt": "2026-09-16T17:43:56.068Z",
  "updatedAt": null,
  "applyUrl": "https://jobs.lever.co/ro/3910baa9-f961-4380-ad0a-01deec07f7a2/apply",
  "jobUrl": "https://jobs.lever.co/ro/3910baa9-f961-4380-ad0a-01deec07f7a2",
  "descriptionText": "Join Tech @ Ro to build the future of healthcare, from the g...",
  "sourceUrl": "https://api.lever.co/v0/postings/ro?mode=json",
  "fetchedAt": "2026-10-28T05:26:19.065Z"
}
```

A company that cannot be fetched yields **one** row instead of jobs, and the run continues:

```json
{
  "company": "this-board-does-not-exist-xyz123",
  "ats": "greenhouse",
  "error": "BOARD_NOT_FOUND",
  "message": "Greenhouse board not found: this-board-does-not-exist-xyz123"
}
```

`error` is `BOARD_NOT_FOUND` (no such board), `UPSTREAM_ERROR` (ATS returned an error or unusable payload) or `INVALID_COMPANY` (unknown ATS name or empty slug).

### How to find a company's slug

Open the company's careers page and read the URL: `boards.greenhouse.io/<slug>`, `jobs.lever.co/<slug>`, `jobs.ashbyhq.com/<slug>`, `jobs.smartrecruiters.com/<slug>`. For companies that host the board on their own domain (e.g. Stripe's `stripe.com/jobs`), the slug is the Greenhouse/Lever identifier, usually visible in the embedded job-board iframe URL.

### Pricing

Pay per event: a small start fee per run plus **$0.0018 per delivered job row**. Companies whose board is missing or fails, invalid company inputs, and jobs removed by your filters are free — you are only charged for rows you actually receive. If your spending limit is reached the Actor stops cleanly instead of continuing unpaid. Current prices are shown on this page.

### Limits and honest notes

- **Sources**: the official public job-board APIs of Greenhouse, Lever, Ashby and SmartRecruiters — the endpoints each vendor publishes for embedding a company's board on its own site. No login, no CAPTCHA or anti-bot bypass, no proxies.
- **Companies are user-supplied.** The Actor only reads boards you name; it does not discover or crawl companies on its own.
- **No personal data beyond the posting.** Rows contain only what the job posting publishes. No recruiter names, emails or phone numbers are extracted, even when a description mentions them.
- **Hard caps**: at most 100 companies and 1000 jobs per company per run (defaults 20 and 200). For Greenhouse, Lever and Ashby the whole board arrives in one response, so the cap is applied after your filters and `maxJobsPerCompany: 200` means at most 200 matching jobs. For SmartRecruiters the same cap also bounds how many postings are fetched while it paginates (100 per request), so a highly selective filter on a large SmartRecruiters board can return fewer rows than the cap — raise `maxJobsPerCompany` there if you need deeper coverage.
- **Field coverage differs by ATS.** Greenhouse publishes no remote flag and no employment type, so those are `null`. Lever publishes no description HTML update time; Ashby and Lever publish no `updatedAt`. SmartRecruiters' public list endpoint has no description, so `descriptionText` is `null` there.
- **SmartRecruiters paginates** 100 postings per request, up to your `maxJobsPerCompany`. Its unknown-company response is an empty list rather than an error, so a typo in a SmartRecruiters slug yields no rows and no error row.
- **Descriptions are truncated at 2000 characters** to keep rows small; the full posting is always at `jobUrl`.
- **Politeness**: boards are fetched with concurrency 3 and retries with exponential backoff on HTTP 429/5xx. Each board is fetched at most a handful of times per run (once, or one page per 100 postings for SmartRecruiters).
- Listings change constantly; `fetchedAt` tells you exactly when a row was read.

# Actor input Schema

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

Companies to pull open jobs from, as "ats:slug" strings (e.g. "greenhouse:stripe") or {ats, slug} objects. ats is one of greenhouse, lever, ashby, smartrecruiters. slug is the company's board identifier on that ATS (usually visible in their careers page URL).

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

Optional. Keep only jobs whose title or department contains any of these words (case-insensitive).

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

Optional. Keep only jobs whose location contains any of these substrings (case-insensitive), e.g. "New York", "Remote", "Germany".

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

Keep only jobs explicitly marked remote by the ATS. Jobs where the ATS doesn't expose remote status are excluded too (never guessed).

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

Upper bound on jobs taken from a single company's board, after filters (hard limit 1000).

## `maxCompanies` (type: `integer`):

Upper bound on companies processed in this run (hard limit 100).

## Actor input object example

```json
{
  "companies": [
    "ashby:ashby",
    "greenhouse:stripe",
    "lever:ro",
    "smartrecruiters:smartrecruiters"
  ],
  "remoteOnly": false,
  "maxJobsPerCompany": 200,
  "maxCompanies": 20
}
```

# Actor output Schema

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

All delivered job rows (plus free error rows for companies that could not be fetched) as dataset items.

# 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": [
        "ashby:ashby",
        "greenhouse:stripe",
        "lever:ro",
        "smartrecruiters:smartrecruiters"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("devsolal/ats-jobs-aggregator").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": [
        "ashby:ashby",
        "greenhouse:stripe",
        "lever:ro",
        "smartrecruiters:smartrecruiters",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("devsolal/ats-jobs-aggregator").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": [
    "ashby:ashby",
    "greenhouse:stripe",
    "lever:ro",
    "smartrecruiters:smartrecruiters"
  ]
}' |
apify call devsolal/ats-jobs-aggregator --silent --output-dataset

```

## MCP server setup

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

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/0rq8B6znOHYFyhjVz/builds/qefHveztbqRqSxkjN/openapi.json
