# Greenhouse Jobs Scraper: Lever & Ashby Job Feed (`leadsbrary/ats-jobs-watch-feed`) Actor

Greenhouse jobs scraper for Lever and Ashby too: get jobs by company name, domain or careers URL with title, location, remote flag, salary and apply link as JSON, CSV or Excel. Watch mode returns new, changed and closed jobs. From $1/1,000 results.

- **URL**: https://apify.com/leadsbrary/ats-jobs-watch-feed.md
- **Developed by:** [Alexandre Manguis](https://apify.com/leadsbrary) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
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

## Greenhouse Jobs Scraper: Lever & Ashby Job Feed with New/Closed Alerts

### What does ATS Jobs Watch Feed do?

This Greenhouse jobs scraper also reads **Lever** and **Ashby** career boards. Give it company names, domains, career-page URLs or board tokens. It finds each company's job board and returns every open posting in one normalized format: title, department, locations, remote flag, salary (when published), description and direct apply URL.

In **watch mode** it remembers the previous run and returns only the **new, changed and closed** postings, so a daily schedule stays small and cheap.

It reads the public job-board APIs that Greenhouse, Lever and Ashby publish for third parties. No login, no browser and no proxy is involved.

### Use cases

- **Sales and RevOps teams**: use hiring as a buying signal. Which target accounts are hiring a "data engineer" right now?
- **Recruiting agencies**: track competitors' and clients' open roles.
- **Job-board and newsletter builders**: aggregate postings with apply links and normalized salary and remote fields.
- **Competitive intelligence**: spot roles that were closed (filled or frozen).
- **Automations and AI agents**: structured job data per company, ready for Slack, Google Sheets, Make, Zapier or a CRM.

### Features

- Three ATSs in one schema: Greenhouse, Lever, Ashby.
- Input by **company name, domain, careers URL, board URL or board token**. The ATS and token are detected automatically (best effort, see Limitations).
- **Watch mode**: NEW / CHANGED / CLOSED rows since the last run, one snapshot per watch list name. Only the rows returned are billed.
- Filters: title keywords, location keywords, remote only, cap per company, overall result limit.
- Normalized salary (min, max, currency, period) when the ATS publishes it, remote flag, workplace type, country and seniority inferred from the title.
- Optional plain-text and HTML descriptions (turn off for lighter rows).
- Export to JSON, CSV, Excel, XML or HTML from the Apify dataset, or call it through the API.

### Input and configuration

#### Input fields

| Field | Type | Default | Description |
|---|---|---|---|
| `companies` (required) | array of strings | – | Board URLs (`jobs.lever.co/palantir`), board tokens (`stripe`), domains (`stripe.com`), careers page URLs or company names. |
| `ats` | `auto` / `greenhouse` / `lever` / `ashby` | `auto` | Force one ATS for bare tokens and names. |
| `mode` | `all` / `watch` | `all` | `all` returns every open posting. `watch` returns only NEW, CHANGED and CLOSED postings since the previous run with the same watch list name. The first watch run returns everything as NEW. |
| `watchStateName` | string | `default` | Name of the watch list (a-z, 0-9, `-`). Each name keeps its own snapshot, so several schedules run independently. |
| `titleKeywords` | array of strings | `[]` | Keep jobs whose title contains any of these words (case-insensitive). |
| `locationKeywords` | array of strings | `[]` | Keep jobs whose location text contains any of these words. |
| `remoteOnly` | boolean | `false` | Keep only jobs flagged as remote. |
| `includeDescription` | boolean | `true` | Include `descriptionText` and `descriptionHtml`. |
| `maxJobsPerCompany` | integer | `0` | Cap per company (0 = no cap). |
| `maxResults` | integer | `100` | Maximum rows for the whole run (also caps your spending). |

#### Example input

```json
{
  "companies": ["stripe", "jobs.lever.co/palantir", "jobs.ashbyhq.com/linear"],
  "mode": "all",
  "titleKeywords": ["engineer"],
  "maxResults": 100
}
```

### Output

#### Example output

One dataset row per job. Short example (description shortened, values illustrative):

```json
{
  "jobId": "greenhouse:stripe:8172508",
  "ats": "greenhouse",
  "boardToken": "stripe",
  "companyInput": "stripe.com",
  "companyName": "Stripe",
  "title": "Abuse Investigator",
  "department": "8611 Security Analytics",
  "locations": ["Dublin"],
  "country": null,
  "isRemote": false,
  "workplaceType": null,
  "employmentType": null,
  "seniority": null,
  "salaryMin": null,
  "salaryMax": null,
  "salaryCurrency": null,
  "salaryPeriod": null,
  "salaryText": null,
  "descriptionText": "About the team and the role …",
  "descriptionHtml": "<h2>About the team</h2> …",
  "jobUrl": "https://stripe.com/jobs/search?gh_jid=8172508",
  "applyUrl": "https://stripe.com/jobs/search?gh_jid=8172508#app",
  "publishedAt": "2026-09-03T17:32:53.000Z",
  "updatedAt": "2026-09-25T20:45:00.000Z",
  "changeType": null,
  "changedFields": null,
  "firstSeenAt": null,
  "closedAt": null,
  "scrapedAt": "2026-10-04T10:21:10.285Z"
}
```

#### Watch mode rows

In watch mode `changeType` is `NEW`, `CHANGED` or `CLOSED`, `firstSeenAt` is set, and CHANGED rows list the changed fields in `changedFields`.

#### Output fields and fill rates

Fill rates measured on a 60-row test run across several boards (all three ATSs, `mode: all`). They depend on what each company publishes, so your own results will vary.

| Field | Fill rate | Notes |
|---|---|---|
| `jobId`, `ats`, `boardToken`, `companyInput`, `title`, `jobUrl`, `applyUrl`, `scrapedAt` | 100% | Always present. |
| `department`, `locations`, `isRemote`, `publishedAt` | 100% | |
| `descriptionText`, `descriptionHtml` | 100% | `null` when `includeDescription` is off. |
| `workplaceType` | 62% | When the ATS publishes it. |
| `country` | 68% | Only when it can be inferred reliably. |
| `employmentType` | 60% | |
| `companyName` | 40% | Only when the board publishes it. |
| `updatedAt` | 40% | Not exposed by every ATS. |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod`, `salaryText` | 47% | Only when the posting publishes pay. In a Greenhouse-only discovery run (15 rows) none did. No guessing. |
| `seniority` | 15% | Inferred from the title; `null` when unclear. |
| `changeType`, `firstSeenAt` | watch mode only | 100% in a watch test run, `null` in `all` mode. |
| `changedFields`, `closedAt` | watch mode only | Only on CHANGED / CLOSED rows. |

### How to use via the API

Replace `YOUR_APIFY_TOKEN` with your own token.

**curl**

```bash
curl -X POST "https://api.apify.com/v2/acts/YOUR_USERNAME~ats-jobs-watch-feed/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"companies": ["stripe", "jobs.lever.co/palantir"], "maxResults": 50}'
```

**JavaScript**

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

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('YOUR_USERNAME/ats-jobs-watch-feed').call({
    companies: ['stripe', 'jobs.lever.co/palantir'],
    mode: 'watch',
    watchStateName: 'sales-targets',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.length, 'new, changed or closed jobs');
```

**Python**

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("YOUR_USERNAME/ats-jobs-watch-feed").call(run_input={
    "companies": ["stripe", "jobs.lever.co/palantir"],
    "mode": "watch",
    "watchStateName": "sales-targets",
    "remoteOnly": False,
})
items = client.dataset(run["defaultDatasetId"]).list_items().items
print(len(items), "new, changed or closed jobs")
```

#### Scheduling a "new jobs" feed in watch mode

1. Create a Task with `mode: watch` and a fixed `watchStateName`.
2. Add an Apify Schedule (for example daily).
3. Add a webhook or an integration (Slack, Google Sheets, Make, Zapier) on run success. Each run's dataset holds only what changed since the last run.

Use a different `watchStateName` for each list of companies you want to track separately.

### Pricing

Pay per result: you are billed one `job` event per dataset row returned. Unresolved companies and runs that return nothing cost no result events. Apify platform usage (a few hundredths of a cent per test run) is shown separately in your run.

| Apify plan | Price per 1,000 results |
|---|---|
| Free | $1.50 |
| Bronze | $1.30 |
| Silver | $1.15 |
| Gold, Platinum, Diamond | $1.00 |

In watch mode only NEW, CHANGED and CLOSED rows are returned and billed, so unchanged jobs cost nothing. Use `maxResults` to cap spending.

### Limitations and troubleshooting

- **Only Greenhouse, Lever and Ashby** are supported. Other systems (Workday, SmartRecruiters, …) are not.
- **A company is not found.** Name and domain discovery is best effort: it needs a recognisable board token or a careers page that links to the ATS board. Try the board URL or token directly. Unresolved entries are listed with a reason in the `RUN_SUMMARY` key-value record, and the run still succeeds without billing them. In a test with an unknown board and `example.com`, the run succeeded with 0 results.
- **Missing salary or `null` fields.** Fields are filled only when the posting publishes them. The Actor does not guess.
- **"Field input.companies is required".** Provide at least one company.
- **Watch mode returned everything.** The first run of a watch list returns all jobs as NEW. If the snapshot is missing or corrupted, the run is treated as a first run.
- **Rate limits.** Requests are paced and retried with backoff.

### FAQ

**Does it need a board token?** No. You can pass a name, domain or careers URL, though a board URL or token is the most reliable.

**Is the data public?** Yes. It comes from the public job-board APIs these ATS vendors offer, with no login. Descriptions are kept as the employer published them. Check your own legal basis if you process recruiter contact details found in a description.

**Can it apply to jobs or read candidate data?** No. It only reads public postings.

**Does watch mode work across different Tasks?** Snapshots are stored per `watchStateName`, in your account. Reuse the same name to continue a list.

### Keywords

greenhouse jobs scraper, lever jobs api, ashby jobs scraper, ats job feed, career page scraper, greenhouse job board api, lever jobs scraper, new job alerts, hiring signals, company job postings, job change monitor, career site jobs api

### Hashtags

\#greenhouse #lever #ashby #jobs #jobscraper #ats #careerpages #hiringsignals #recruiting #leadgeneration #jobalerts

# Changelog

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

# Actor input Schema

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

Board URLs (boards.greenhouse.io/stripe, jobs.lever.co/palantir, jobs.ashbyhq.com/linear), bare board tokens, company domains (stripe.com), careers page URLs or company names. The ATS and token are detected automatically.

## `ats` (type: `string`):

Force one ATS for bare tokens and names, or let the Actor detect it.

## `mode` (type: `string`):

'all' returns every open posting. 'watch' returns only NEW, CHANGED and CLOSED postings since the previous run with the same watch name (the first watch run returns everything as NEW).

## `watchStateName` (type: `string`):

Each name keeps its own snapshot, so several schedules run independently. Only a-z, 0-9 and '-'.

## `titleKeywords` (type: `array`):

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

## `locationKeywords` (type: `array`):

Keep only jobs whose location text contains any of these words (case-insensitive).

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

Keep only jobs flagged as remote.

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

Include plain-text and HTML job descriptions. Turn off for lighter rows.

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

Cap per company (0 = no cap).

## `maxResults` (type: `integer`):

Stop after this many rows in total (also protects your spending).

## Actor input object example

```json
{
  "companies": [
    "stripe",
    "jobs.lever.co/palantir",
    "jobs.ashbyhq.com/linear"
  ],
  "ats": "auto",
  "mode": "all",
  "watchStateName": "default",
  "titleKeywords": [],
  "locationKeywords": [],
  "remoteOnly": false,
  "includeDescription": true,
  "maxJobsPerCompany": 0,
  "maxResults": 30
}
```

# Actor output Schema

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

No description

# 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": [
        "stripe",
        "jobs.lever.co/palantir",
        "jobs.ashbyhq.com/linear"
    ],
    "watchStateName": "default",
    "maxResults": 30
};

// Run the Actor and wait for it to finish
const run = await client.actor("leadsbrary/ats-jobs-watch-feed").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": [
        "stripe",
        "jobs.lever.co/palantir",
        "jobs.ashbyhq.com/linear",
    ],
    "watchStateName": "default",
    "maxResults": 30,
}

# Run the Actor and wait for it to finish
run = client.actor("leadsbrary/ats-jobs-watch-feed").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": [
    "stripe",
    "jobs.lever.co/palantir",
    "jobs.ashbyhq.com/linear"
  ],
  "watchStateName": "default",
  "maxResults": 30
}' |
apify call leadsbrary/ats-jobs-watch-feed --silent --output-dataset

```

## MCP server setup

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

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/0UqaMjcTAlnitXe4g/builds/wAgZkHRHi19J6DDtX/openapi.json
