# Greenhouse, Lever and Ashby Jobs Scraper (`bravona/ats-jobs-greenhouse-lever-ashby`) Actor

- **URL**: https://apify.com/bravona/ats-jobs-greenhouse-lever-ashby.md
- **Developed by:** [Bravona](https://apify.com/bravona) (community)
- **Categories:** Jobs, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 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 and Ashby Jobs Scraper

**Greenhouse, Lever and Ashby Jobs Scraper** collects **every open job** from company career boards hosted on **Greenhouse, Lever and Ashby** and returns them in **one normalized dataset**, with salary ranges and **new / open / closed tracking** between runs. Enter company board names or careers URLs and download the jobs as JSON, CSV or Excel.

Unofficial tool. Not affiliated with, endorsed by, or sponsored by Greenhouse, Lever or Ashby.

### What does Greenhouse, Lever and Ashby Jobs Scraper do?

- Reads public job boards on Greenhouse, Lever and Ashby through each platform's public job-board API. No login, no browser, no proxy.
- Detects which ATS a company uses from its board name (slug) or careers URL.
- Returns one schema for all three platforms.
- Adds salary min, max, currency and period: structured where the ATS provides it, otherwise parsed from the job text, with a `salarySource` field saying which.
- Marks each job as `new`, `open` or `closed` compared with your previous run, so a scheduled run shows what changed since yesterday.
- Leaves out recruiter and hiring-manager fields and removes email addresses from description text.
- Runs on Apify: call it via API, schedule runs, export to JSON, CSV or Excel, and connect integrations.

### Who is it for?

**Job seekers and job-search tools**: watch the companies you care about and see new openings first. **Job-market and labor analysts**: track hiring by company, department, location and salary over time. **Job-board and aggregator builders**: feed one clean jobs feed from many company boards.

### What data can you get from Greenhouse, Lever and Ashby job boards?

One record per job. Fields that can be empty are null; `allLocations` can be an empty list.

| Field | Type | Description |
|---|---|---|
| `company` | string | Company board name (slug), lowercased |
| `ats` | string | Which platform hosts the board: `greenhouse`, `lever` or `ashby` |
| `jobId` | string | The job's ID on that platform |
| `title` | string | Job title |
| `department` | string or null | Department (Greenhouse: first department; Lever and Ashby: department) |
| `team` | string or null | Team (Greenhouse: second department; Lever and Ashby: team) |
| `location` | string or null | Primary job location as the ATS shows it |
| `allLocations` | list of strings | Primary plus any other listed locations |
| `isRemote` | boolean | True when the ATS marks the workplace as Remote or a listed location says remote, anywhere or work from home. Hybrid alone is not remote |
| `employmentType` | string or null | `FullTime`, `PartTime`, `Contract`, `Intern` or `Temporary`. Greenhouse's API has no standard field, so it is often null there |
| `salaryMin` | number or null | Lowest salary in the range (null for "up to" amounts) |
| `salaryMax` | number or null | Highest salary in the range (null for "from" amounts) |
| `salaryCurrency` | string or null | ISO code, e.g. USD, EUR, GBP |
| `salaryPeriod` | string or null | `year`, `month`, `week`, `day`, `hour`, or null |
| `salarySource` | string or null | `structured` (from the ATS), `parsed` (from the job text), or null (no salary) |
| `postedAt` | string or null | When the job was published, ISO 8601 UTC (Greenhouse `first_published`, Lever `createdAt`, Ashby `publishedAt`) |
| `status` | string | `new`, `open` or `closed` compared with the previous run |
| `descriptionText` / `descriptionHtml` | string or null | Job description in the format you picked, with email addresses replaced by `[email removed]`. Only present when **Job description** is `text` or `html`; null on `closed` records |
| `applyUrl` | string or null | Link to apply (Greenhouse: the job's public URL, which may be on the company's own careers site) |
| `jobUrl` | string or null | Link to the job posting |
| `firstSeenAt` | string | When this Actor first output the job (ISO 8601 UTC) |
| `lastSeenAt` | string | The last run that saw the job on its board |
| `scrapedAt` | string | When this run read the board |

No recruiter or hiring-manager fields are output.

#### Salary

- **Structured first.** Greenhouse pay ranges (`pay_transparency=true`, the first range listed; amounts converted from cents), Lever `salaryRange`, Ashby compensation summary (the Salary component).
- **Otherwise parsed** from the salary text and description. Supported formats: ranges (`$120,000 - $150,000`, `between $100,000 and $130,000`), the `k` suffix (`$120k–$150k`, `$120-150K`, `$211.4K`), single values (`Salary: $95,000`, `up to £60k`, `from €50.000`), hourly / daily / weekly / monthly / yearly wording (`$45/hr`, `per day`, `monthly`, `p.a.`), currencies USD, EUR, GBP, CAD and AUD by symbol or ISO code before or after the number (`$`, `US$`, `CA$`, `C$`, `A$`, `AU$`, `€`, `£`; a bare `$` is read as USD); "up to" sets only `salaryMax`, "from" / "starting at" sets only `salaryMin`, and labels: a range marked **base** wins over one marked **OTE**.
- **Ambiguous text gives null:** no currency, mixed currencies, funding or revenue amounts, benefits (bonus, HRA, relocation, budgets), max below min, a max more than 5× the min, hourly values over 1,000, or yearly values under 10,000. A single amount needs a pay word (salary, pay, compensation, base...) nearby or a period.
- **salaryPeriod** comes from the ATS's interval field (Lever `per-hour-wage`, Ashby `1 YEAR`, a Greenhouse range titled "Monthly Pay Range") or from period words in the text. When neither exists, amounts of 15,000 or more are treated as yearly and smaller amounts keep `salaryPeriod: null`.

### Sources and terms

| Source | Access method | Bravona's pacing | What the source says |
|---|---|---|---|
| Greenhouse | Public Job Board API (`boards-api.greenhouse.io/v1/boards/{token}/jobs`) | 2 requests per second | [Greenhouse docs](https://developers.greenhouse.io/job-board.html): "Job Board data is publicly available, so authentication is not required for any GET endpoints." |
| Lever | Public Postings API (`api.lever.co/v0/postings/{company}`; EU boards on `api.eu.lever.co`) | 1 request per second | [Lever postings API README](https://github.com/lever/postings-api): "all job postings in the `published` state are publicly viewable." The API's robots.txt sets `Crawl-delay: 1`. |
| Ashby | Public Job Postings API (`api.ashbyhq.com/posting-api/job-board/{org}`) | 1 request per second | [Ashby docs](https://developers.ashbyhq.com/docs/public-job-posting-api): the API returns "data for all currently published Job Postings" for an organization. |

Pacing is the Actor's own request rate, with backoff and retries on HTTP 429 and 5xx. Greenhouse and Ashby publish no rate limit. Requests identify the Actor with the User-Agent `Bravona-ATSJobs/0.1 (+apify.com/bravona)`.

### How to find a company's board name

Enter either the company's careers URL on Greenhouse, Lever or Ashby, or just its board name (slug), e.g. `airbnb`. The board name is the first part of the path in the board URL, which isn't always the company name.

| ATS | Board URL | Example |
|---|---|---|
| Greenhouse | `boards.greenhouse.io/<name>` or `job-boards.greenhouse.io/<name>` | https://job-boards.greenhouse.io/discord |
| Lever | `jobs.lever.co/<name>` (EU: `jobs.eu.lever.co/<name>`) | https://jobs.lever.co/spotify |
| Ashby | `jobs.ashbyhq.com/<name>` | https://jobs.ashbyhq.com/ramp |

- **Greenhouse:** open any job on the company's careers page. If the address is `boards.greenhouse.io/<name>/jobs/...` or `job-boards.greenhouse.io/<name>/jobs/...`, `<name>` is the board name. If the careers page is on the company's own site, look for `gh_jid=` in job links or an embed `boards.greenhouse.io/embed/job_board?for=<name>`. You can paste the embed URL too.
- **Lever:** job links look like `jobs.lever.co/<name>/<job id>`. For boards in Lever's EU region (`jobs.eu.lever.co/<name>`), paste the URL so the EU API is used directly.
- **Ashby:** job links look like `jobs.ashbyhq.com/<name>/<job id>`.

With **ATS** on `auto`, a URL decides the platform. For a bare name the Actor checks Greenhouse, Lever (US, then EU) and Ashby and uses the board with the most jobs, because a name can exist on more than one ATS (Ramp has a leftover 1-job Greenhouse board and its real board on Ashby). The result is cached and reused on later runs; if the cached board disappears or goes empty, the Actor checks again.

Careers pages on other systems (Workday, iCIMS, SmartRecruiters, a company's own site without one of the boards above) are not supported.

### How to use Greenhouse, Lever and Ashby Jobs Scraper

1. Open the Actor and go to the **Input** tab.
2. Enter **companies**: 1 to 500 board names or careers URLs (e.g. airbnb, spotify, ramp).
3. Leave **ATS** on auto, or pick Greenhouse, Lever or Ashby.
4. Optionally filter by **title keywords**, **locations** or **remote only**, choose how to include descriptions, and set **Max jobs**.
5. Click **Start**, then download the dataset as JSON, CSV or Excel, or fetch it via the Apify API.

### Track new and closed jobs

Every record carries a `status` on every run, whatever your settings. **Only new since last run** does not turn status on; it only limits output to `new` and `closed` records.

- **First run** for a state key: every job is `new`.
- **Later runs:** a job not returned before is `new`; a job returned before and still on the board is `open`; a job returned before that is gone from the board is `closed` (once). A closed record carries the job's last known fields, with `lastSeenAt` = the last run that saw it.
- `onlyNewSinceLastRun: true` outputs only `new` and `closed`; `open` jobs are still tracked.
- **Order and caps:** per company, `closed` records come first, then `new` and `open` together, most recently posted first. So `maxJobsPerCompany: 5` gives the 5 most recent jobs, and on the next run those same jobs come back as `open` (with any newer posting as `new`).
- "Seen" means **delivered**: a job counts as seen only after it has been output. A job held back by `maxJobs`, `maxJobsPerCompany`, a filter, or your max-charge limit is returned as `new` on a later run instead of being lost.
- **A company switches ATS:** if the detected ATS differs from the one stored, all previously stored jobs are output as `closed` (free) and all current jobs as `new`, and the log shows a warning.
- If a company's board cannot be read in a run (error or not found), its state is left unchanged and the error is in the run summary.

**Where state lives:** the named key-value store **`bravona-ats-jobs-state`** in your Apify account (it persists across runs, unlike a run's default store). Keys:

- `<stateKey>--<company>`: the jobs seen for one company, with the ATS it was on. Characters other than letters, digits and `!-_.'()` are replaced by `_` plus a short hash.
- `<stateKey>--INDEX`: the list of companies under that state key, used by `resetState`.
- `DETECT--<company>`: the cached auto-detected ATS (shared by all state keys; not cleared by `resetState`).

The default `stateKey` is `jobs-` plus a hash of the sorted, lowercased company list, so the same list always uses the same state and a different list starts fresh. Set your own `stateKey` to keep history when you add or remove companies, or to give two tasks with the same companies separate state. `resetState: true` deletes that state key's records before the run.

**Schedule example:** save a task with your company list, a fixed `stateKey` (e.g. `daily-fintech`) and `onlyNewSinceLastRun: true`, then add an Apify schedule with cron `0 7 * * *`. Each morning the dataset holds only jobs posted and closed since the previous run. Connect the dataset to a webhook, Google Sheets or your database to process it.

#### Run summary

Each run writes `OUTPUT` to its default key-value store: the state store and key, totals by status, how many results were charged, whether the max-charge limit or `maxJobs` was reached, and per company: input, detected ATS (and Lever region), how it was detected (`url`, `explicit`, `cache`, `probe`, with what each probe found), jobs on the board, jobs matching filters, output counts by status, results charged, a previous ATS if it switched, and any error. A company that can't be read doesn't fail the run; the run fails only if no company could be read.

### Input

| Field | Type | Required | Default | Allowed values / limits | What it does |
|---|---|---|---|---|---|
| `companies` | array of strings | yes | none | 1 to 500 entries | Board slugs or careers URLs (patterns above). Duplicates (case-insensitive) are removed. |
| `ats` | string | no | `auto` | `auto`, `greenhouse`, `lever`, `ashby` | `auto` detects per company. A fixed value skips detection for bare slugs. A URL always wins over this setting. |
| `titleKeywords` | array of strings | no | `[]` | any | Keep a job if its title contains **any** keyword (case-insensitive substring). |
| `locations` | array of strings | no | `[]` | any | Keep a job if **any** entry is a case-insensitive substring of its location or one of its other listed locations. The entry `Remote` also matches jobs flagged remote. |
| `remoteOnly` | boolean | no | `false` | | Keep only jobs with `isRemote: true`. |
| `includeDescription` | string | no | `text` | `text`, `html`, `none` | Adds `descriptionText`, `descriptionHtml`, or neither. |
| `onlyNewSinceLastRun` | boolean | no | `false` | | Output only `new` and `closed` jobs (skip `open`). |
| `maxJobs` | integer | no | `10000` | 1 to 100000 | Max output records per run (all companies, all statuses). |
| `maxJobsPerCompany` | integer | no | no limit | 1 to 100000 | Max output records per company (all statuses). |
| `stateKey` | string | no | derived from the company list | letters, digits, `-`, `_`, `.`; up to 100 chars | Name of the change-tracking state. |
| `resetState` | boolean | no | `false` | | Delete the stored state for this `stateKey` before the run. |

Filters combine with AND (`titleKeywords` AND `locations` AND `remoteOnly`). Jobs removed by a filter are never output, never charged and never stored as seen.

Example input (this is also the prefilled input, and the input used for the output example below):

```json
{
  "companies": ["airbnb", "spotify", "ramp"],
  "ats": "auto",
  "includeDescription": "none",
  "maxJobsPerCompany": 5
}
```

### Output example

One record per job. Real, unedited records from a second run of the example input on Oct 30, 2026 (`includeDescription: "none"`, so no description fields), one per ATS. The first run returned 15 jobs in about 3.3 seconds; this rerun returned the same 15 as `open`.

Greenhouse (airbnb):

```json
{
  "company": "airbnb",
  "ats": "greenhouse",
  "jobId": "8337229",
  "title": "Senior Manager, Community Policy",
  "department": "Public Policy",
  "team": null,
  "location": "United States",
  "allLocations": [
    "United States"
  ],
  "isRemote": true,
  "employmentType": null,
  "salaryMin": 204000,
  "salaryMax": 255000,
  "salaryCurrency": "USD",
  "salaryPeriod": "year",
  "salarySource": "structured",
  "postedAt": "2026-10-30T19:19:06.000Z",
  "applyUrl": "https://careers.airbnb.com/positions/8337229?gh_jid=8337229",
  "jobUrl": "https://careers.airbnb.com/positions/8337229?gh_jid=8337229",
  "status": "open",
  "firstSeenAt": "2026-10-30T19:56:03.273Z",
  "lastSeenAt": "2026-10-30T19:58:06.840Z",
  "scrapedAt": "2026-10-30T19:58:06.840Z"
}
```

Lever (spotify):

```json
{
  "company": "spotify",
  "ats": "lever",
  "jobId": "2d67c471-9015-41c0-a041-b982ec4a14fd",
  "title": "Account Executive - Commercial, Spotify Technology",
  "department": "Operations and Business Support",
  "team": "Platform",
  "location": "London",
  "isRemote": false,
  "allLocations": [
    "London"
  ],
  "employmentType": "FullTime",
  "salaryMin": null,
  "salaryMax": null,
  "salaryCurrency": null,
  "salaryPeriod": null,
  "salarySource": null,
  "postedAt": "2026-10-23T21:51:17.203Z",
  "applyUrl": "https://jobs.lever.co/spotify/2d67c471-9015-41c0-a041-b982ec4a14fd/apply",
  "jobUrl": "https://jobs.lever.co/spotify/2d67c471-9015-41c0-a041-b982ec4a14fd",
  "status": "open",
  "firstSeenAt": "2026-10-30T19:56:03.273Z",
  "lastSeenAt": "2026-10-30T19:58:06.840Z",
  "scrapedAt": "2026-10-30T19:58:06.840Z"
}
```

Ashby (ramp):

```json
{
  "company": "ramp",
  "ats": "ashby",
  "jobId": "aa0c645d-7aa0-40b0-bdfa-9b3fb19da404",
  "title": "Customer Activation Manager | Stack Accounting",
  "department": "Sales",
  "team": "Customer Success Management",
  "location": "New York, NY (HQ)",
  "isRemote": false,
  "allLocations": [
    "New York, NY (HQ)"
  ],
  "employmentType": "FullTime",
  "salaryMin": 121000,
  "salaryMax": 184000,
  "salaryCurrency": "USD",
  "salaryPeriod": "year",
  "salarySource": "structured",
  "postedAt": "2026-10-30T19:35:16.997Z",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/aa0c645d-7aa0-40b0-bdfa-9b3fb19da404/application",
  "jobUrl": "https://jobs.ashbyhq.com/ramp/aa0c645d-7aa0-40b0-bdfa-9b3fb19da404",
  "status": "open",
  "firstSeenAt": "2026-10-30T19:56:03.273Z",
  "lastSeenAt": "2026-10-30T19:58:06.840Z",
  "scrapedAt": "2026-10-30T19:58:06.840Z"
}
```

That run's `OUTPUT` record showed 15 records (5 per company, all `open`), every company detected from the cache, and nothing failed.

### How much does it cost to get job postings from Greenhouse, Lever and Ashby?

You pay for `new` and `open` job records saved to your dataset. `closed` records are free. Jobs removed by your filters are never output or charged.

| Event | When it is charged | Price |
|---|---|---|
| Actor start | Once per run | $0.00005 |
| Job returned | Each `new` or `open` job saved | $0.0015 ($1.50 per 1,000) |

**Important:** a full rerun charges again for jobs that are still open. With **Only new since last run** on, a run is charged only for new jobs.

Examples:

- 100 jobs: about **$0.15** plus the $0.00005 start fee. 1,000 jobs: about **$1.50** plus the start fee.
- Prefilled run (airbnb, spotify, ramp, Max jobs per company 5, descriptions off): up to 3 × 5 = 15 jobs × $0.0015 = about **$0.0225**, plus the $0.00005 start fee.
- Daily schedule for 30 days, illustrative numbers: boards with 300 open jobs, about 10 new jobs a day.
  - New-only **off**: each run charges about 300 jobs, about $0.45 a day, about **$13.50** for 30 days.
  - New-only **on**: the first run charges all 300 (every job is new on a first run), $0.45; then about 10 new jobs a day, $0.015 a day, about **$0.89** for 30 days.
  - Plus $0.00005 per run start ($0.0015 for 30 runs).
- A run that finds no jobs pays only the start fee.

Platform usage (compute, proxy, storage) is included in these prices. You pay only the event prices listed above, nothing extra on top.

Works on Apify's free plan; you pay a tiny start fee plus only for results returned, meaning `new` and `open` jobs saved. `closed` records are free, and a rerun charges again for jobs that are still open. Charges come out of your Apify account's prepaid usage; on the Free plan, once that monthly usage is used up, runs stop until the next billing cycle.

**Spending limit:** set a maximum cost per run. When it is reached, the Actor stops saving chargeable jobs (closed records of the company in progress still go out), skips the remaining companies and keeps their state, so nothing is lost on the next run.

### Limits

- The Actor paces itself: Greenhouse 2 requests per second, Lever and Ashby 1 per second. Greenhouse and Ashby publish no limit.
- Each board is downloaded in full (one request per company, descriptions included) whatever the settings, so **Job description: none** and **Max jobs per company** make the output smaller, not the download faster. Only **Max jobs per company** (fewer records) lowers the cost; descriptions don't change the price, since charges are per record. When **Max jobs** or the spending limit is reached, the remaining companies are skipped without being fetched. Detecting the ATS of a new bare name takes up to 4 requests.
- Covers only boards hosted on Greenhouse, Lever and Ashby. Other ATS and company sites are not supported.
- Up to 500 companies per run; up to 100,000 output records (default 10,000).
- Salary appears only when the ATS provides it or the job text states it clearly; parsed salaries can be wrong, and ambiguous values are left null.
- Recruiter and hiring-manager fields are not collected; email addresses are removed from descriptions.

### FAQ

**Is it legal to use this data?** The Actor reads public job boards through each platform's public API without logging in. You are responsible for how you use the data. This is not legal advice.

**Does it include recruiter or contact details?** No. Recruiter and hiring-manager fields are not collected, and email addresses are removed from description text.

**Why is a company not found?** The name must match the board name (see How to find a company's board name), and the board must be public on Greenhouse, Lever or Ashby. The log and the `OUTPUT` record say which platforms were checked.

**What if a company switches ATS?** Its old jobs are reported once as `closed` (free), its current jobs come through as `new`, and a warning is logged. See Track new and closed jobs.

**How is salary parsed?** Structured ATS salary data is used first. Otherwise the Actor parses ranges, "k" suffixes, single values ("up to" gives only a max, "from" only a min), and hourly, daily, weekly, monthly or yearly figures in USD, EUR, GBP, CAD or AUD, and sets `salaryPeriod` and `salarySource`. Without a period word, amounts of 15,000 or more count as yearly. Ambiguous values are left null. See Salary above.

**Why is employmentType null for Greenhouse jobs?** Greenhouse's public API has no standard employment type field. The Actor uses a company's custom field (e.g. "Employment Type") when there is one.

**Why do closed jobs have no description?** Descriptions aren't stored between runs, to keep state small. All other fields are the last known values.

**Am I charged again for the same job on every run?** On a full run, yes: every `new` or `open` job is charged each time. Turn on **Only new since last run** to be charged only for new jobs; `closed` records are always free. See the daily schedule example under Pricing.

**Can I run different filters on the same companies?** Yes. Give each task its own **State key** so each keeps its own history.

**Do I pay for Apify compute on top?** No. Platform usage is included. You pay only the listed event prices: $0.00005 per run start and $0.0015 per job.

**Can I use it via API or integrations?** Yes, like any Apify Actor, through the API tab, schedules and integrations.

**Something broke or a field is missing?** Open an issue in the **Issues** tab.

### Changelog

- **0.1 (Unreleased):** First version. Greenhouse, Lever (US and EU) and Ashby; auto-detect with caching; normalized salary and employment type; new/open/closed tracking with State key and Reset state; per-company `OUTPUT` summary.

### Disclaimer

Greenhouse, Lever and Ashby Jobs Scraper is an unofficial tool built by Bravona. It is not affiliated with, endorsed by, or sponsored by Greenhouse, Lever or Ashby. All product names and trademarks belong to their owners and are used only to describe compatibility. The Actor reads publicly available job postings through public job-board APIs without logging in, and follows a conservative request rate. Data is provided as-is; accuracy and availability depend on the source. You are responsible for using the data in line with each platform's terms and applicable law (including privacy law).

# Actor input Schema

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

Required. 1 to 500 company board names (slugs) or careers URLs on Greenhouse, Lever or Ashby, e.g. "airbnb" or https://jobs.lever.co/spotify. Accepted URLs: boards.greenhouse.io/X, job-boards.greenhouse.io/X, jobs.lever.co/X, jobs.eu.lever.co/X, jobs.ashbyhq.com/X. Duplicates are removed. Each new or open job saved is billed; see Pricing.

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

Optional, default auto. auto: a careers URL decides the ATS; for a bare slug the Actor checks Greenhouse, Lever (US, then EU) and Ashby, uses the board with the most jobs, and caches the result for later runs. Pick one ATS to skip detection for every bare slug (URLs still win).

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

Optional. Keep a job if its title contains any of these words (not case-sensitive), e.g. "data engineer". Leave empty to keep all. Filtered-out jobs are not output or charged.

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

Optional. Keep a job if its location (or one of its other listed locations) contains any of these entries, not case-sensitive, e.g. "New York". "Remote" also matches jobs flagged remote. Leave empty for all.

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

Only keep remote jobs (isRemote = true). Default off. Combined with the other filters using AND.

## `includeDescription` (type: `string`):

Include the description as plain text (descriptionText), HTML (descriptionHtml), or not at all. Default text. Email addresses are removed.

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

Every record always has a status (new, open or closed). Turn this on to output only new and closed records (closed records are free). Use with a schedule to be charged only for new jobs. Default off. On the first run under a state key every job is new.

## `maxJobs` (type: `integer`):

Maximum records to output in total, all companies and statuses. Default 10,000, from 1 to 100,000. Caps cost. Companies after the cap is reached are skipped and keep their state.

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

Maximum records to output for each company, all statuses. No limit by default (1 to 100,000). Caps cost. Jobs cut off are not marked as seen, so they come back as new on a later run.

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

Optional. Name of the change-tracking state. Default: derived from the sorted company list (jobs-<hash>), so a different company list starts fresh. Set your own (letters, digits, - \_ . ; up to 100 characters) to keep state when you edit the list, or to give two tasks separate state. State lives in the named key-value store bravona-ats-jobs-state under <stateKey>--<company>.

## `resetState` (type: `boolean`):

Optional, default false. When true, deletes the stored state for this state key first, so every current job comes back as new and nothing is reported closed.

## Actor input object example

```json
{
  "companies": [
    "airbnb",
    "spotify",
    "ramp"
  ],
  "ats": "auto",
  "remoteOnly": false,
  "includeDescription": "none",
  "onlyNewSinceLastRun": false,
  "maxJobs": 10000,
  "maxJobsPerCompany": 5,
  "resetState": false
}
```

# Actor output Schema

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

No description

## `summary` (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": [
        "airbnb",
        "spotify",
        "ramp"
    ],
    "ats": "auto",
    "includeDescription": "none",
    "maxJobsPerCompany": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("bravona/ats-jobs-greenhouse-lever-ashby").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": [
        "airbnb",
        "spotify",
        "ramp",
    ],
    "ats": "auto",
    "includeDescription": "none",
    "maxJobsPerCompany": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("bravona/ats-jobs-greenhouse-lever-ashby").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": [
    "airbnb",
    "spotify",
    "ramp"
  ],
  "ats": "auto",
  "includeDescription": "none",
  "maxJobsPerCompany": 5
}' |
apify call bravona/ats-jobs-greenhouse-lever-ashby --silent --output-dataset

```

## MCP server setup

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

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/WckEJiI7kUtjhWHqV/builds/7GQfnf2usoeWMHu5m/openapi.json
