# Company Job Search API: Greenhouse, Lever, Ashby & more (`lucymade/ats-job-search`) Actor

Search open jobs posted directly on company career sites (Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee, BambooHR, Workday). Keyword, country, remote, salary and seniority filters, clean salary fields, and only-new-jobs monitoring.

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

## Pricing

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

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

### What does Company Job Search do?

**Company Job Search** finds open jobs posted directly on company career sites, the job boards that companies run themselves on **Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee, BambooHR and Workday**.

You can use it in two ways:

- **Search every company at once.** Type a job title like `data engineer`, pick countries, remote, salary or seniority, and get matching jobs from thousands of companies in seconds. Results come from an index that refreshes every company several times a day.
- **Pull every job from the companies you care about.** Give company names, website domains or careers-page links (`Stripe`, `notion.so`, `https://jobs.lever.co/spotify`). The Actor works out which job board each company uses and fetches its current openings.

Every job comes back in **one clean format**, whichever platform it came from: title, company, location, countries, remote/hybrid/on-site, employment type, seniority, department, posted date, apply link and, when the employer publishes one, **salary as numbers** (min, max, currency, period, and a yearly figure).

### Why use it?

- **Jobs from the source.** Postings come straight from the employer's own job board, so the links work and the jobs aren't reposts from aggregators.
- **Salary you can filter on.** Pay ranges are read from structured fields and from pay-transparency text in the description, then converted to yearly numbers. `minAnnualSalary: 150000, salaryCurrency: USD` just works.
- **Countries that are actually tagged.** Locations like `SF, CA`, `Remote - EMEA` or `Tel Aviv-Yafo` become `US`, `EMEA` and `IL`, so a country filter catches them.
- **Only new jobs.** Schedule it daily with *Only new jobs since my last run* and you get, and pay for, only the jobs that appeared since the previous run.
- **Recently closed jobs.** Jobs that a company took down are kept with a `closedAt` date, which is useful for tracking hiring at specific companies.
- **No setup.** No cookies, no logins, no proxies to configure.

### How to use it

1. Click **Try for free**.
2. Enter **job title keywords**, for example `product designer`, or leave it empty.
3. Add filters: countries (`US`, `GB`, `DE`, or `EU`/`EMEA`/`LATAM`), remote, minimum salary, posted within N days.
4. Optional: add **companies** to limit the search to them.
5. Click **Start**. Download the results as JSON, CSV or Excel, or connect them to Google Sheets, Slack, Zapier, Make or your own code through the Apify API.

#### Example inputs

Remote senior backend roles in the US paying at least $180k, posted this week:

```json
{
  "keywords": ["backend engineer", "software engineer"],
  "seniority": ["senior", "lead"],
  "workplaceTypes": ["remote"],
  "countries": ["US"],
  "minAnnualSalary": 180000,
  "salaryCurrency": "USD",
  "postedWithinDays": 7,
  "maxItems": 500
}
```

Every open job at a list of companies:

```json
{
  "companies": ["Stripe", "notion.so", "https://jobs.lever.co/spotify", "linear.app"],
  "maxItems": 2000
}
```

Daily alert for new data jobs in Europe (schedule it in Apify):

```json
{
  "keywords": ["data engineer", "analytics engineer"],
  "countries": ["EU", "GB"],
  "onlyNewSinceLastRun": true,
  "stateKey": "data-jobs-europe"
}
```

### Use it from ChatGPT, Claude or Cursor (MCP)

Let your AI assistant search jobs for you. Add this to any MCP client:

```
https://mcp.apify.com?tools=lucymade/ats-job-search
```

Then ask something like *"Find remote senior data engineer jobs in Europe posted this week that show a salary"*. The assistant fills in the filters and gets clean results back.

### Reliable, and checked every day

A self-built job scraper often breaks without anyone noticing: a company moves to another job platform or a board goes quiet, and its jobs silently stop showing up. This Actor is built to catch that:

- every supported platform is tested several times a day against live company boards
- companies that move platforms under the same name are followed automatically
- every job has a `lastSeenAt` time, so you can see how fresh each result is

### Output

Each job looks like this:

```json
{
  "id": "ashby:openai:8fb1615c-34bf-47c4-a1d1-b7b2f836bbd3",
  "title": "Technical Program Manager, Compute Infrastructure",
  "company": "OpenAI",
  "ats": "ashby",
  "atsBoard": "openai",
  "url": "https://jobs.ashbyhq.com/openai/8fb1615c-34bf-47c4-a1d1-b7b2f836bbd3",
  "applyUrl": "https://jobs.ashbyhq.com/openai/8fb1615c-34bf-47c4-a1d1-b7b2f836bbd3/application",
  "location": "San Francisco",
  "locations": ["San Francisco"],
  "countries": ["US"],
  "workplaceType": "hybrid",
  "department": "Technical Program Management",
  "team": "Technical Program Management",
  "employmentType": "full-time",
  "seniority": null,
  "salary": {
    "min": 207000, "max": 285000, "currency": "USD", "period": "year",
    "annualMin": 207000, "annualMax": 285000, "source": "structured"
  },
  "visaSponsorship": null,
  "postedAt": "2026-11-05T02:20:02.702Z",
  "firstSeenAt": "2026-11-05T02:20:02.702Z",
  "lastSeenAt": "2026-11-09T09:30:11.000Z",
  "closedAt": null,
  "status": "open"
}
```

| Field | Meaning |
|---|---|
| `countries` | ISO country codes plus region tags (`EU`, `EMEA`, `APAC`, `LATAM`, `NA`, `WORLD`) |
| `workplaceType` | `remote`, `hybrid`, `onsite` or `null` when the employer doesn't say |
| `seniority` | Read from the title: `intern`, `entry`, `mid`, `senior`, `lead`, `manager`, `director`, `executive` |
| `salary.source` | `structured` when the platform has a pay field, `description` when read from the posting text |
| `salary.annualMin/annualMax` | Pay converted to a yearly amount in the same currency |
| `visaSponsorship` | `true` or `false` when the description says so, otherwise `null` |
| `firstSeenAt` | When the job appeared (the platform's posting date for jobs that existed before we started tracking the company) |
| `closedAt` | When the job disappeared from the company's board |

Turn on **Include full job description** to add the description as plain text.

The run's **OUTPUT** record lists, for each company you entered, which job board was found, so you can see what matched and what didn't.

### How much does it cost?

You pay **per job returned**. The price is shown on the Pricing tab. Companies we can't find, empty boards and filtered-out jobs are free. With *Only new jobs since my last run*, a daily alert usually returns a handful of jobs, so it costs cents a month.

Apify's free plan includes monthly credit, which is enough to try the Actor many times.

### Coverage

The search index tracks thousands of company job boards and grows every day:

- when a company moves from one platform to another under the same name, it is picked up automatically
- every company you look up that isn't indexed yet is fetched live, then added to the index

Workday boards are fetched live when you give a Workday careers link.

If a company you need isn't found, paste its careers-page link. That usually works when the name alone doesn't.

### FAQ

**Is this legal?** The Actor reads the public job-board feeds that Greenhouse, Lever, Ashby and the others publish for exactly this purpose: showing a company's open jobs. It doesn't log in, doesn't touch candidate data and collects no personal information beyond what a company puts in its own job ads. Use the data in line with the laws that apply to you.

**How fresh is it?** Indexed companies are refreshed several times a day. `lastSeenAt` on every job tells you when it was last confirmed open. Companies fetched live are as fresh as the moment of your run.

**Why is a field `null`?** We only fill a field when the employer's posting supports it. A `null` salary means the company didn't publish one. We don't guess.

**Can I get LinkedIn or Indeed jobs?** No. This Actor covers jobs on company career sites. Many companies post there first, and some only post there.

**Found a problem?** Open an issue on the Issues tab with the input you used.

# Actor input Schema

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

Return jobs whose title contains any of these words or phrases, e.g. "data engineer", "react", "product designer". Leave empty to get all jobs.

## `keywordScope` (type: `string`):

Search only the job title (precise) or the title and the full description (broader).

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

Drop jobs whose title contains any of these, e.g. "intern", "senior", "sales".

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

Company names, website domains or careers-page links, e.g. "Stripe", "notion.so", "https://jobs.lever.co/spotify". We find which job board each company uses. Companies we can't find are listed in the run's OUTPUT and never charged. Leave empty to search every company in the index.

## `countries` (type: `array`):

Two-letter country codes (US, GB, DE, CA, IN, ...) or regions: EU, EMEA, APAC, LATAM, NA, WORLD ("anywhere").

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

Free-text match on the location, e.g. "Berlin", "New York", "Remote".

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

Remote, hybrid or on-site.

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

Full-time, part-time, contract, internship or temporary, as stated by the employer.

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

Worked out from the job title, e.g. Senior, Staff, Manager, Director.

## `departments` (type: `array`):

e.g. "Engineering", "Marketing".

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

Only jobs posted in the last N days.

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

Skip jobs that don't publish a pay range.

## `minAnnualSalary` (type: `integer`):

Top of the posted range must be at least this much per year. Hourly and monthly pay is converted to yearly. Jobs without a salary are excluded when this is set.

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

Use with minimum salary, e.g. USD, EUR, GBP, CAD.

## `visaSponsorship` (type: `boolean`):

Keep only jobs whose description says visa sponsorship is available.

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

For scheduled runs: return only jobs that appeared after the previous run with the same memory name. You pay only for the new jobs.

## `stateKey` (type: `string`):

Use a different name for each saved search you schedule.

## `status` (type: `string`):

Open jobs (default), or jobs the company recently took down (useful for hiring-signal research).

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

With "Recently closed": only jobs taken down in the last N days.

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

Limit to some platforms. Leave empty for all.

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

Adds the description as plain text (and HTML for live-fetched companies).

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

Stop after this many jobs.

## Actor input object example

```json
{
  "keywords": [
    "software engineer"
  ],
  "keywordScope": "title",
  "hasSalary": false,
  "visaSponsorship": false,
  "onlyNewSinceLastRun": false,
  "stateKey": "default",
  "status": "open",
  "includeDescription": false,
  "maxItems": 100
}
```

# Actor output Schema

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

All matching jobs in one normalized format.

## `summary` (type: `string`):

Which job board was found for each company you entered, and counts.

# 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": [
        "software engineer"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lucymade/ats-job-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": ["software engineer"] }

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

```

## MCP server setup

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