# Greenhouse, Lever & Ashby Jobs Feed: $1/1K (`moo_iscool/ats-job-feed`) Actor

Paste careers page URLs, slugs or company names and get open jobs from Greenhouse, Lever and Ashby in one clean schema, with salary when published, remote flag and apply link. Monitor mode returns only new jobs; seen jobs are free. A bad URL never fails the run. $1 per 1,000 jobs.

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

## Pricing

from $1.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

## Greenhouse, Lever & Ashby Jobs Feed

**Paste careers pages. Get every open job in one clean feed.** This actor reads the public job boards that companies host on Greenhouse, Lever, and Ashby, and returns their open jobs in one normalized schema: title, location, remote flag, department, posted date, salary when the board publishes one, and the apply link. Export as JSON, CSV, or Excel, or send to Google Sheets or a webhook. No API key, no login, no browser.

- **Who it's for:** recruiters and talent teams watching a fixed set of employers, sales and research teams using hiring as a signal, job seekers tracking target companies, and developers who need Greenhouse, Lever, and Ashby jobs in the same fields.
- **Any input format:** a careers URL, a board slug, or a company name. A bad line becomes a free error row, and the rest of the run still finishes.
- **Monitor mode:** with only new jobs since last run, a daily schedule returns only jobs you haven't seen, and seen jobs are not charged.
- **Price:** $1.00 per 1,000 jobs, plus a small per-run start fee. Error rows are free.

#### Quick example

Input (this is the prefilled form):

```json
{
  "companies": [
    "https://job-boards.greenhouse.io/stripe",
    "jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "keywords": ["engineer"],
  "maxJobsPerCompany": 25,
  "maxResults": 100
}
```

Sample output rows (from a real run on 25 October 2026; a full record is shown further down):

| company | provider | title | location | salary |
| --- | --- | --- | --- | --- |
| ramp | ashby | Software Engineer, Forward Deployed | New York, NY (HQ) | $168K – $285K • Offers Equity |
| Stripe | greenhouse | Staff Software Engineer, Payments Intelligence | Seattle, New York, San Francisco | – |
| palantir | lever | Forward Deployed Software Engineer, New Grad - Warp Speed | New York, NY | $135,000 - $145,000 |

#### Limits, up front

- **Public Greenhouse, Lever, and Ashby boards only.** Companies on Workday, iCIMS, SmartRecruiters, or a custom careers site are not covered; such a line comes back as a free error row.
- **Live snapshot, not a database.** Each run reads the boards at that moment. There is no historical archive of closed jobs.
- **Salary only when published.** Many Greenhouse boards publish no pay range; then `salary` is read from the posting text when possible, or left null.
- `company` is the board's display name on Greenhouse and the lowercase board token (for example `ramp`) on Lever and Ashby.

### How to scrape Greenhouse jobs, plus Lever and Ashby job postings

The actor calls the official public JSON endpoints that the career pages themselves use (`boards-api.greenhouse.io`, `api.lever.co`, `api.eu.lever.co`, `api.ashbyhq.com`). A bad URL or an unknown name becomes a free error row, and the run succeeds even when every company fails.

### Get only new jobs since last run

Only-new mode remembers jobs you have already collected, which is what you want on a daily schedule. Jobs you have already seen are not saved again and are not charged. See **Tips** below for how the memory works.

### What data you get

Each open job is one dataset item with `type` set to `job`. A company that cannot be resolved or fetched is one free item with `type` set to `error`. Closed items appear only when you turn on both only new jobs and include closed jobs. A closed item has the id and the company, and the other fields are null because the board no longer returns them.

| Field | Meaning |
| --- | --- |
| type | `job` or `error` |
| jobId | Id from the source board |
| jobKey | `provider:companyToken:jobId` |
| provider | `greenhouse`, `lever`, or `ashby` |
| company | Board display name when the API sends one, otherwise the token |
| companyToken | Token used in the API URL |
| title | Job title |
| url | Public job page |
| applyUrl | Apply link. On Greenhouse this is the job page |
| location | Primary location |
| locations | Primary location plus offices or secondary locations |
| isRemote | True when the workplace type is remote, false for hybrid, on-site, in-office, and office, and when the workplace type is missing or unspecified the board's remote flag is used, then location text that contains "remote" |
| workplaceType | Workplace label when the board sends one |
| department | Department names |
| team | Team, when the board has one |
| employmentType | Lever commitment or Ashby employment type. Null for Greenhouse |
| postedAt | Greenhouse first published, with updated time as a fallback. Lever created time. Ashby published time |
| updatedAt | Greenhouse update time. Null for Lever and Ashby |
| salary | Pay range from the board when it publishes one, otherwise a range read from the description |
| requisitionId | Greenhouse requisition id, or null |
| description | Plain text. Null when descriptions are turned off, and on closed records |
| status | `open`, `changed`, or `closed` |
| source | API URL used for that company |
| scrapedAt | Time this run read the board |
| input | The company line, on error rows |
| error | `NOT_FOUND`, `INVALID_INPUT`, `HTTP_ERROR`, or `TIMEOUT` |
| errorMessage | Why that company line failed |

`salary.source` is `ats` when the number came from the board, and `description` when it was read from the posting text. Greenhouse requests pay transparency and converts `pay_input_ranges` from cents. Lever uses `salaryRange`. Ashby uses structured compensation when the board sends it, then the compensation summary, then the description. `salary.text` is the board's own line or the snippet that was parsed. `min` and `max` are numbers when they can be read. A dot used as a thousands separator, as in €60.000, is handled. Odd formats stay null rather than guessed.

The Jobs view lists postings. The Errors view lists company lines that failed.

### How to use

1. Add one company per line. A careers URL is the safest input. A slug or a company name works too.
2. Add keywords, locations, or a posted-within window if you do not want the whole board. Use posted within hours for a window such as the last 24 hours.
3. Set max results if you want a cap. The default is 1,000 jobs across all companies.
4. Run the actor. Open the dataset for the jobs, and the key-value store key `SUMMARY` for per-company counts, the board that was chosen, and errors.
5. For a daily feed, turn on only new jobs since last run, keep the same state key, and schedule the task.

Remote only keeps a job when the workplace type is remote, drops hybrid, on-site, in-office, and office jobs, and when the workplace type is missing or unspecified uses the board's remote flag, then location text that contains "remote".

#### How to find a company

Paste the careers URL as it appears in the browser. These all work, including uppercase, a missing `https://`, a trailing slash, a job id in the path, and a query string.

Greenhouse:

- `https://boards.greenhouse.io/stripe`
- `https://job-boards.greenhouse.io/stripe`
- `https://job-boards.eu.greenhouse.io/stripe`
- `https://boards.greenhouse.io/embed/job_board?for=stripe`
- `greenhouse:stripe`

Lever:

- `https://jobs.lever.co/palantir`
- `https://jobs.eu.lever.co/aavelabs` reads the EU API directly
- `lever:spotify`

Ashby:

- `https://jobs.ashbyhq.com/ramp`
- A job URL such as `https://jobs.ashbyhq.com/ramp/JOB_ID/application` still uses the board name `ramp`
- `ashby:ramp`

You can also pass a slug such as `stripe`, or a company name such as `Ramp` or `Palantir Technologies`. Auto mode builds a few slug variants, asks Greenhouse, Lever, and Ashby in parallel, and keeps the board with the most listed jobs. The run log and the `resolvedFrom` field on `SUMMARY` record the choice. `Ramp` resolves to Ashby, because that board lists far more jobs than the small Greenhouse board that uses the same slug.

If two lines resolve to the same provider and token, the board is fetched once.

Some companies put a custom domain in front of the board, such as `careers.example.com`. Search the page source for `boards-api.greenhouse.io`, `api.lever.co`, `api.eu.lever.co`, or `api.ashbyhq.com`. The token is the path segment on that request. You can paste the API URL itself.

Set Default provider when a bare slug should stay on one system. A full URL always stays on the system in that URL.

Jobs with `isListed` set to false on Ashby are skipped. They are not on the public board.

### Input example

```json
{
  "companies": [
    "https://job-boards.greenhouse.io/stripe",
    "jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "keywords": ["engineer"],
  "includeDescription": false,
  "maxJobsPerCompany": 25,
  "maxResults": 100,
  "onlyNew": false
}
```

### Output example

```json
{
  "type": "job",
  "jobId": "29a5da77-37f0-402a-92bc-072778e77ed8",
  "jobKey": "ashby:ramp:29a5da77-37f0-402a-92bc-072778e77ed8",
  "provider": "ashby",
  "company": "ramp",
  "companyToken": "ramp",
  "title": "Software Engineer, Forward Deployed",
  "url": "https://jobs.ashbyhq.com/ramp/29a5da77-37f0-402a-92bc-072778e77ed8",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/29a5da77-37f0-402a-92bc-072778e77ed8/application",
  "location": "New York, NY (HQ)",
  "locations": ["New York, NY (HQ)", "San Francisco, CA"],
  "isRemote": false,
  "workplaceType": "Hybrid",
  "department": "Engineering",
  "team": "Engineering",
  "employmentType": "FullTime",
  "postedAt": "2026-10-22T19:20:16.077Z",
  "updatedAt": null,
  "salary": {
    "min": 168000,
    "max": 285000,
    "currency": "USD",
    "interval": "year",
    "text": "$168K – $285K • Offers Equity",
    "source": "ats"
  },
  "requisitionId": null,
  "description": null,
  "status": "open",
  "source": "https://api.ashbyhq.com/posting-api/job-board/ramp?includeCompensation=true",
  "scrapedAt": "2026-10-24T22:30:03.326Z"
}
```

An unknown company looks like this. It is not charged.

```json
{
  "type": "error",
  "input": "not-a-real-company-zzzz",
  "provider": null,
  "companyToken": null,
  "error": "NOT_FOUND",
  "errorMessage": "No Greenhouse, Lever, or Ashby board listed jobs for \"not-a-real-company-zzzz\". Tried notarealcompanyzzzz, not-a-real-company-zzzz, not."
}
```

### How much does it cost?

$1.00 per 1,000 jobs ($0.001 per job) plus a small per-run start fee. Error rows and jobs you've already seen are never charged.

That is $0.001 per job pushed with the `job` event. A remembered job whose title, location, department, salary text, or update time changed is charged when you turn on include changed jobs, and its status is `changed`. Closed records are free. Companies that return nothing, and jobs dropped by filters, are not charged. If the run has a max charge set, pushing stops when that limit is reached and the log says so. The run still succeeds.

### Tips

Schedule the task once a day with only new jobs since last run turned on. Keep the state key stable. Seen jobs live in the named key-value store `ats-job-feed-state`, under a key that starts with `SEEN_`. That store is kept indefinitely, including on the free plan. A different state key is a different saved search, so an engineering feed and a design feed do not share memory.

Each remembered job also stores a short hash of the title, location, department, salary text, and update time. Turn on include changed jobs to receive a row when that hash changes. Those rows are charged as jobs. The hash is updated on every run that sees the job, so a later run only reports a change that happened after the last check. The first run after this hash was added records the current text and does not mark every old job as changed.

Jobs are remembered only after they pass the filters and are actually pushed. A later run with wider keywords can still pick up a role the earlier filter hid. Entries that have not matched for 90 days are dropped, and the map is capped at 300,000 jobs.

Include closed jobs when you want a record that a previously returned role left the board. That flag does nothing unless only-new mode is on. A tight max jobs per company can leave older roles out of the window, so their last-seen time is not refreshed. They are not marked closed while they remain on the board, and they age out of memory after 90 days.

Descriptions are off unless you turn them on. Leave them off for a large board. The actor reads one company at a time per worker and drops the description text when you did not ask for it.

Posted within days and posted within hours can be set together. The stricter window wins. Greenhouse dates use first published, then updated time.

#### Export jobs to Google Sheets

Apify can send the dataset to Google Sheets, Slack, or a webhook from the task's integrations. The run stops at your max results.

The default timeout is 10 minutes. The default memory is 256 MB, and a run can use from 256 MB to 1024 MB. Raise the timeout if you add a long list of large boards.

### FAQ

#### Is this legal, and where does the data come from?

The actor sends a normal GET request to the public JSON endpoints Greenhouse, Lever, and Ashby publish for career pages. The response is the same information a visitor sees on the job board. There is no login and no applicant data. Your use of the results, including who you contact, is yours to judge.

#### Why is a job missing?

The company may not use these three systems. Check the Errors view for `NOT_FOUND` or `HTTP_ERROR`. Keyword, location, department, remote, and posted-within filters drop roles that do not match. Ashby hides posts marked unlisted. Only-new mode hides jobs already stored for that state key. A role that moved to Workday or a custom site will not appear here.

#### How often is the data updated?

The actor shows the board as it is at the moment you run it. It does not refresh itself in the background. Companies update their own postings on their own schedule. Run it on the cadence you want. Daily is the usual choice with only-new mode.

#### Does it work with Workday or other ATS?

No. Only Greenhouse, Lever, and Ashby public boards are supported. Other systems return a free error row.

#### What am I charged for?

Each job row saved with the `job` event costs $0.001 ($1.00 per 1,000), plus a small start fee per run. Error rows, closed records, jobs dropped by your filters, and jobs already seen in only-new mode are free. Changed jobs are charged only when you turn on include changed jobs.

### Support

Questions, or a board URL that should have worked: use the feedback tab on this actor's Apify Store page.

# Actor input Schema

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

One company per line. A careers URL, a board slug, or a company name all work. Query strings and job ids are ignored. A bad line becomes a free error row and the run still finishes.

## `defaultProvider` (type: `string`):

Used when you set a provider and pass a bare slug. Auto compares Greenhouse, Lever, and Ashby for a slug or company name and keeps the board with the most listed jobs.

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

Keep a job when its title contains any of these words. Matching is case-insensitive. Leave empty to keep every title.

## `searchInDescription` (type: `boolean`):

Also match keywords against the plain-text job description.

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

Drop a job when its title contains any of these words. Case-insensitive.

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

Keep a job when any of its location strings contains one of these values. Primary location, offices, and secondary locations are all checked.

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

Keep a job when the workplace type is remote, drop hybrid, on-site, in-office, and office jobs, and when the workplace type is missing or unspecified use the board's remote flag, then location text that contains "remote".

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

Keep a job when its department or team contains any of these values.

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

Keep jobs posted in the last N days. Greenhouse uses first published, then updated. Lever uses created date. Ashby uses published date. Jobs with no date are dropped when this is set. If posted within hours is also set, the stricter window wins.

## `postedWithinHours` (type: `integer`):

Keep jobs posted in the last N hours. For example 24 keeps jobs from the last day. Greenhouse uses first published, then updated. Jobs with no date are dropped when this is set.

## `onlyNew` (type: `boolean`):

Skip jobs this state key has already returned. Seen jobs are not charged. They are stored in the named key-value store ats-job-feed-state, which is kept until you delete it, including on the free plan. Use a different state key for each saved search.

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

Separates seen-job memory when you run several feeds. Sanitized to characters allowed in a key-value store key.

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

With only new jobs since last run, also output a short closed record for a remembered job that is no longer listed on a board that was fetched successfully, then forget it. Closed records are free. This does nothing when only new jobs is off.

## `includeChanged` (type: `boolean`):

With only new jobs since last run, also output a remembered job when its title, location, department, salary text, or update time changes. Status is changed. These rows are charged as jobs. The latest text is stored either way.

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

Add the plain-text job description to each open job. Off by default so a large board stays small.

## `descriptionMaxLength` (type: `integer`):

Maximum description characters. Use 0 for the full text.

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

Stop after this many output records across all companies, including closed records.

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

After filters, consider only the newest N jobs from each company. Leave empty for no per-company cap.

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

How many companies to fetch at the same time. Each company's jobs are written and released before that worker starts another company.

## Actor input object example

```json
{
  "companies": [
    "https://job-boards.greenhouse.io/stripe",
    "jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "defaultProvider": "auto",
  "keywords": [
    "engineer"
  ],
  "searchInDescription": false,
  "excludeKeywords": [],
  "locations": [],
  "remoteOnly": false,
  "departments": [],
  "onlyNew": false,
  "stateKey": "default",
  "includeClosed": false,
  "includeChanged": false,
  "includeDescription": false,
  "descriptionMaxLength": 5000,
  "maxResults": 100,
  "maxJobsPerCompany": 25,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

Dataset with one row per job, and one error row per company that could not be read.

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

JSON summary of the run: counts, stop reason, and input warnings.

# 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": [
        "https://job-boards.greenhouse.io/stripe",
        "jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/ramp"
    ],
    "keywords": [
        "engineer"
    ],
    "maxResults": 100,
    "maxJobsPerCompany": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("moo_iscool/ats-job-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": [
        "https://job-boards.greenhouse.io/stripe",
        "jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/ramp",
    ],
    "keywords": ["engineer"],
    "maxResults": 100,
    "maxJobsPerCompany": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("moo_iscool/ats-job-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": [
    "https://job-boards.greenhouse.io/stripe",
    "jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "keywords": [
    "engineer"
  ],
  "maxResults": 100,
  "maxJobsPerCompany": 25
}' |
apify call moo_iscool/ats-job-feed --silent --output-dataset

```

## MCP server setup

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