# ATS Jobs Scraper | Greenhouse, Lever and Ashby (`silentflow/ats-jobs-scraper`) Actor

Search job listings published on company career sites running Greenhouse, Lever, Ashby, SmartRecruiters, or Recruitee. Filter by keyword, location, remote status, and posting date, and get full job descriptions with stable IDs, ideal for job boards, recruiters, and sales hiring signal tracking.

- **URL**: https://apify.com/silentflow/ats-jobs-scraper.md
- **Developed by:** [SilentFlow](https://apify.com/silentflow) (community)
- **Categories:** Jobs, Lead generation, Business
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.40 / 1,000 results

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?

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 Scraper

**Search the jobs companies publish on their own career sites, by keyword, location and date, without knowing which companies to look at.** One input, five hiring systems (Greenhouse, Lever, Ashby, SmartRecruiters, Recruitee), full descriptions included. A keyword search returns matching jobs in seconds, not after an afternoon of career page checks.

### How it works

![How it works](https://api.apify.com/v2/key-value-stores/YXm81xySHg6uRkewS/records/ats-jobs-scraper-how-it-works-v1.png)

1. Enter keywords, a location, a date window or a list of companies.
2. The scraper searches a daily rebuilt index of jobs published on company career sites.
3. Every returned job comes back as one structured row, with its full description fetched live.

### ✨ Why teams choose this over other ATS scrapers

Still finding out about openings after everyone else? Tired of aggregators that lag by days and cut descriptions short? Do you know which hiring system each company uses? Nobody does, and you should not have to.

- 🔎 **Search without naming a company.** Type `backend engineer` and `Berlin` and get every matching job across every indexed company. Other career site scrapers need the company name and its system first.
- 🧩 **Five systems, one set of fields.** Greenhouse, Lever, Ashby, SmartRecruiters and Recruitee jobs come back with the same 14 fields, so one script handles all of them.
- 📄 **Full descriptions, fetched live.** The description is pulled from the career site during the run, never from a stale copy. In the reference run, 22 of 25 jobs came back with their complete description.
- 🆔 **Stable IDs and permanent URLs.** `platformId` is the employer's own job ID and `url` is the career site page, so deduplication and archiving just work.
- 📅 **Fresh by design.** The index is rebuilt every day and results are sorted newest first, with a `postedDate` in RFC 3339 for every job.
- 🌍 **Country codes on every job.** `countryCode` is normalized to ISO codes and `isRemote` is set from the employer's own flag, so filtering by geography takes one line.
- 🚫 **No login, no API key.** Career site data is public. You start a run and get rows.

### 🎯 What you can do with ATS job data

| Team | What they build |
|---|---|
| Job boards | A niche board fed with jobs that reach aggregators days later, refreshed every morning |
| Recruiting agencies | A daily list of new openings matching their placements, with the employer's own job ID to avoid double outreach |
| Sales | Hiring signals: which companies opened engineering, sales or finance roles this week, by country |
| Investors | Headcount velocity per portfolio company or target, tracked from their own career sites |
| Career coaches | Remote roles for a skill, posted in the last 3 days, with full descriptions for tailoring applications |
| HR analytics | Time to fill and posting patterns per platform, department and country |
| Newsletter authors | A weekly digest of new remote jobs in one function, generated from a single run |

### 📥 Input parameters

#### Search

| Field | Type | Default | What it does |
|---|---|---|---|
| `keywords` | array of strings | none | Words matched against the job title and department. Any one keyword matching is enough. |
| `location` | string | none | City, region or country. A country name also matches its country code. |
| `remoteOnly` | boolean | `false` | Keep only jobs the employer flagged as remote. |
| `companies` | array of strings | none | Restrict the search to these company names as they appear on their career site. |
| `postedSince` | `24h`, `3d`, `7d`, `30d`, `any` | `30d` | Only jobs published within this window. `any` returns every open job. |
| `platforms` | array of `greenhouse`, `lever`, `ashby`, `smartrecruiters`, `recruitee` | all five | Restrict the search to these systems. |

#### Output control

| Field | Type | Default | What it does |
|---|---|---|---|
| `maxItems` | integer | `100` | Maximum number of jobs to return, newest first. |
| `includeDescription` | boolean | `true` | Fetch the full description of every returned job. Turn off for faster, lighter runs. |
| `debugMode` | boolean | `false` | Verbose diagnostics in the log. |

Leave every field empty to get the freshest jobs across all indexed companies.

### 📊 Output data

Every job is one row. With `includeDescription` on:

```json
{
  "platform": "greenhouse",
  "slug": "stripe",
  "platformId": "7954688",
  "title": "Engineering Manager, Abuse Control Engineering",
  "company": "Stripe",
  "location": "Seattle, SF, NYC, Remote in the US",
  "countryCode": "US",
  "isRemote": true,
  "department": "Engineering",
  "postedDate": "2026-09-02T17:12:35Z",
  "url": "https://boards.greenhouse.io/stripe/jobs/7954688",
  "description": "Who we are. About Stripe. Stripe is a financial infrastructure platform for businesses...",
  "scrapedAt": "2026-09-16T16:23:49Z",
  "dataType": "job"
}
```

With `includeDescription` off, the same row without the `description` field:

```json
{
  "platform": "lever",
  "slug": "spotify",
  "platformId": "3c2b0a55-1e2f-4d7a-9c11-2b5a0c1d9e77",
  "title": "Backend Engineer, Personalization",
  "company": "spotify",
  "location": "Stockholm",
  "countryCode": "SE",
  "isRemote": false,
  "department": "Engineering",
  "postedDate": "2026-09-10T08:41:12Z",
  "url": "https://jobs.lever.co/spotify/3c2b0a55-1e2f-4d7a-9c11-2b5a0c1d9e77",
  "scrapedAt": "2026-09-16T16:23:49Z",
  "dataType": "job"
}
```

### 🗂️ Data fields

14 fields per job. `description` is the only one that can be missing, when the career site does not serve it at run time.

| Category | Field | Type | Notes |
|---|---|---|---|
| Identity | `platform` | string | `greenhouse`, `lever`, `ashby`, `smartrecruiters` or `recruitee` |
| Identity | `slug` | string | The company's identifier on its hiring system |
| Identity | `platformId` | string | The employer's own job ID, stable across runs |
| Job | `title` | string | Job title as published |
| Job | `company` | string | Company name as shown on the career site |
| Job | `department` | string | Department or team, when the employer sets one |
| Job | `description` | string | Full description as plain text, fetched live |
| Place | `location` | string | Location text as published |
| Place | `countryCode` | string | ISO 3166 two letter code, `other` when the employer gives none |
| Place | `isRemote` | boolean | The employer's own remote flag |
| Time | `postedDate` | string | RFC 3339 timestamp of the publication |
| Time | `scrapedAt` | string | RFC 3339 timestamp of the run |
| Links | `url` | string | Permanent job page on the career site |
| Meta | `dataType` | string | Always `job` |

### 🚀 Examples

#### Get the newest jobs everywhere

```json
{
  "maxItems": 100
}
```

#### Find remote Go jobs posted this week

```json
{
  "keywords": ["golang", "go engineer"],
  "remoteOnly": true,
  "postedSince": "7d",
  "maxItems": 200
}
```

#### Watch every opening at a list of companies

```json
{
  "companies": ["stripe", "ramp", "spotify"],
  "postedSince": "any",
  "includeDescription": false,
  "maxItems": 1000
}
```

#### Sales roles in the United Kingdom, Greenhouse and Lever only

```json
{
  "keywords": ["account executive", "sales development"],
  "location": "United Kingdom",
  "platforms": ["greenhouse", "lever"],
  "postedSince": "30d",
  "maxItems": 300
}
```

#### Yesterday's engineering jobs, titles only, for a daily digest

```json
{
  "keywords": ["engineer", "developer"],
  "postedSince": "24h",
  "includeDescription": false,
  "maxItems": 500
}
```

#### Data roles in Germany, remote or on site, with full descriptions

```json
{
  "keywords": ["data engineer", "data scientist", "analytics"],
  "location": "Germany",
  "postedSince": "3d",
  "includeDescription": true,
  "maxItems": 150
}
```

### 🤖 Copy to your AI assistant

Paste this block into Claude, ChatGPT or Cursor to give it full context about this scraper:

```
You have access to the ATS Jobs Scraper on Apify: silentflow/ats-jobs-scraper

Input schema:
- keywords (array of strings): words matched against job title and department, any match is enough
- location (string): city, region or country
- remoteOnly (boolean, default false)
- companies (array of strings): restrict to these company slugs
- postedSince (string, one of 24h | 3d | 7d | 30d | any, default 30d)
- platforms (array of strings, values greenhouse | lever | ashby | smartrecruiters | recruitee, default all)
- maxItems (integer, default 100): newest jobs first
- includeDescription (boolean, default true): fetch the full description live
- debugMode (boolean, default false)

Output per job (14 fields):
- platform, slug, platformId (strings, stable identity of the job on its hiring system)
- title, company, department, description (strings; description may be absent)
- location (string), countryCode (ISO 3166 code or "other"), isRemote (boolean)
- postedDate, scrapedAt (RFC 3339 strings)
- url (permanent job page), dataType ("job")

No login or API key is needed. Use apify-client for Python or JavaScript.
```

### 💻 Integrations

#### Feed a job board with fresh remote roles (Python)

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("silentflow/ats-jobs-scraper").call(run_input={
    "keywords": ["frontend", "react"],
    "remoteOnly": True,
    "postedSince": "3d",
    "maxItems": 300,
})
jobs = list(client.dataset(run["defaultDatasetId"]).iterate_items())

## keep one row per employer job ID, newest first, and hand it to your board
seen, fresh = set(), []
for job in sorted(jobs, key=lambda j: j["postedDate"], reverse=True):
    key = (job["platform"], job["platformId"])
    if key not in seen:
        seen.add(key)
        fresh.append({"title": job["title"], "company": job["company"], "url": job["url"], "posted": job["postedDate"]})
print(f"{len(fresh)} new remote frontend jobs")
```

#### Turn openings into hiring signals for sales (JavaScript)

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const { defaultDatasetId } = await client.actor('silentflow/ats-jobs-scraper').call({
    keywords: ['sales', 'account executive', 'revenue'],
    postedSince: '7d',
    includeDescription: false,
    maxItems: 1000,
});
const { items } = await client.dataset(defaultDatasetId).listItems();

// companies that opened three or more sales roles this week, by country
const counts = {};
for (const job of items) {
    const key = `${job.company} (${job.countryCode})`;
    counts[key] = (counts[key] || 0) + 1;
}
const hot = Object.entries(counts).filter(([, n]) => n >= 3).sort((a, b) => b[1] - a[1]);
console.log(hot.slice(0, 20));
```

#### Export a company watchlist to CSV every morning (Apify CLI)

```bash
apify call silentflow/ats-jobs-scraper --input '{"companies":["stripe","ramp"],"postedSince":"24h","includeDescription":false,"maxItems":500}' --wait
apify datasets get-items --format csv > openings-$(date +%F).csv
```

Schedule the same input from the Apify Console and receive the CSV by webhook or in your bucket.

### 📈 Performance

| Metric | Value |
|---|---|
| Time to first results | 2 to 3 seconds after the run starts |
| Descriptions | fetched in parallel, most runs finish in under a minute for a few hundred jobs |
| Description coverage | 22 of 25 jobs in the reference run |
| Freshness | index rebuilt daily, descriptions live at run time |
| Results per run | no fixed limit, `maxItems` is yours to set |

### 💾 Data export

Every run's dataset is available as JSON, CSV, Excel, XML, HTML table or RSS from the run's dataset page, or straight from the API:

```
https://api.apify.com/v2/datasets/{DATASET_ID}/items?format=csv&token=YOUR_TOKEN
```

Swap `csv` for `json`, `xlsx` or `xml`. The same URL works from Google Sheets, Make, Zapier or a cron job.

### 💡 Tips for best results

1. **Combine a keyword with a location or a date window.** `engineer` alone matches hundreds of jobs; `engineer` plus `Netherlands` plus `7d` gives you a list you can act on.
2. **Turn off `includeDescription` for monitoring runs.** Titles, companies and dates are enough to detect new openings; fetch descriptions only when you need the text.
3. **Deduplicate on `platform` plus `platformId`.** The pair is the employer's own identity for the job and does not change between runs.
4. **Use `postedSince: "any"` for a full snapshot of a company.** The default 30 day window is meant for fresh jobs, not for a complete catalogue.
5. **Match `companies` to the slug on the career site.** It is the identifier in the career page URL, for example `stripe` in `boards.greenhouse.io/stripe`.

### ❓ FAQ

**What exactly does this scraper return?**
Jobs published by employers on their own career sites through Greenhouse, Lever, Ashby, SmartRecruiters or Recruitee, with 14 fields per job including the full description.

**Do I need an account, a key or a login?**
No. Career site listings are public. You give an input and get rows.

**How fresh is the data?**
The index of jobs is rebuilt every day. Descriptions are fetched live from the career site during your run, so the text you get is the text currently online.

**Which companies are covered?**
Companies that publish their jobs through one of the five supported systems and are part of the index. Coverage grows as new career sites are added. To check a specific company, run a search with `companies` set to its slug and `postedSince` set to `any`.

**Can I search several keywords at once?**
Yes. `keywords` is a list and any one match is enough. Add `location`, `platforms` or `remoteOnly` to narrow the result.

**How do filters combine?**
All filters apply together. `keywords: ["engineer"]` plus `location: "Germany"` plus `postedSince: "3d"` plus `remoteOnly: true` returns remote engineering jobs in Germany published in the last three days.

**Why is `description` missing on some jobs?**
A small share of career pages do not serve the description at run time. The job row still ships with its title, company, location, date and URL.

**Is the `url` permanent?**
Yes. It is the job page on the company's career site, which stays valid as long as the job is open. `platformId` stays the same across runs.

**Why does `countryCode` say `other`?**
The employer did not give a recognizable country in the location text. The `location` field still has what they wrote.

**What about Workday, Taleo or SAP SuccessFactors?**
Not covered yet. The scraper focuses on the five systems above, which share the same public structure.

**Is this legal?**
The scraper reads job listings that employers publish for the public. See the Legal section below for your obligations.

### ⚖️ Legal

This Actor extracts publicly available job listings from company career sites. It does not bypass any login, paywall or CAPTCHA. Users are responsible for complying with the terms of service of the sites they collect from and with applicable data protection laws (GDPR, CCPA, and PIPL where relevant). Job listings are company data; they do not contain candidate profiles or personal data. The data returned is informational; verify accuracy for regulated use cases.

### 🔗 Related scrapers

- [LinkedIn Jobs Scraper](https://apify.com/silentflow/linkedin-jobs-scraper-ppr) for jobs posted on LinkedIn.
- [Indeed Jobs Scraper](https://apify.com/silentflow/indeed-jobs-scraper-ppr) for Indeed listings by keyword and location.
- [Glassdoor Jobs Scraper](https://apify.com/silentflow/glassdoor-jobs-scraper-ppr) for Glassdoor jobs with company ratings.
- [All Jobs Scraper](https://apify.com/silentflow/all-jobs-scraper-ppe) for one search across the big job boards.
- [Welcome to the Jungle Scraper](https://apify.com/silentflow/welcome-to-the-jungle-scraper-ppe) for French and European startup jobs.

### 📬 Support

Need something this scraper doesn't do yet? We ship features fast.

- Feature requests go straight to our backlog
- Enterprise needs? We do custom integrations and high-volume plans
- Pricing questions? Check the Monetization tab on the actor page

Response time: usually under 24 hours.

Check out our other scrapers: [silentflow on Apify](https://apify.com/silentflow)

# Actor input Schema

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

Words to find in the job title or department, for example <code>golang</code>, <code>backend engineer</code>, <code>account executive</code>. Any one keyword matching is enough. A realistic keyword returns a few hundred jobs; combine it with a location or a date window to narrow down.

## `location` (type: `string`):

City, region or country: <code>Berlin</code>, <code>California</code>, <code>Netherlands</code>. A country name also matches jobs tagged with its country code. Leave empty for jobs everywhere.

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

Keep only jobs the employer flagged as remote.

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

Restrict the search to these company names, as they appear on their career site (for example <code>stripe</code>, <code>spotify</code>). Leave empty to search every indexed company.

## `postedSince` (type: `string`):

Only return jobs published within this window. <code>any</code> returns every job currently open on the career sites.

## `platforms` (type: `array`):

Restrict the search to these systems. Leave empty to search all five.

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

Maximum number of jobs to return. Newest jobs come first. Set it to what you actually need: each returned job is one dataset row.

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

Fetch the complete job description for every returned job, live from the career site. A small share of descriptions can be unavailable at run time; the job row still ships without it. Turn off for faster, lighter runs.

## `debugMode` (type: `boolean`):

Print verbose diagnostics to the log. Leave off in normal use.

## Actor input object example

```json
{
  "keywords": [
    "software engineer",
    "backend engineer",
    "data engineer"
  ],
  "remoteOnly": false,
  "postedSince": "30d",
  "maxItems": 100,
  "includeDescription": true,
  "debugMode": false
}
```

# Actor output Schema

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

Complete job data including: title, company, platform, location, countryCode, isRemote, department, employmentType, postedDate, url, applyUrl, description, scrapedAt.

# 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",
        "backend engineer",
        "data engineer"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("silentflow/ats-jobs-scraper").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",
        "backend engineer",
        "data engineer",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("silentflow/ats-jobs-scraper").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",
    "backend engineer",
    "data engineer"
  ]
}' |
apify call silentflow/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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

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/iMsJMUzpktqLbm4OV/builds/guMUYSbkd8MklYjgc/openapi.json
