# ATS Jobs API with Salary: Greenhouse, Lever, Ashby & more (`flipvo/ats-jobs-scraper`) Actor

Every open job from any company on Greenhouse, Lever, Ashby, Workable, Recruitee or Personio: salary as numbers, and what changed since your last run. $1.20 per 1,000 jobs. Fields: salaryMin, salaryMax, salaryCurrency, salaryInterval, salarySource, changeStatus (new, changed, closed).

- **URL**: https://apify.com/flipvo/ats-jobs-scraper.md
- **Developed by:** [Christopher Nowlan](https://apify.com/flipvo) (community)
- **Categories:** Jobs, Lead generation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.20 / 1,000 jobs

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 Jobs API with Salary: Greenhouse, Lever, Ashby, Workable, Recruitee & Personio

Read the open jobs of any company that hires through **Greenhouse**, **Lever**, **Ashby**, **Workable**, **Recruitee** or **Personio** in one
clean format. Every row carries three things a plain job list does not:

1. **The salary as numbers, even when the board has no pay field.** The Actor uses the range the employer filled in on
   the board (`salarySource: "structured"`) and, when there is none, reads the range written in the job description,
   such as `$150,000 - $200,000` or `$75/hr - $90/hr` (`salarySource: "text"`). It never estimates: a posting that
   states no pay returns `null`.
2. **What changed since your last run.** Each job is flagged `new`, `changed` or `unchanged` against the previous run
   of the same board, and with `onlyChanges` you also get a free `closed` row for every job that left the board.
3. **A price you can work out in advance.** **$1.20 per 1,000 jobs delivered**: filtered-out jobs, closed rows and
   company-name lookups are free, and with `onlyChanges` so are unchanged jobs. A board of 200 jobs costs about $0.24.

Give a company name, a careers link or a ready-made preset; no board tokens needed. No login, no proxy, no browser: the
Actor calls the public job-board endpoints that these vendors provide for exactly this purpose, so runs are fast, cheap
and stable.

### What you get

- One schema for all six platforms: title, department, team, location, remote flag, workplace type, employment
  type, posted date, job and apply URLs.
- **Salary as numbers**, not text: `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryInterval` (`year`, `month`,
  `week`, `day`, `hour`), plus every pay zone the employer listed in `salaryRanges`. `salarySource` says where the
  number came from: `structured` (the field the employer filled in on the board) or `text` (a range such as
  `$150,000 - $200,000` written in the job description, as US pay-transparency laws require). Greenhouse pay ranges
  (which come in cents) are converted to normal currency units.
- **Change tracking**: each row says whether the job is `new`, `changed` or `unchanged` since the last run of the same
  board. Turn on `onlyChanges` to be charged only for new and changed jobs, which suits a daily schedule, and to get a
  free `closed` row for every job that left the board (see below).
- **No tokens to look up**: type `airbnb`, paste `https://careers.airbnb.com` or a board link, or pick a preset. The
  Actor finds which of the six vendors hosts the company and tells you in the run's `SUMMARY`.
- Filters: title keywords, locations, remote only, only jobs with a published salary.
- Polite by design: robots.txt is read and obeyed on every run, requests are spaced (at least 0.5 s apart, longer if a
  site's Crawl-delay says so) and failed requests are retried with backoff.

### Use from AI agents (Apify MCP)

Add the Actor to an MCP client with `https://mcp.apify.com/?actors=flipvo/ats-jobs-scraper`. It appears as one tool,
`flipvo--ats-jobs-scraper`, whose arguments are the fields below. Nothing is required: a call with no arguments reads
the three example boards. Pass `boards` (a list) for real work. The result names the run's dataset; fetch the rows
with the dataset tool and ask for the fields you need, for example `company,title,salaryMin,salaryMax,salaryCurrency,
salaryInterval,salarySource,changeStatus,url`.

### Input

| Field                              | What it does                                                                                                                                                                                                                 | Default                                                |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| `boards`                           | Companies to read: a name (`airbnb`), a careers URL, a board URL, or `greenhouse:stripe` / `lever:spotify` / `ashby:ashby` / `workable:hirefellows` / `recruitee:channable` / `personio:lanch`. Up to 200, presets included. | boards or presets; none at all runs the example boards |
| `presets`                          | Ready-made company lists added to `boards`: `ai-labs`, `devtools` (see below)                                                                                                                                                | none                                                   |
| `keywords`                         | Keep jobs whose title or department contains any of these words                                                                                                                                                              | all jobs                                               |
| `locations`                        | Keep jobs whose location contains any of these texts                                                                                                                                                                         | all locations                                          |
| `remoteOnly`                       | Only jobs the employer marks or labels as remote                                                                                                                                                                             | off                                                    |
| `onlyWithSalary`                   | Only jobs with a published salary, structured or written in the description                                                                                                                                                  | off                                                    |
| `includeDescription`               | Add the plain-text description to each job (the salary is read from it either way)                                                                                                                                           | off                                                    |
| `onlyChanges`                      | Deliver only new and changed jobs since your previous run, plus a free `closed` row per job that left                                                                                                                        | off                                                    |
| `maxJobsPerBoard` / `maxJobsTotal` | Caps (per board, counted after `onlyChanges`); `maxJobsTotal` is your budget cap                                                                                                                                             | 1000 / 1000                                            |
| `requestDelayMs`                   | Minimum gap between requests to one host (200 to 10000)                                                                                                                                                                      | 500                                                    |
| `maxRetries`                       | Retries with backoff for 429, 5xx and network errors (0 to 6)                                                                                                                                                                | 3                                                      |

Example:

```json
{
    "boards": ["airbnb", "https://careers.zoox.com", "ashby:ashby"],
    "presets": ["devtools"],
    "keywords": ["engineer"],
    "onlyWithSalary": true,
    "maxJobsTotal": 500
}
```

#### Company names, careers links and board links

Each entry of `boards` is read like this:

- **A board link or `platform:token`** (`https://boards.greenhouse.io/stripe`, `https://job-boards.greenhouse.io/stripe`,
  `https://jobs.lever.co/spotify`, `https://jobs.ashbyhq.com/ashby`, `https://apply.workable.com/hirefellows`, `https://channable.recruitee.com`, `https://lanch.jobs.personio.de`,
  `greenhouse:stripe`, `workable:hirefellows`, `recruitee:channable`, `personio:lanch`): used as given. Nothing is
  looked up.
- **A company name** (`airbnb`, `Scale AI`) **or a careers link** (`https://careers.airbnb.com`): the Actor builds the
  likely board token from the name (`scaleai`, then `scale-ai`; for a link, from the domain), asks the public API of
  Greenhouse, then Lever, then Ashby, then Workable, then Recruitee, then Personio, and uses the **first board that has open
  jobs**. Every request goes through the
  same robots.txt check and request spacing as the rest of the run. It does not open or scrape the careers page.
  Where a company's board token differs from its name, or one name exists on two vendors, give the explicit form.
  **A bare name matches a board token, not a company:** a token can belong to a different company with the same or a
  similar name (`ashby` reads Ashby's own careers board). Check `ats` and `board` in `SUMMARY`, or paste the company's
  careers or board link when you need that exact company.
- Links to other job-board vendors (SmartRecruiters, Workday and so on) are refused with a message naming the vendor.

The `SUMMARY` record lists every input with `input` (what you typed), `ats` (`greenhouse`, `lever`, `ashby`, `workable`, `recruitee`, `personio`, or `null`
when no board was found), `board` (the board that was read), `source` (`explicit`, `url`, `name`, `careers-url` or
`preset:<name>`) and, for names and careers links, `tried` (every board asked). A name that matches nothing is reported
there with the reason and does not stop the other boards.

#### Presets

`presets` adds a curated list of boards to the run. Each list holds only boards that returned open jobs from their
public API when it was last checked (2026-11-04 20:03 UTC); a board that has since moved or closed is skipped and
reported in `SUMMARY`.

| Preset     | Boards                                                                                                                   |
| ---------- | ------------------------------------------------------------------------------------------------------------------------ |
| `ai-labs`  | Anthropic, OpenAI, xAI, Cohere, Perplexity, Scale AI, ElevenLabs, Together AI (8)                                        |
| `devtools` | Vercel, GitLab, Datadog, MongoDB, LaunchDarkly, Grafana Labs, Supabase, Linear, PostHog, Sentry, Render, ClickHouse (12) |

The exact boards are in [`src/presets.json`](src/presets.json).

### Output

One dataset row per job. A sample from a real run is in [`docs/sample-output.json`](docs/sample-output.json); a row
looks like this:

```json
{
    "platform": "lever",
    "board": "zoox",
    "company": "zoox",
    "jobId": "7980e9a3-99f3-4988-ae6d-355c9199d8fb",
    "title": "Body Structures Engineer",
    "department": "Vehicle Development",
    "team": "Body, Closures and Chassis",
    "location": "Foster City, CA",
    "isRemote": false,
    "workplaceType": "onsite",
    "employmentType": "Full-time",
    "postedAt": "2026-09-10T20:41:07.958Z",
    "url": "https://jobs.lever.co/zoox/7980e9a3-99f3-4988-ae6d-355c9199d8fb",
    "salaryMin": 170000,
    "salaryMax": 204000,
    "salaryCurrency": "USD",
    "salaryInterval": "year",
    "salarySource": "structured",
    "changeStatus": "new",
    "scrapedAt": "2026-11-04T19:47:27.000Z"
}
```

#### Fields

| Field                                                        | Meaning                                                                                                                     |
| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `platform`, `board`, `company`, `jobId`                      | Where the job lives and its id on that board                                                                                |
| `title`, `department`, `team`, `location`                    | As the board states them (`team` is Lever and Ashby only; the other vendors have none)                                      |
| `isRemote`                                                   | `true` when the board marks the job remote or the location or title says Remote; otherwise `null` (never `false` from text) |
| `workplaceType`, `employmentType`                            | `remote` / `hybrid` / `onsite` and the employment type, where the board states them; otherwise `null`                       |
| `postedAt`, `updatedAt`                                      | ISO 8601 dates; `updatedAt` is Greenhouse and Recruitee only; Workable and Personio give only a publish date                |
| `url`, `applyUrl`                                            | Job page and application link                                                                                               |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryInterval` | First salary range, in normal currency units; `null` when there is none                                                     |
| `salarySource`                                               | `structured`, `text` or `null` (see Salary below)                                                                           |
| `salaryRanges`                                               | Every range found, one per pay zone: `label`, `min`, `max`, `currency`, `interval`                                          |
| `descriptionText`                                            | Plain-text description, only with `includeDescription`                                                                      |
| `changeStatus`, `scrapedAt`                                  | `new`, `changed`, `unchanged` (or `closed`) since your previous run, and when the row was read                              |

#### Salary

1. **Structured first.** If the employer filled in the board's pay fields (Greenhouse pay transparency, Lever
   `salaryRange`, Ashby compensation, Recruitee `salary`), that range is used and `salarySource` is `structured`. It always wins over text.
2. **Otherwise the description text.** Many employers, Stripe among them, state the pay only in the posting prose. The
   Actor reads ranges written as `$150,000 - $200,000`, `$150K–$200K USD`, `$75/hr - $90/hr`,
   `$120,000 to $150,000 per year`, `Salary: $193,232 - 288,000/yr`, `Rs 4000000 - Rs 6000000`,
   `80.000 - 120.000 EUR`, `15,50 € und 19,50 € pro Stunde` and similar, with an interval from the words around the range
   (`/yr`, `per hour`, `annually`, "the hourly rate is") or, with none, from an annual-sized number. These rows have
   `salarySource: "text"`.
3. **Strict, so a wrong salary is rare.** A range needs a currency symbol and two amounts. Single figures,
   `$5M funding`, `$100B market`, `$20k-$100k+ ARR`, `401(k)`, and ranges next to "bonus", "sign-on", "equity" or "relocation" are
   ignored; a range with no interval and a small number (`$20 - $30`) is ignored. If nothing qualifies the salary is
   `null` and `salarySource` is `null`.

The description is read even when `includeDescription` is off, but it only appears in the row when you turn it on.

#### Accuracy, measured

Measured 2026-11-04 on a sample of 145 jobs, drawn at random (seed 41) from 5,538 jobs the Actor delivered from 34 public
boards between 22:46Z and 22:49Z UTC. The sample covers all six vendors and every `salarySource` (structured, text, none).
Each job's salary and remote status were checked by hand against the posting's own pay text. Each vendor and
`salarySource` group was sampled on its own, so the population-weighted rates scale each group by its size.

| Measure                                                                       | Result                                                                     |
| ----------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `salarySource: structured`: exact (min, max, currency and interval all right) | 35 of 36 (97.2%)                                                           |
| `salarySource: structured`: per field (min / max / currency / interval)       | 97.2% / 100.0% / 100.0% / 100.0%                                           |
| `salarySource: text`: exact (min, max, currency and interval all right)       | 41 of 41 (100.0%)                                                          |
| `salarySource: text`: per field (min / max / currency / interval)             | 100.0% / 100.0% / 100.0% / 100.0%                                          |
| Postings that state pay where we deliver none                                 | 7 of 84 sampled (6 state one value, not a range); 9.1% population-weighted |
| Delivered salaries exact, population-weighted                                 | 99.7%                                                                      |
| `isRemote: true` right                                                        | 17 of 17 (100.0%)                                                          |
| `isRemote: false` right                                                       | 61 of 62 (98.4%)                                                           |
| `isRemote: null` (no remote signal in the vendor's fields, location or title) | 66 of 145                                                                  |
| Before this release's fixes                                                   | text exact 94.3%, missed 15 of 84, `isRemote: true` right 51.5%            |

- **The misses** are mostly single figures ("$30 per hour", "USD 280 per approved task", "450 EUR"). The Actor reads
  only ranges, on purpose: a lone number in a posting is too often a bonus, a budget or a funding round. One more is
  an internship whose pay is called a "stipend", a word the Actor skips because it usually means a relocation or
  learning allowance.
- **The one structured error**: a Recruitee employer typed one range in the pay fields and a different one in the text
  (EUR 5,500 vs 5,800 a month). We deliver what the pay fields say.
- **The fixes this check found:** rupee ranges (`Rs`, `₹`, `INR`), ranges with the currency after the amounts (German
  and Dutch postings), a customer `ARR` band read as pay, and Ashby's `isRemote` flag, which is set on hybrid jobs too
  (522 of OpenAI's 811 postings). `isRemote` now follows Ashby's stated workplace type.

Every sampled job, with its posting link, pay text, our answer and the hand-checked answer, is kept with the Actor's
source (`docs/salary-accuracy-2026-11.json`), next to the scripts that draw, re-run and score the sample. A test fails
the build if this table stops matching that file. Ask in the Issues tab for a copy.

Export as JSON, CSV, Excel or via the API. A summary of every input (which vendor matched, jobs on the board, matched,
delivered, closed, error) is saved to the `SUMMARY` record of the run's key-value store.

#### Closed jobs

With `onlyChanges` on, a job that was on the board at your previous run and is gone now comes back as a short row:

```json
{
    "changeStatus": "closed",
    "platform": "greenhouse",
    "board": "airbnb",
    "company": "Airbnb",
    "jobId": "7123456",
    "title": "Senior Backend Engineer",
    "url": "https://boards.greenhouse.io/airbnb/jobs/7123456",
    "closedAt": "2026-11-05T06:00:03.120Z",
    "scrapedAt": "2026-11-05T06:00:03.120Z"
}
```

`closedAt` is the time your run noticed the job gone (the vendors do not publish a close time). Each closure is
reported once. Closed rows follow your `keywords`, `locations`, `remoteOnly` and `onlyWithSalary` filters, judged on the
details stored when the job was last seen, and they do not count against `maxJobsPerBoard` or `maxJobsTotal`. If a
board answers with an empty list while it had jobs at the last run, the Actor treats that as a failed answer, reports
no closures and keeps its state, so a vendor hiccup does not close a whole board. State saved by earlier versions is
read as before; a job from that old state that has left the board is reported with `title` and `url` as `null` (and, because its details were not stored, only when no keyword, location, remote or salary filter is set).

### Pricing

Pay per event: **$1.20 per 1,000 jobs delivered** ($0.0012 per job), nothing for jobs you filter out, and with
`onlyChanges` nothing for unchanged jobs. **Closed-job rows are free**: they are written without the `job` event.
Each one reports a job you already paid for when it was delivered, so a monitoring run costs only the new and changed
jobs. Looking up a company name or careers link is free too. Apify platform usage is not charged on top. A board of 200 jobs costs about
$0.24 per run.

### Good to know

- The salary comes from the structured fields the employer filled in and, failing that, from a range written in the
  job description (`salarySource` tells you which). The Actor never estimates a salary: a posting that states none
  returns `null`.
- Reading the text costs requests on Greenhouse, whose job list carries no description. After the other filters
  (keywords, locations, remote), jobs with no structured salary are read: up to 5 of them one by one, more than that
  with one extra request for the whole board. Lever, Ashby, Workable, Recruitee and Personio already send the text in their list, so they
  cost nothing extra. If that read fails, a run with `onlyWithSalary` reports the board as an error in `SUMMARY` (instead of silently returning fewer jobs); a run without it still delivers the board with structured salaries only and puts a `warning` on that board in `SUMMARY`.
- **Workable** publishes no structured pay, so its salaries always come from a range written in the description
  (`salarySource: "text"`); a posting that states none returns `null`. Workable's public widget endpoint returns the
  whole board, descriptions included, in **one request per board** (no paging), so it costs one request however many
  jobs the board has. It has no update date or team: change detection compares the title, location, department,
  employment type and description. `isRemote` is `true` for a job the account marks as telecommuting
  (`workplaceType: "remote"`) or whose location or title says Remote. A board is its account name: `hirefellows` for
  `apply.workable.com/hirefellows` (or `hirefellows.workable.com`); `company` is the account's display name.
- **Recruitee** boards are read from the company's public careers API, `https://<company>.recruitee.com/api/offers/`,
  in **one request per board**, descriptions included. Companies that set a salary on the offer return it as numbers
  with `salarySource: "structured"` (a pay of 0 or an empty one counts as none); otherwise a range in the text is read.
  Recruitee does not document the `salary` field of this API, so a change on their side would show as `null` salaries
  rather than errors. `workplaceType` comes from the offer's remote, hybrid and on-site flags; `employmentType` is
  Recruitee's code (for example `fulltime_permanent`). A board is the company's `<company>.recruitee.com` subdomain,
  even when its careers page runs on its own domain (`jobs.channable.com` is `recruitee:channable`).
- **Personio** boards are read from the company's public XML feed, `https://<company>.jobs.personio.de/xml` (the
  `.jobs.personio.com` domain is tried when the company is not on `.de`), in **one request per board**, descriptions
  included, in the language the company set as its default. The feed carries no pay, no update date, no apply link and no
  company name, so `salarySource` is `text` or `null`, `url` and `applyUrl` are the job page, and `company` is the board
  name. `location` lists the main office and any additional ones, separated by `;`; `employmentType` joins the schedule and
  the employment type (`full-time, permanent`). Pay written with a dot as thousands separator (`€55.000`) is not
  read, because it cannot be told from a decimal.
- Greenhouse and Lever expose different amounts of detail: Lever has no last-updated date, so change detection on Lever
  and Ashby compares the posting content. Greenhouse's job list has no department, so the Actor makes one extra small
  request per board to the public departments endpoint and fills `department` (a job in several departments gets the
  first); it does not download the full descriptions for that.
- With `onlyChanges`, `maxJobsPerBoard` is applied after unchanged jobs are removed. Jobs a run does not deliver (over
  the cap, or stopped by `maxJobsTotal`) stay marked as not yet seen and are delivered on the next run.
- Greenhouse pay ranges carry no unit. The interval is read from the range label ("Annual", "Hourly", "Monthly") and,
  failing that, inferred from the size of the number; check `salaryInterval` for `unknown` before relying on it.
- A board that does not exist, a name that matches no vendor, or a host that forbids access in robots.txt is skipped and
  reported in `SUMMARY`; the other boards still run. If every input fails, the run fails.

### Legal and data note

This Actor only reads public job postings that employers publish on purpose, through the public endpoints provided by
Greenhouse, Lever, Ashby, Workable, Recruitee and Personio for job boards. It does not log in, bypass any protection or use proxies, and it checks
robots.txt before every host. Postings contain business information; the Actor does not collect recruiter or applicant
contact details. You are responsible for using the data in line with the law and with the terms of the boards you
read, including data-protection rules where they apply. This Actor is unofficial and not affiliated with, endorsed by
or sponsored by Greenhouse, Lever, Ashby, Workable, Recruitee or Personio.

### Support

Found a board that does not parse or a field that is wrong? Open an issue on the Actor page; include the board token.

# Actor input Schema

## `boards` (type: `array`):

Companies to read: a name (`airbnb`), a careers or board URL (`https://jobs.lever.co/spotify`), or `vendor:token` with vendor greenhouse, lever, ashby, workable, recruitee or personio (`greenhouse:stripe`). A name or careers URL tries the vendors in that order and uses the first board with open jobs; SUMMARY says which. A bare name matches the exact board token, which may be another company: paste its board URL then. Max 200 with presets. Omit boards and presets only for a demo run.

## `presets` (type: `array`):

Ready-made board lists, added to boards. `ai-labs`: AI labs and AI-native companies. `devtools`: developer tools and infrastructure. Each list only holds boards checked live (reading time in the README).

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

Keep a job when its title or department contains any of these words (case-insensitive). Leave empty to keep all jobs.

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

Keep a job when its location contains any of these texts (for example `Remote`, `New York`, `Germany`). Leave empty to keep all locations.

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

Keep only jobs the employer marks or labels as remote.

## `onlyWithSalary` (type: `boolean`):

Keep only jobs with a salary range: the structured one the employer filled in or, failing that, a range such as $150,000 - $200,000 written in the job description. Rows say which in `salarySource`.

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

Add the plain-text job description to each row. Makes the Greenhouse response about ten times larger (Lever, Ashby, Workable, Recruitee and Personio already send the text). The department and the salary are filled either way.

## `onlyChanges` (type: `boolean`):

Compare with the previous run of the same boards and deliver (and charge for) only jobs that are new or changed. Also adds a free row with `changeStatus: closed` for each job that has left the board since the previous run. The first run delivers everything. State is kept in the key-value store `ats-jobs-scraper-state` of your account.

## `maxJobsPerBoard` (type: `integer`):

Stop delivering from a board after this many jobs that match your filters. With "Only new and changed jobs" on, the cap counts only new and changed jobs, so a change far down a large board is still delivered.

## `maxJobsTotal` (type: `integer`):

Stop the run after this many delivered jobs. Use it as a budget cap.

## `requestDelayMs` (type: `integer`):

Minimum wait between two requests to the same host. A longer Crawl-delay in the host's robots.txt always wins (Lever asks for 1 second).

## `maxRetries` (type: `integer`):

Retries with exponential backoff for rate-limit (429), server (5xx) and network errors. Retry-After headers are honored.

## Actor input object example

```json
{
  "boards": [
    "airbnb",
    "lever:spotify",
    "https://jobs.ashbyhq.com/ashby"
  ],
  "keywords": [
    "engineer"
  ],
  "locations": [],
  "remoteOnly": false,
  "onlyWithSalary": false,
  "includeDescription": false,
  "onlyChanges": false,
  "maxJobsPerBoard": 1000,
  "maxJobsTotal": 1000,
  "requestDelayMs": 500,
  "maxRetries": 3
}
```

# 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 = {
    "boards": [
        "airbnb",
        "lever:spotify",
        "https://jobs.ashbyhq.com/ashby"
    ],
    "keywords": [
        "engineer"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("flipvo/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 = {
    "boards": [
        "airbnb",
        "lever:spotify",
        "https://jobs.ashbyhq.com/ashby",
    ],
    "keywords": ["engineer"],
}

# Run the Actor and wait for it to finish
run = client.actor("flipvo/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 '{
  "boards": [
    "airbnb",
    "lever:spotify",
    "https://jobs.ashbyhq.com/ashby"
  ],
  "keywords": [
    "engineer"
  ]
}' |
apify call flipvo/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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