# ATS Jobs Scraper: Greenhouse, Lever, Ashby, Workday & More (`tessellware/ats-jobs-scraper`) Actor

Scrape jobs from Greenhouse, Lever, Workday, Ashby, Workable, SmartRecruiters & Recruitee in one schema with salary & remote data. Monitor mode: only new jobs.

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

## Pricing

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

## ATS Jobs Scraper: Greenhouse, Lever, Ashby, Workday & More

Collect job postings from company career pages that run on seven major applicant tracking systems (ATS) and get them in **one clean, unified format**. Paste career page URLs, ATS board URLs, or company names. The Actor detects the ATS, pulls the open jobs, normalizes the fields (including salary where the employer publishes it), and can run in **monitoring mode** to give you only the jobs that are new since your last run.

You are **billed only for jobs written to the dataset**. Jobs removed by your filters, jobs already seen in monitoring mode, duplicates and jobs skipped by your limits are never written and never billed.

Maintained by Tessellware.

### Quick start

1. Open the Actor and paste one or more companies into **Companies**: a board URL such as `https://boards.greenhouse.io/lattice`, a career page URL, or a company name. The prefilled example is small (about 40 jobs) and capped at 100 jobs in total.
2. Optionally add filters (keywords, locations, remote only, posted within N days) and pick a description format.
3. Click **Start**, then open the **Jobs overview** table, or download the dataset as JSON, CSV or Excel. For a recurring feed of new jobs, turn on **Only new jobs** and add a schedule.

### How much does it cost to scrape job postings?

You pay per job written to the dataset (see Pricing). At $2.00 per 1,000 jobs, scanning 50 companies with about 40 open jobs each is about 2,000 jobs, or about $4.00 for the first run. With monitoring mode on and a daily schedule, the following runs write only newly posted jobs, typically a small fraction of that. Set **Max jobs in total** (or the maximum cost per run in the Apify Console) to cap any run.

### What you can do with it

- **Recruiters and sourcers:** build a feed of openings across many employers, filtered by keyword, location, department or remote.
- **Job-board operators and aggregators:** one schema for every source, with stable `jobId`s and canonical `jobUrl` / `applyUrl` links.
- **Sales and RevOps teams:** use hiring as a buying signal. The per-company `COMPANY_SUMMARY` shows how many roles a company has open, in which departments and locations, and when it last posted.
- **Market and labor researchers:** track demand for roles, skills and locations over time, with salary ranges where employers publish them.

### How it works

The Actor reads the public job-board APIs and the public JSON endpoints that power companies' careers pages. No login, no cookies, no proxies and no CAPTCHA bypass. Greenhouse, Lever, Ashby, SmartRecruiters and Recruitee publish job-board APIs; Workday's `/wday/cxs` endpoint and Workable's v3 listing are the JSON endpoints behind their public careers pages. Requests are paced (at most 4 at a time per host), retried with back-off on rate limits and server errors, and identify themselves honestly.

### Supported systems

| ATS | Example input | Notes |
|---|---|---|
| Greenhouse | `https://boards.greenhouse.io/stripe`, `https://job-boards.greenhouse.io/stripe`, embed URLs with `?for=stripe` | Department/team hierarchy, pay-transparency ranges (every pay zone) |
| Lever | `https://jobs.lever.co/palantir`, `https://jobs.eu.lever.co/...` (EU) | Workplace type, salary range when published |
| Ashby | `https://jobs.ashbyhq.com/openai` | Structured compensation (per tier), remote/hybrid flags |
| Workable | `https://apply.workable.com/huggingface`, `https://huggingface.workable.com` | Full descriptions, remote/hybrid |
| SmartRecruiters | `https://careers.smartrecruiters.com/Experian` | Description via one extra request per job |
| Recruitee | `https://bunq.recruitee.com` | Descriptions, requirements, salary when published |
| Workday | `https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite` | Needs the full Workday board URL (tenant + site). Posting date and full description via one extra request per job |

Greenhouse boards hosted under `job-boards.eu.greenhouse.io` are parsed and queried through the standard Greenhouse endpoint. That path has not been verified against a live EU-hosted board, so paste the board URL and check the result.

#### Company names, career pages and bare names

- **Career page URL** (for example `https://example.com/careers`): the Actor fetches that one page (public hosts only, redirects re-checked, at most 3 MB read) and looks for links to supported boards. **At most 3 board links** are taken from a page; the report notes if there were more.
- **Bare name or domain** (`stripe`, `Hugging Face`, `airbnb.com`): the Actor tries sensible slug variants against the supported systems. A name can match boards at several vendors (for example `mercury`). By default **only the single best match is scraped**, chosen by: exact company name where the API gives one, then the most jobs, then the order Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Recruitee. The other matches are listed under `alternatives` in `RESOLUTION_REPORT`. Set **Use every matching board for a bare name** (`scrapeAllMatches`) to scrape all matches that have jobs. Pasting the exact board URL always avoids ambiguity.
- **Workday** cannot be found from a bare name, because its URL contains a company-specific site name. Use the full Workday URL.
- **Duplicates:** the same board given twice (for example `boards.greenhouse.io/figma`, `job-boards.greenhouse.io/figma` and `figma`) is scraped and billed once. Later copies appear in `RESOLUTION_REPORT` as `duplicate of <input>`.

### Input

Only `companies` is required.

```json
{
  "companies": [
    "https://boards.greenhouse.io/lattice",
    "https://jobs.ashbyhq.com/linear",
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"
  ],
  "keywords": ["data engineer", "machine learning"],
  "locations": ["London", "Remote"],
  "descriptionFormat": "markdown",
  "maxJobsPerCompany": 200,
  "maxTotalJobs": 1000
}
```

| Field | What it does |
|---|---|
| `companies` | Career page URLs, ATS board URLs, slugs, names or domains. |
| `scrapeAllMatches` | For bare names that match several systems, scrape every match with jobs instead of the single best one. |
| `keywords` / `excludeKeywords` | Keep jobs whose title contains any keyword / drop jobs whose title contains any exclude word. Turn on `matchDescription` to also search the description. |
| `locations` | Keep jobs where any location contains one of the terms. |
| `departments` | Keep jobs whose department or team contains one of the terms. |
| `remoteOnly` | Keep remote jobs only. If the ATS gives no flag, a job counts as remote when its title or location says "remote". |
| `postedWithinDays` | Keep jobs posted in the last N days. Jobs for which the ATS gives no date are kept. |
| `descriptionFormat` | `none`, `text` (default), `markdown` or `html`. |
| `extractSalaryFromDescription` | Experimental, off by default. Reads an explicit pay range from the description text when the ATS has no structured salary. Labelled `salary.source: "description"`. |
| `fetchJobDetails` | SmartRecruiters and Workday only: read one extra page per job. Only jobs that pass your filters are fetched. SmartRecruiters skips this request automatically when `descriptionFormat` is `none`. Workday needs it for dates and locations. |
| `onlyNewJobs`, `monitorName` | Monitoring mode, see below. |
| `maxJobsPerCompany`, `maxTotalJobs` | Cost control. `maxTotalJobs: 0` means no limit. When the total is reached, the remaining inputs are marked `skipped (limit reached)` in `RESOLUTION_REPORT` and are not resolved or listed. |
| `proxyConfiguration` | Optional. Not needed for normal use. |

**Filter matching** is whole-word and case-insensitive. `AI` matches "AI Engineer" but not "Retail"; `US` matches "Remote - US" but not "Austin"; a multi-word term such as `data engineer` matches as a phrase. Filters run **before** anything is written to the dataset.

### Output

One dataset item per job, in the same shape for every ATS:

```json
{
  "jobId": "8fb1615c-34bf-47c4-a1d1-b7b2f836bbd3",
  "source": "ashby",
  "company": "Openai",
  "companySlug": "openai",
  "title": "Technical Program Manager, Compute Infrastructure",
  "department": "Technical Program Management",
  "team": "Technical Program Management",
  "locations": ["San Francisco"],
  "location": "San Francisco",
  "isRemote": false,
  "workplaceType": "hybrid",
  "employmentType": "full-time",
  "postedAt": "2026-11-05T02:20:02.702Z",
  "updatedAt": null,
  "jobUrl": "https://jobs.ashbyhq.com/openai/8fb1615c-34bf-47c4-a1d1-b7b2f836bbd3",
  "applyUrl": "https://jobs.ashbyhq.com/openai/8fb1615c-34bf-47c4-a1d1-b7b2f836bbd3/application",
  "salary": { "min": 207000, "max": 285000, "currency": "USD", "interval": "year", "raw": "$207K - $285K", "source": "provider" },
  "salaryRanges": [{ "min": 207000, "max": 285000, "currency": "USD", "interval": "year", "raw": "$207K – $285K", "source": "provider" }],
  "description": "ABOUT THE TEAM\n\nThe Compute Infrastructure team runs the GPU fleet ...",
  "scrapedAt": "2026-11-15T12:41:47.918Z"
}
```

- **Nothing is invented.** `postedAt`, `updatedAt`, `workplaceType`, `employmentType`, `department` and `salary` are `null` when the ATS does not provide them.
- **Salary:** `salary` is the first pay range the employer published; `salaryRanges` lists every published range exactly as given (for example one per Greenhouse pay zone or Ashby compensation tier). Ranges are never merged. Many postings have none, which is normal.
- **HTML descriptions are sanitised** to a safe tag allowlist (no scripts, styles or event handlers; only `http`, `https` and `mailto` links). Markdown output is built from the sanitised HTML.
- **Company names:** Greenhouse, SmartRecruiters, Workable and Recruitee return the official name. Lever, Ashby and Workday do not, so for those `company` is derived from the board slug, such as "Openai" for `openai`. `companySlug` is always exact.

#### Where dates come from

| ATS | `postedAt` | `updatedAt` |
|---|---|---|
| Greenhouse | `first_published` | `updated_at` |
| Lever | `createdAt` (when the posting was created in Lever) | not provided |
| Ashby | `publishedAt` | not provided |
| Workable | `published_on`, falling back to `created_at` | not provided |
| SmartRecruiters | `releasedDate` | not provided |
| Recruitee | `published_at`, falling back to `created_at` | `updated_at` |
| Workday | `startDate` from the job page (a calendar date, so the time part is `00:00:00Z`); `null` if details are off | not provided |

#### Company summary (hiring signals)

Each run also saves `COMPANY_SUMMARY` to the default key-value store, one entry per company:

```json
[
  {
    "company": "Stripe", "source": "greenhouse", "totalJobs": 785, "listingComplete": true,
    "matchingJobs": 785, "outputJobs": 25, "remoteJobs": 0,
    "departments": { "GEO Sales (EMEA) (Planning Group)": 58, "Solutions Architecture (Planning Group)": 43 },
    "locations": { "Dublin": 59, "Singapore": 56 },
    "newestPostedAt": "2026-11-13T21:14:55.000Z", "oldestPostedAt": "2023-11-01T17:50:50.000Z"
  },
  {
    "company": "Experian", "source": "smartrecruiters", "totalJobs": 389, "listingComplete": false,
    "matchingJobs": 100, "outputJobs": 25, "remoteJobs": 7,
    "departments": { "Technology": 25, "Product Management": 17 },
    "locations": { "Hyderabad, , India": 18 },
    "newestPostedAt": "2026-11-13T22:12:32.217Z", "oldestPostedAt": "2026-11-06T03:22:25.469Z"
  }
]
```

Example from a run capped at 25 jobs per company. `outputJobs` is the number written to the dataset; `totalJobs` is what the board lists. `listingComplete: false` means the Actor stopped listing early, so `matchingJobs`, departments and locations describe only the part it read. This applies to SmartRecruiters and Workday, where the Actor reads the list page by page and stops at your per-company cap (when no filters are set), and to a Workday board that hit its roughly 2,000-result window or was searched by keyword (one search per keyword). Greenhouse, Lever, Ashby, Workable and Recruitee return the whole board in one response, so their summary is always complete.

#### Resolution report

`RESOLUTION_REPORT` lists every input with its status (`resolved`, `partial`, `duplicate`, `skipped` or `failed`), how it was detected (`url`, `page` or `probe`), the matched ATS and slug, job counts, alternatives for ambiguous names, and a clear reason for failures. The run's status message also names the inputs that could not be resolved. One bad input never fails the run.

### Monitoring mode: only new jobs

Set `onlyNewJobs: true` and schedule the Actor (for example daily). The first run outputs all matching jobs and remembers them. Every later run outputs **only jobs that were not seen before**.

- Seen jobs are stored in a **named key-value store** `ats-monitor-<name>-<hash>` in your account, so they survive between runs. Use a different `monitorName` per watch list; names that differ only by case or punctuation get separate stores.
- State is saved after every batch, so a run that stops or restarts does not lose what it already delivered.
- A job that disappears from a board is forgotten, so it counts as new if it is posted again. A board that comes back empty (or with less than half of the remembered jobs) never wipes the memory.
- Jobs skipped because of `maxJobsPerCompany` or a spending limit are not marked as seen and show up in the next run.
- A run with **0 new jobs is a successful run**, and its status message says so.
- Independently of monitoring mode, each run remembers what it already delivered, so if the platform restarts it mid-run (for example a server migration), jobs already delivered are not written or billed again, except, at most, the one batch being written at the moment of a hard crash. A write-ahead marker and a read of the dataset tail are used to recover even that batch where possible; if the dataset cannot be read, nothing is assumed delivered.

### Pricing

**$2.00 per 1,000 jobs** ($0.002 per job) on Apify's Free and Bronze discount tiers; Silver $1.60 / 1,000; Gold $1.20 / 1,000.

You pay per job written to the dataset (pay-per-event). Apify's standard start charge also applies: $0.00005 per run for up to 1 GB of memory, plus $0.00005 for each additional GB (the default 512 MB run costs $0.00005). Apify Store discount tiers are based on your total monthly spend on Apify. Set a maximum cost per run in the Apify Console. The Actor stops cleanly when that limit is reached and keeps everything it has already delivered.

### Integrations

Everything on Apify works with this Actor: run it from the **API** or the client libraries, put it on a **schedule**, trigger **webhooks** when a run finishes, and connect the dataset to **Make**, **Zapier**, **n8n**, Google Sheets and other integrations. In monitoring mode, a schedule plus a webhook gives you a "new jobs" notification pipeline.

### Use it from AI agents (MCP)

AI agents and assistants can find and run this Actor through the [Apify MCP server](https://mcp.apify.com) (`search-actors`, then `call-actor`), for example to answer "which companies opened new ML roles this week?". The input is plain JSON (`companies`, `keywords`, `locations`, `onlyNewJobs`), and the output is one flat record per job, so it is easy for an agent to use. The Actor runs with Apify's limited permissions.

### Other Actors from Tessellware

- [Bulk Domain Checker](https://apify.com/tessellware/bulk-domain-checker): availability, expiry date and registrar (RDAP/WHOIS), DNS records and an SPF/DMARC email-authentication grade for a list of domains. Pairs well with this Actor when you turn hiring companies into a clean lead list.

### FAQ

**Is it legal to scrape job postings?** This Actor reads only job postings that employers publish openly: the public job-board APIs and careers-page JSON endpoints behind their careers pages. It does not log in, bypass access controls or read anything behind authentication. You are responsible for complying with the terms of the sites you use it on and with applicable law, including rules on the use of personal data and on how you reuse job postings. If in doubt, ask a lawyer about your specific use.

**How do I find out which ATS a company uses?** Often you do not have to: paste the careers page URL, or the company name or domain, and the Actor looks for the ATS itself. If you know the address of the job board (it usually contains `greenhouse.io`, `lever.co`, `ashbyhq.com`, `workable.com`, `smartrecruiters.com`, `recruitee.com` or `myworkdayjobs.com`), paste that for the most reliable result. Click "Apply" on a job and look at the address you land on.

**A company was not found. What now?** Check `RESOLUTION_REPORT` for the reason, then paste the exact job-board URL. Companies on other systems, or with a fully custom careers site, are not supported yet.

**Are there limits?** Workday returns at most about 2,000 results per search, so for very large employers add `keywords`; the Actor then runs one search per keyword. Unknown fields stay `null`.

**Why do SmartRecruiters and Workday runs take longer?** Their list pages carry no description or posting date, so the Actor reads one extra page per job, at most 4 at a time, and only for jobs that pass your filters. Use `descriptionFormat: none` (SmartRecruiters) or `fetchJobDetails: false` for a faster, lighter run.

**What happens when some companies fail?** The run still succeeds with the jobs it got and tells you which inputs failed. If **no** input can be resolved, the run fails with a clear message instead of finishing silently with an empty dataset.

**Do I need proxies?** No.

# Actor input Schema

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

One entry per company. Accepts ATS board URLs (e.g. https://boards.greenhouse.io/stripe, https://jobs.lever.co/palantir, https://jobs.ashbyhq.com/openai, https://apply.workable.com/huggingface, https://careers.smartrecruiters.com/Experian, https://bunq.recruitee.com, https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite), company career page URLs that link to one of these boards (at most 3 board links are taken from a page), or bare slugs, names and domains (e.g. `stripe`, `Hugging Face`, `airbnb.com`). A bare name that matches several systems uses the single best match; see RESOLUTION_REPORT for alternatives. Workday boards need the full Workday URL. The prefilled example is small (about 40 jobs).

## `scrapeAllMatches` (type: `boolean`):

A bare name such as `mercury` can match boards at several ATS vendors. By default only the single best match is scraped (exact company name, then most jobs, then Greenhouse > Lever > Ashby > Workday > SmartRecruiters > Workable > Recruitee). Turn this on to scrape every matching board that has jobs. Pasting the exact board URL always avoids ambiguity.

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

Only keep jobs whose title contains at least one of these words or phrases. Whole-word, case-insensitive: `AI` matches 'AI Engineer' but not 'Retail'; `data engineer` matches that exact phrase. Leave empty to keep all jobs.

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

Drop jobs whose title contains any of these words or phrases (whole-word, case-insensitive; e.g. intern, director).

## `matchDescription` (type: `boolean`):

When on, Keywords and Exclude keywords are also checked against the job description, not only the title.

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

Keep jobs where any listed location contains one of these words or phrases (whole-word, case-insensitive: `US` matches 'Remote - US' but not 'Austin'; `London`, `Germany`, `Remote`).

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

Keep jobs whose department or team contains one of these words or phrases (whole-word, case-insensitive; e.g. `Engineering`, `Sales`).

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

Keep only jobs flagged as remote by the ATS. If the ATS gives no flag, a job counts as remote when its title or location says 'remote'.

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

Keep jobs published in the last N days. 0 disables the filter. Jobs with no publication date from the ATS are kept.

## `descriptionFormat` (type: `string`):

Format of the `description` field. Use `none` for the smallest, fastest output.

## `extractSalaryFromDescription` (type: `boolean`):

When the ATS provides no structured salary, look for an explicit pay range such as 'Salary: $120,000 - $150,000' in the description. These values are labelled `salary.source = "description"`. Off by default.

## `fetchJobDetails` (type: `boolean`):

SmartRecruiters and Workday list pages do not include descriptions, posting dates or all locations; the Actor reads one extra page per job (only for jobs that pass your filters). Workday always needs it for dates and locations. For SmartRecruiters, the extra request is skipped automatically when Description format is `none`. Turn this off for a faster, lighter run with fewer fields.

## `onlyNewJobs` (type: `boolean`):

Outputs only jobs not seen in a previous run with the same Monitor name. The first run outputs everything and remembers it. Schedule the Actor to get a feed of newly posted jobs. A run with 0 new jobs is a normal success.

## `monitorName` (type: `string`):

Name of this monitor. Jobs seen are remembered in a key-value store named `ats-monitor-<name>`. Use different names for different watch lists.

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

Upper limit of jobs output per company, for cost control.

## `maxTotalJobs` (type: `integer`):

Stop after this many jobs across all companies, to cap cost. 0 means no limit. The prefilled value keeps a first test run cheap.

## `proxyConfiguration` (type: `object`):

Not needed: the Actor reads public job-board APIs directly. Only set a proxy if your own network policy requires it.

## Actor input object example

```json
{
  "companies": [
    "https://boards.greenhouse.io/lattice",
    "https://jobs.ashbyhq.com/linear",
    "https://apply.workable.com/huggingface"
  ],
  "scrapeAllMatches": false,
  "matchDescription": false,
  "remoteOnly": false,
  "postedWithinDays": 0,
  "descriptionFormat": "text",
  "extractSalaryFromDescription": false,
  "fetchJobDetails": true,
  "onlyNewJobs": false,
  "monitorName": "default",
  "maxJobsPerCompany": 1000,
  "maxTotalJobs": 100
}
```

# Actor output Schema

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

One item per job posting: jobId, source, company, title, department, locations, isRemote, workplaceType, employmentType, postedAt, jobUrl, applyUrl, salary and description.

## `companySummary` (type: `string`):

One entry per company: total and matching jobs, jobs output, remote jobs, counts by department and location, newest and oldest posting dates.

## `resolutionReport` (type: `string`):

Every input with its status (resolved, partial, duplicate, skipped or failed), the detected ATS and board, job counts and the reason for any failure.

## `runStats` (type: `string`):

Totals for the run: jobs output, companies, duration, whether the run stopped early and why.

# 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://boards.greenhouse.io/lattice",
        "https://jobs.ashbyhq.com/linear",
        "https://apply.workable.com/huggingface"
    ],
    "maxTotalJobs": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("tessellware/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 = {
    "companies": [
        "https://boards.greenhouse.io/lattice",
        "https://jobs.ashbyhq.com/linear",
        "https://apply.workable.com/huggingface",
    ],
    "maxTotalJobs": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("tessellware/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 '{
  "companies": [
    "https://boards.greenhouse.io/lattice",
    "https://jobs.ashbyhq.com/linear",
    "https://apply.workable.com/huggingface"
  ],
  "maxTotalJobs": 100
}' |
apify call tessellware/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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