# ATS Job Scraper: Greenhouse, Lever, Ashby, SmartRecruiters (`seotrader/ats-job-scraper-greenhouse-lever-ashby-smartrecruiters`) Actor

Open jobs from company career sites on Greenhouse, Ashby, Lever, SmartRecruiters and Personio. Returns only jobs added since your last run when you want a diff.

- **URL**: https://apify.com/seotrader/ats-job-scraper-greenhouse-lever-ashby-smartrecruiters.md
- **Developed by:** [FAST SEO LIMITED](https://apify.com/seotrader) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 job listeds

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 Job Feed

Read open jobs straight from the career sites of the companies you care about,
and get one clean table back. Greenhouse, Ashby, Lever, SmartRecruiters and
Personio all return different shapes. This returns one.

Every run reads the live job board at the moment you ask. There is no index in
between, so nothing is stale and nothing is missing because a crawler has not
caught up yet.

### Three things it does well

- **You choose the companies.** Give it a list of employers and it reads only
  those boards. No searching a database of millions of postings to find the
  fifty companies you actually track.
- **It tells you only what is new.** Switch on the incremental feed and it
  remembers the exact job IDs it has already given you, so the next run returns
  genuinely new postings and nothing else.
- **One schema from every platform.** Greenhouse calls it `title`, Lever calls
  it `text`, SmartRecruiters calls it `name`. You get `title` from all of them.

### Supported platforms

| Platform | Status |
| --- | --- |
| Greenhouse | Verified against live boards |
| Ashby | Verified against live boards |
| Lever | Verified against live boards |
| SmartRecruiters | Verified against live boards |
| Personio | Verified against live boards |
| Workable | Implemented, awaiting a live board to confirm |
| Recruitee | Implemented, awaiting a live board to confirm |

These are each platform's own public endpoints. No login, no cookies, no
proxies, nothing that breaks the first time a site changes its markup.

### Quick start

Paste any of these into the input and run it.

**Every open job at three companies**

```json
{ "companies": ["greenhouse:stripe", "ashby:ramp", "lever:gopuff"] }
```

**Remote engineering roles only, posted in the last month**

```json
{
  "companies": ["greenhouse:stripe", "ashby:ramp"],
  "titleIncludes": ["engineer", "developer"],
  "remoteOnly": true,
  "postedAfter": "30 days"
}
```

**A daily alert feed that never repeats itself**

```json
{
  "companies": ["greenhouse:monzo", "lever:gopuff"],
  "newSinceLastRun": true,
  "includeDescription": false
}
```

Put that last one on a daily schedule and each run returns only the jobs that
appeared since the previous run.

### Naming a company

Three accepted forms, and you can mix them in one run.

| You type | What happens |
| --- | --- |
| `greenhouse:stripe` | Used directly. Fastest, and never ambiguous. |
| `https://jobs.ashbyhq.com/ramp` | The token is read out of the URL. |
| `monzo.com` | The careers page is fetched and the board found automatically. |

Auto-detection follows links from the company's site to find its job board. It
works on most sites. Some marketing sites block automated requests, and when
that happens the run names the entry and tells you to pass its board token
directly instead of silently returning nothing.

### Only what is new

This is the part worth understanding, because it is not the same as filtering
by date.

A date filter asks "what was posted in the last 24 hours". If a run fails, or a
job is posted twice, or a board backdates a posting, you get duplicates or
gaps. Shortening the window loses jobs and widening it repeats them.

The incremental feed instead remembers the exact job IDs it has already handed
you. The next run compares against that list and returns the difference. No
window to tune, no duplicates, and nothing missed because a run was late.

Three details that matter in practice:

- History is keyed to your company list, so two schedules watching different
  companies never interfere. Set **State key** yourself to override that.
- Jobs are recorded as seen only after they have actually been delivered, so a
  run that stops at a spend limit will not hide those postings from the next one.
- Your history lives in your own account. It is yours, and nobody else's runs
  can touch it.

### Filters

Every filter is optional, and leaving one empty keeps everything.

| Filter | Behaviour |
| --- | --- |
| `titleIncludes` | Keep a job if its title contains any of these. Case-insensitive. |
| `titleExcludes` | Drop a job if its title contains any of these. |
| `locationIncludes` | Match against the job's location text. |
| `remoteOnly` | Uses the board's own remote flag where it has one, and the location text where it does not. |
| `postedAfter` | A date, or a relative value such as `7 days`. |

Filters run before the per-company cap and before descriptions are fetched. So
asking for five software roles at a company with 4,836 openings searches the
whole board, returns five, and fetches five descriptions rather than 4,836.

### Output

One row per job, identical in shape whichever platform it came from.

| Field | Notes |
| --- | --- |
| `jobId`, `source`, `companyToken`, `companyName` | `source` names the platform the job came from |
| `title`, `department`, `team`, `employmentType` | |
| `location`, `locations`, `isRemote`, `workplaceType` | `locations` lists every location when a job has several |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryText` | Parsed into numbers you can sort on, including European formats |
| `postedAt`, `updatedAt` | Always UTC ISO 8601, whatever the board sent |
| `jobUrl`, `applyUrl` | |
| `descriptionHtml`, `descriptionText` | Both provided. Turn them off to make large boards much faster |
| `scrapedAt` | |

Fields a platform does not publish come back as `null` rather than being left
out, so the shape never changes between rows.

```json
{
  "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "source": "ashby",
  "companyToken": "ramp",
  "companyName": "Ramp",
  "title": "Security Engineer, Cloud",
  "department": "Engineering",
  "team": "Backend",
  "employmentType": "FullTime",
  "location": "New York, NY (HQ)",
  "locations": ["New York, NY (HQ)", "Remote (US)", "Miami, FL"],
  "isRemote": true,
  "workplaceType": "Hybrid",
  "salaryText": "$211.4K - $290.6K",
  "salaryMin": 211400,
  "salaryMax": 290600,
  "salaryCurrency": "USD",
  "postedAt": "2026-04-07T17:12:35.753Z",
  "jobUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application"
}
```

### What people use it for

- **Niche job boards** that list roles from a curated set of employers and need
  the board to stay current without manual work.
- **Recruiters and sourcers** watching a target account list for the moment a
  relevant role opens.
- **Sales teams** treating a new engineering hire as a buying signal, since a
  company staffing up a data team is a company about to buy data tooling.
- **AI agents** that need live hiring data rather than a model's stale memory.
- **Market and salary research**, using the parsed salary fields across a
  defined peer group.

### How this compares

Being straight with you, because it will save you time.

If you need to search across every company at once, or you want millions of
postings from 150 platforms, the large aggregators are a better fit. They run
enormous indexes and this does not try to.

This is the right tool when you know which employers matter. It reads their
boards live rather than serving you a cached copy, you pay only for the jobs at
the companies you asked for rather than querying a vast database, and the
incremental feed gives exact-once delivery rather than a time window you have
to tune.

### Calling it from your own code

Small runs can use the synchronous endpoint, which returns the rows directly in
the response body.

```bash
curl -X POST "https://api.apify.com/v2/acts/seotrader~ats-job-scraper-greenhouse-lever-ashby-smartrecruiters/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
        "companies": ["greenhouse:stripe", "ashby:ramp"],
        "titleIncludes": ["engineer"],
        "includeDescription": false
      }'
```

That endpoint gives up after 300 seconds, so for long company lists start the
run asynchronously and read the dataset when it finishes.

### Frequently asked questions

**How current is the data?** It is read from the career site during your run.
There is no index and no refresh delay.

**Does it find jobs that are not on a company's board?** No. It reads the
board, so you get what the employer has actually published, with nothing
inferred and nothing duplicated from an aggregator.

**What if a company changes platform?** Supply the company as a bare domain and
detection finds the new board on the next run.

**Why did a small max-jobs value return nothing?** It should not. Filters run
before the cap, so a cap of five returns five matches if five exist. If you see
otherwise, that is a bug worth reporting.

**Can I get only new jobs without duplicates?** Yes, that is what
`newSinceLastRun` does, and it is tracked by job ID rather than by a date
window.

**Which platforms are coming next?** Workable and Recruitee are written and
waiting on a live board to verify against. Tell me which company you need and
it moves to the front.

### Speed and cost

- Turning descriptions off is the single biggest speed win, and on
  SmartRecruiters it removes one request per job.
- **Max jobs per company** counts matching jobs, not jobs looked at, so a small
  cap with a narrow filter still searches the whole board.
- **Max concurrent requests** applies per platform rather than globally, so one
  slow career site cannot hold up the others. Lower it if you see rate limiting.

# Actor input Schema

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

One per line. Accepts a vendor-prefixed token (greenhouse:stripe), a job board URL (https://jobs.ashbyhq.com/ramp), or a bare company domain (stripe.com) which is auto-detected.

## `titleIncludes` (type: `array`):

Case-insensitive. A job is kept if its title contains any of these. Leave empty for all titles.

## `titleExcludes` (type: `array`):

Case-insensitive. Jobs matching any of these are dropped.

## `locationIncludes` (type: `array`):

Case-insensitive match against the job location.

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

Keep only jobs the career site marks as remote. Greenhouse does not expose a remote flag, so its jobs are matched on the location text instead.

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

Keep jobs published on or after this date. Accepts a date or a relative value such as 7 days.

## `newSinceLastRun` (type: `boolean`):

Remembers the job IDs it has already returned and emits only jobs that appeared since. State is kept per company list, so schedules with different inputs do not collide.

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

Optional. Override the automatic key used to store seen job IDs. Set this when you want two schedules to share, or not share, the same history.

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

Fetches the job description. On SmartRecruiters this costs one extra request per job, so turn it off for large boards.

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

Stop after this many jobs from any single company. 0 means no limit.

## `maxConcurrency` (type: `integer`):

Lower this if you see rate limiting. Applies per vendor, not globally.

## Actor input object example

```json
{
  "companies": [
    "greenhouse:stripe",
    "ashby:ramp",
    "lever:gopuff"
  ],
  "titleIncludes": [
    "engineer"
  ],
  "remoteOnly": false,
  "newSinceLastRun": false,
  "includeDescription": true,
  "maxJobsPerCompany": 0,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

All matching jobs as JSON. One object per job with title, company, location, remote flag, parsed salary, posting date, apply URL and description.

## `jobsCsv` (type: `string`):

The same jobs as a CSV file, for loading straight into a spreadsheet or a database.

## `jobsOverview` (type: `string`):

A trimmed view with just the fields most people scan first: title, company, location, salary, posting date and link.

# 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": [
        "greenhouse:stripe",
        "ashby:ramp",
        "lever:gopuff"
    ],
    "titleIncludes": [
        "engineer"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("seotrader/ats-job-scraper-greenhouse-lever-ashby-smartrecruiters").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": [
        "greenhouse:stripe",
        "ashby:ramp",
        "lever:gopuff",
    ],
    "titleIncludes": ["engineer"],
}

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,seotrader/ats-job-scraper-greenhouse-lever-ashby-smartrecruiters"
        }
    }
}
```

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/oFdwkauJGrr5cujkv/builds/a9W10moB9kEiPJP62/openapi.json
