# ATS Jobs Search: Greenhouse, Lever & Ashby Jobs API (`redomic/ats-jobs-search`) Actor

Search 328,000+ open jobs from 11,000+ companies on Greenhouse, Lever and Ashby in one run. Filter by keyword, location, remote, salary, seniority and posting date. Flat rows with annual salary, days open and company hiring velocity. Lead-list mode and daily new-jobs monitor.

- **URL**: https://apify.com/redomic/ats-jobs-search.md
- **Developed by:** [Redomic](https://apify.com/redomic) (community)
- **Categories:** Jobs, Lead generation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 jobs

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 Search: Greenhouse, Lever & Ashby Jobs API

**Search 328,829 open jobs across 11,242 companies on Greenhouse, Lever and Ashby in one run. Updated daily. 152,559 with a salary.**

Type what you are looking for once, across every company, instead of scraping one careers page at a time:

- "every remote data engineer job with a USD salary"
- "all new sales roles first seen this week, one row per company" (a lead list)
- "Python jobs in Berlin, sent to me only when a new one appears" (a daily monitor)

No scraping happens at run time: you search our own daily snapshot of 11,000+ career sites (5,838 Greenhouse, 3,501 Ashby,
1,903 Lever companies), so a full search over every open job finishes in seconds and never gets blocked.

### What you get that a live scraper can't give you

| Field | What it tells you |
|---|---|
| `salaryAnnualMin`, `salaryAnnualMax` | Pay normalised to a yearly number (hourly x 2080, monthly x 12), so you can sort and filter across companies. A yearly figure a company mis-tagged as monthly or hourly is kept as yearly, not multiplied |
| `daysOpen` | Days since the job first appeared in our daily crawl (`openSinceBeforeTracking` = it was already open when tracking began). For closed jobs, this is `firstSeen` to `closedOn` |
| `closedOn` | The day a job disappeared from its board (`null` while open). With `daysOpen`, this is time-to-fill for roles that closed |
| `companyOpenRoles` | How many jobs this company has open right now, across all its roles |
| `companyNewRoles7d` | How many new roles the company opened in the last 7 days: a hiring-velocity signal for sales and recruiting |
| `firstSeen` | The day the job first appeared, even when the company never shows a posted date |
| `locationCount`, `otherLocations` | The same role posted in many cities comes back once, with every location listed (and is billed once) |
| `evergreen` | Still open more than 180 days after posting: usually a standing ad, not a fresh opening |

### Sample output (real run, 5 Nov 2026)

Input: `{"keywords": ["data engineer"], "remoteOnly": true, "hasSalary": true, "salaryCurrency": "USD", "maxItems": 5}`
matched 124 of 328,829 open jobs.

| company | title | location | salaryAnnualMin | salaryAnnualMax | postedAt | companyOpenRoles | companyNewRoles7d |
|---|---|---|---|---|---|---|---|
| Natera | Senior Data Engineer, Data Foundations | US Remote | 125000 | 156300 | 2026-11-04 | 224 | 30 |
| tala | Senior Data Engineer | US | 130000 | 200000 | 2026-11-04 | 11 | 2 |
| Machinify | Senior Data Engineer | Remote - US | 180000 | 220000 | 2026-11-03 | 61 | 8 |
| Glacier | Computer Vision DATA Engineer - Fully Remote | Remote Latin America based | 24000 | 42000 | 2026-11-03 | 4 | 0 |

Every row also has `ats`, `board`, `id`, `department`, `workplaceType`, `employmentType`, `salaryMin`, `salaryMax`,
`salaryCurrency` (ISO 4217), `salaryPeriod`, `roleFamily`, `seniority`, `remote`, `daysSincePosted`, `url` (the apply
link) and `dataAsOf`. All columns are flat, so the CSV or Excel export opens cleanly.

### Input recipes

**Remote jobs with a salary**

```json
{"keywords": ["python", "backend"], "remoteOnly": true, "hasSalary": true, "minSalary": 150000, "salaryCurrency": "USD", "sort": "salary"}
```

**New today in one city**

```json
{"locations": ["London"], "firstSeenWithinDays": 1, "maxItems": 500}
```

**One department across all companies**

```json
{"keywords": ["sales"], "keywordsInDepartment": true, "seniority": ["Senior", "Lead/Principal"]}
```

**Lead list: companies hiring sales people this week** (one row per company)

```json
{"keywords": ["account executive", "sales development"], "firstSeenWithinDays": 7, "output": "companies", "maxItems": 200}
```

Real run, 5 Nov 2026 (with `"excludeCompanies": ["jobgether"]`): 776 matching sales roles first seen this week.

| company | matchingJobs | newMatchingJobs7d | companyOpenRoles | salaryAnnualMinUsd | salaryAnnualMaxUsd | topDepartments |
|---|---|---|---|---|---|---|
| bluelightconsulting | 203 | 203 | 815 | | | Sales & Marketing |
| Toast | 26 | 26 | 341 | 115000 | 237000 | Sales : Retail Sales AEs; Sales : Strategic Cuisines Sales AEs |
| Databricks | 19 | 19 | 927 | 272000 | 428450 | Enterprise Sales; Commercial & Mid-Market Sales |
| HubSpot | 18 | 18 | 106 | | | Sales |

Company rows also have `ats`, `board`, `boardUrl` (the careers page), `remoteShare` (0-1), `topRoleFamily`,
`sampleTitles` (up to 3), `latestPostedAt` and `dataAsOf`. Company rows count every matching posting.

**Only some companies or platforms**

```json
{"companies": ["stripe", "notion", "ramp"], "ats": ["greenhouse", "ashby"]}
```

**Closed-role history (time-to-fill)**

```json
{"keywords": ["data engineer"], "includeClosed": true, "closedWithinDays": 30, "maxItems": 200}
```

Set `includeClosed: true` to also return jobs that closed since tracking began. Closed rows carry `closedOn` (the day the job disappeared) and `daysOpen` (firstSeen to closedOn); open rows carry `closedOn: null`. Setting `closedWithinDays: 30` keeps only jobs closed in the last 30 days, so the result is a clean time-to-fill sample without daily-expiry logic of your own.

**Skip regions you don't hire in**

```json
{"keywords": ["python"], "remoteOnly": true, "excludeLocations": ["India", "LATAM"]}
```

**Full posting text for a shortlist**

```json
{"keywords": ["data engineer"], "maxItems": 5, "includeDescription": true}
```

Each returned row then has non-empty `descriptionText` on reachable boards and `descriptionSnippet` for a quick scan. A company whose board is gone gives `descriptionStatus: "unavailable"` instead of failing the run.

### Field reference (job rows)

| Field | Meaning |
|---|---|
| `company`, `ats`, `board`, `id` | Company name, platform (greenhouse, lever, ashby), careers board token, the job's id on that board |
| `title`, `department`, `roleFamily`, `seniority` | As posted; `roleFamily` and `seniority` are our classification of the title |
| `location`, `workplaceType`, `remote` | Location text as the company wrote it; Remote/Hybrid/Onsite when the company says so (often empty); `remote` is true when either says remote |
| `employmentType` | Full-time, Part-time, Contract, Internship, Temporary, when the company says so |
| `postedAt`, `daysSincePosted` | The company's own posted (or last updated) date and its age in days. The most reliable freshness column |
| `firstSeen`, `trackedSince` | First day the job appeared in our daily crawl; first day we crawled this company. Equal = it was already open when we started |
| `daysOpen`, `openSinceBeforeTracking` | Days since `firstSeen` for jobs that opened after we started tracking the company, else empty + `true`. For closed jobs, `daysOpen` is `firstSeen` to `closedOn` |
| `closedOn` | Day the job closed (disappeared from its board); `null` while open. Together with `daysOpen`, this is time-to-fill |
| `evergreen` | Posted more than 180 days ago and still open |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod` | The posted range as published (ISO 4217 currency) |
| `salaryAnnualMin`, `salaryAnnualMax` | The same range as a yearly figure |
| `locationCount`, `otherLocations` | How many locations this role is posted in, and every location besides `location` |
| `companyOpenRoles`, `companyNewRoles7d` | The company's open jobs today, and how many of them first appeared in the last 7 days |
| `url`, `dataAsOf` | Apply link; date of the snapshot the row came from |
| `descriptionText`, `descriptionSnippet`, `descriptionStatus` | Only when `includeDescription: true`: full posting as plain text (up to 20000 chars), its first 280 chars, and `ok` or `unavailable` when that company's board could not be reached live. Off by default with no extra calls. |

Every run ends with two free rows: a status row (`_status`: ok or partial) and a summary row (`matched`, `scanned`,
`returned`, `dataAsOf`, the filters used, and for monitors `newSinceLastRun`). Filter them out with `_status`.

**Aggregators are left out by default.** A few boards (for example jobgether) re-post other companies' jobs by the
thousand, so they would crowd every "newest" search. Boards with over 1,000 open jobs, most of them new this week, are
recomputed daily and skipped; set `"includeAggregators": true` to get them back. `excludeCompanies` drops any other
board you don't want.

**Only real openings.** Talent pools, "general application" and "future opportunities" postings are left out by
default (`"includeGeneralApplications": true` brings them back, flagged `generalApplication`). When the keywords themselves
ask for them (for example `"talent pool"` or `"general application"`), they are included automatically. Salary figures that
cannot be real pay (a $0 range, "$19 a year") are dropped rather than shown wrong, and workplace type is only
Remote, Hybrid or Onsite.

### Daily monitor (schedules)

Set `onlyNewSinceLastRun: true` and schedule the Actor daily. The first run saves a baseline, and every later run returns only
the matching jobs that were not there last time. A quiet day returns just the free summary row, so you pay only for new jobs.
Each distinct filter set keeps its own history, so you can run many monitors side by side. Connect the run to Slack, email,
Google Sheets or a webhook through Apify integrations.

### Filters

Keywords (whole-word, any-of, title or title + department), exclude keywords, locations (any-of), exclude locations, exclude companies, remote only, workplace type,
employment type, role family, seniority, platform, company, has salary, minimum annual salary + currency, posted within N days,
first seen within N days, include recently closed jobs (`includeClosed`) with `closedOn`/`daysOpen` optionally limited to closures in the last N days (`closedWithinDays`),
sort by newest or highest salary, and max results. Every field is optional: an empty input returns
the newest 100 jobs.

**Free preview (`countOnly`):** set `countOnly: true` to scan and return only the summary (`matched`, `scanned`, `dataAsOf`) with 0 dataset items and no billing.

### Pricing

$0.003 per row returned (a job, or a company row in lead-list mode). No start fee. The summary row is free, and quiet monitor days cost nothing.
1,000 jobs = $3.

### FAQ

**How fresh is the data?** Every career site is crawled once a day (around 23:00 UTC). `dataAsOf` on every row shows the snapshot date.
It is not real-time: a job posted an hour ago appears the next day.

**Do I get the job description?** Not by default. Set `includeDescription: true` and each returned job row also carries `descriptionText` (full text), `descriptionSnippet` (first 280 chars) and `descriptionStatus`.
The text is fetched live from the company's public board for the rows you asked for, so keep `maxItems` small when you turn it on. If a board is down or moved, that row says `unavailable` and the run still succeeds.

**Which companies are covered?** 11,242 companies with a public Greenhouse, Lever or Ashby careers board, from startups to
public companies. We add new boards every day.

**Why are some salaries missing?** Only companies that publish a pay range have one: 152,559 of 328,829 open jobs do.
Filter with `hasSalary` to keep only those.

### Limits

- Greenhouse, Lever and Ashby only (no Workday, LinkedIn or Indeed).
- History starts on 31 Oct 2026, so `daysOpen` is exact only for jobs that opened after that date.
- Locations are the company's own text, matched as whole words: "US" matches "Austin, TX, US" but not "Russia"; "Berlin" matches "Berlin HQ".

Questions or a missing filter? Open an issue on the Actor's Issues tab. We read every one.

### Use it from AI agents (MCP) and Python

**AI agents.** Add this Actor as a tool in Claude, Cursor, VS Code or any MCP client through the Apify MCP server. The agent reads the input schema, so a plain request is enough.

```json
{
  "mcpServers": {
    "ats-jobs-search": {
      "url": "https://mcp.apify.com?tools=redomic/ats-jobs-search",
      "headers": {
        "Authorization": "Bearer <YOUR_APIFY_TOKEN>"
      }
    }
  }
}
```

**Python.** `pip install apify-client`, then:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("redomic/ats-jobs-search").call(run_input={"keywords": ["engineer"], "maxItems": 20})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)
```

### More from us

- [Ashby Jobs Scraper with Salaries](https://apify.com/redomic/ashby-jobs-monitor): Ashby jobs scraper for any startup careers board (OpenAI, Ramp, Notion and thousands more), with salary ranges as numbers: min, max, currency, period, plus equity and bonus flags
- [Google Jobs Scraper - Job Postings API](https://apify.com/redomic/google-jobs): Google Jobs and job postings scraper: get job title, company, location, posted date, salary, full description and every apply link for any search
- [Google Trends API: Trending Now & Interest Over Time](https://apify.com/redomic/google-trends): Trending-now searches and new-trends alerts/monitoring per country with the news behind them, plus interest over time, interest by region, and related and rising queries for any keyword
- [Greenhouse Jobs Monitor & Scraper](https://apify.com/redomic/greenhouse-jobs-monitor): Monitor Greenhouse jobs from any company careers board (Airbnb, Stripe, Discord and thousands more)
- [Lever Jobs Scraper with Salaries](https://apify.com/redomic/lever-jobs-monitor): Lever jobs scraper for any company's careers page (Palantir, Spotify, Zoox, Shield AI and thousands more): flat rows with title, team, location, workplace, commitment, apply link, salary as numbers and the full description merged

# Changelog

This Actor's version history is a separate document: https://apify.com/redomic/ats-jobs-search/changelog.md

# Actor input Schema

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

Return jobs whose title contains ANY of these words (whole words, case-insensitive). Empty = every job.

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

Drop jobs whose title contains any of these words, e.g. intern, senior, manager.

## `keywordsInDepartment` (type: `boolean`):

Match keywords against the department/team as well as the title (e.g. 'Sales' finds every role in a Sales team).

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

Keep jobs whose location contains ANY of these (city, country or region text, e.g. Berlin, United States, London, EMEA).

## `excludeLocations` (type: `array`):

Drop jobs whose location contains any of these, e.g. India, Philippines.

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

Only remote jobs (workplace type Remote, or 'remote' in the location).

## `workplaceTypes` (type: `array`):

Remote, Hybrid or Onsite. Empty = all.

## `employmentTypes` (type: `array`):

Empty = all.

## `roleFamilies` (type: `array`):

Job function. 'Engineering' covers every engineering family. Empty = all.

## `seniority` (type: `array`):

Empty = all.

## `ats` (type: `array`):

Limit to Greenhouse, Lever or Ashby companies. Empty = all three.

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

Limit to these companies: board token (as in boards.greenhouse.io/<token>) or company name. Empty = all 11,000+.

## `excludeCompanies` (type: `array`):

Drop these companies (board token or name), e.g. job aggregators such as jobgether.

## `includeAggregators` (type: `boolean`):

Aggregator and staffing boards (for example jobgether) re-post other companies' jobs by the thousand. They are left out by default so results are employers' own openings; turn this on to include them. The list is recomputed daily: boards with over 1,000 open jobs, most of them new this week.

## `includeGeneralApplications` (type: `boolean`):

Talent pools, 'general application' and 'future opportunities' postings collect CVs rather than fill a role. They are left out by default; turn this on to include them (each row then carries generalApplication: true).

## `includeClosed` (type: `boolean`):

Also return jobs that have closed since tracking began, with closedOn (the day the job disappeared) and daysOpen (firstSeen to closedOn). Open jobs carry closedOn: null. Use this for time-to-fill and hiring-history work; combine with Closed within (days) to limit to recent closures.

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

For returned job rows only, live-fetch the full posting text from the company's public ATS board (one board call per company) and add descriptionText (plain text, up to 20000 chars), descriptionSnippet (first 280 chars) and descriptionStatus (ok or unavailable when the board cannot be reached). Off by default: no board calls are made. Ignored for countOnly and company lead lists.

## `closedWithinDays` (type: `integer`):

Only recently closed jobs: closedOn within the last N days. 0 = any close date. Setting this also includes closed jobs.

## `hasSalary` (type: `boolean`):

Only jobs that publish a pay range.

## `minSalary` (type: `integer`):

Keep jobs whose top of range, annualised (hourly x 2080, monthly x 12), is at least this. 0 = no minimum. Combine with salary currency.

## `salaryCurrency` (type: `string`):

ISO currency code to keep, e.g. USD, EUR, GBP. Empty = any currency.

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

Only jobs the company posted in the last N days. 0 = any date.

## `firstSeenWithinDays` (type: `integer`):

Only jobs that first appeared in our daily crawl in the last N days: genuinely new openings, even when the posted date is old or missing. 0 = any.

## `output` (type: `string`):

'companies' rolls the matching jobs up into one row per company: matching roles, new this week, pay range, top departments. Built for sales and recruiting lead lists.

## `groupSameTitle` (type: `boolean`):

When a company posts the same title in many cities or countries, return it once with locationCount and otherLocations, and bill it once.

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

Schedule this daily: the first run saves a baseline, every later run returns only matching jobs that were not there last time. A quiet day returns just the free summary row. Jobs output only.

## `maxItems` (type: `integer`):

Maximum rows returned (and billed).

## `countOnly` (type: `boolean`):

Return a free summary with matched, scanned and dataAsOf counts and 0 dataset items. No rows are returned or billed.

## `sort` (type: `string`):

Order of job rows.

## Actor input object example

```json
{
  "keywords": [
    "data engineer"
  ],
  "excludeKeywords": [],
  "keywordsInDepartment": false,
  "locations": [],
  "excludeLocations": [],
  "remoteOnly": false,
  "workplaceTypes": [],
  "employmentTypes": [],
  "roleFamilies": [],
  "seniority": [],
  "ats": [],
  "companies": [],
  "excludeCompanies": [],
  "includeAggregators": false,
  "includeGeneralApplications": false,
  "includeClosed": false,
  "includeDescription": false,
  "closedWithinDays": 0,
  "hasSalary": false,
  "minSalary": 0,
  "salaryCurrency": "",
  "postedWithinDays": 0,
  "firstSeenWithinDays": 0,
  "output": "jobs",
  "groupSameTitle": true,
  "onlyNewSinceLastRun": false,
  "maxItems": 100,
  "countOnly": false,
  "sort": "newest"
}
```

# Actor output Schema

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

All result rows from 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 = {
    "keywords": [
        "data engineer"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("redomic/ats-jobs-search").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 = { "keywords": ["data engineer"] }

# Run the Actor and wait for it to finish
run = client.actor("redomic/ats-jobs-search").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 '{
  "keywords": [
    "data engineer"
  ]
}' |
apify call redomic/ats-jobs-search --silent --output-dataset

```

## MCP server setup

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

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/7V6GtmUh7QVJvrNGN/builds/Yh9fd6grQa1anA5vW/openapi.json
