# ATS Jobs Scraper - Career Sites, Greenhouse, Lever, Workday (`tidytools/ats-career-site-jobs`) Actor

Live jobs from company career sites and careers pages: paste career pages, domains or board URLs. Detects 17 ATSs incl. Greenhouse, Lever, Ashby, Workday, SmartRecruiters. New-job alerts. $1/1k jobs.

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

## Pricing

Pay per event

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

### What does ATS Jobs Scraper do?

It returns the **open jobs of any list of companies, live from their own career sites**. Paste career pages, company websites or job board URLs, in any mix: the Actor finds which applicant tracking system (ATS) each company uses and reads that ATS's **public job board feed** — the JSON, XML and RSS feeds these vendors serve so companies can show their jobs on their own websites. Every job comes back in **one normalised format**, whatever the ATS.

- 🏢 **17 ATSs, one schema**: Greenhouse, Lever (US and EU), Ashby, Workday, SmartRecruiters, Workable, Recruitee, Personio, Breezy HR, Teamtailor, BambooHR, Rippling, Pinpoint, Gem, JazzHR, Homerun and Freshteam
- 🔎 **ATS auto-detection**: give it `stripe.com` or `huggingface.co/jobs` and it finds the board — from links and embeds on the page, the site's careers links, a real browser for JavaScript career pages, and finally verified guesses (`stripe.com` → `greenhouse:stripe`, accepted only when the board's company name or job texts match the domain)
- 🧹 **Clean, comparable fields**: `location` (text) plus `locations` (list), ISO `country`, `workplaceType` and `remote`, `employmentType` (`full-time`, `part-time`, `contract`, `internship`…), `postedAt` in ISO format, `jobUrl`, `applyUrl`, and the real company name (read from the board page when the ATS feed has none)
- 💰 **Salary columns**: `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod` from the ATS (Greenhouse pay-transparency ranges included, with every zone in `payRanges`), or read from the pay-transparency text of the posting (`salarySource` says which)
- ⚡ **Live, not a database copy**: every run reads the boards at that moment, so a job posted an hour ago is there, and jobs that closed are gone
- 🆕 **Only new jobs since the last run**: for daily schedules. Unchanged jobs are not output and not charged; closed jobs get a free `closed` row
- 🎯 **Filters**: title keywords (and exclusions), location, department, remote only, posted in the last N days. Filtered-out jobs are free. Title keywords match whole words and their common forms (engineer → Engineering; excluding intern drops Internship but keeps International Sales); use `*` for part of a word (`*entwickler`)
- 🌍 **Optional remote-job sources**: add the Remote OK feed and the Hacker News "Who is hiring?" thread, with attribution and links back to each listing
- 🧾 **ATS detection mode**: one row per company with its ATS, board URL, number of open jobs, remote jobs, top departments and locations, newest posting date. It also names ATSs it can recognise but not read (iCIMS, Taleo, SuccessFactors, Jobvite, Oracle, UKG, Paylocity…)
- 🧩 **Maps back to your input**: every row has `input` and `inputIndex`; companies without a readable board get an error row saying why (not charged). The same job found on two boards is returned once
- 💵 **$1 per 1,000 jobs**, down to $0.80 on Apify's Business plan and higher. No start fee

### Supported job boards

| ATS | Paste any of these | Feed read | Description | Posted date | Salary |
|---|---|---|---|---|---|
| Greenhouse | `job-boards.greenhouse.io/<slug>`, `boards.greenhouse.io/<slug>`, embed URLs, `greenhouse:<slug>` | Job Board API | ✅ | ✅ first published | structured pay ranges when published, else from the posting text |
| Lever | `jobs.lever.co/<slug>`, `jobs.eu.lever.co/<slug>`, `lever:<slug>` | Postings API | ✅ incl. lists | ✅ | ✅ when published |
| Ashby | `jobs.ashbyhq.com/<slug>`, `ashby:<slug>` | Job Posting API | ✅ | ✅ | ✅ when published |
| Workday | `<tenant>.wd5.myworkdayjobs.com/<site>`, `wd1.myworkdaysite.com/.../recruiting/<tenant>/<site>` | the career site's JSON | ✅ (1 extra request per job) | ✅ start date | — |
| SmartRecruiters | `careers.smartrecruiters.com/<Company>`, `smartrecruiters:<Company>` | Posting API | ✅ (1 extra request per job) | ✅ | — |
| Workable | `apply.workable.com/<slug>`, `<slug>.workable.com`, `workable:<slug>` | widget API | ✅ | ✅ | — |
| Recruitee | `<slug>.recruitee.com`, `recruitee:<slug>` | Careers Site API | ✅ | ✅ | ✅ when published |
| Personio | `<slug>.jobs.personio.de` / `.com`, `personio:<slug>` | XML feed | ✅ | ✅ | — |
| Breezy HR | `<slug>.breezy.hr`, `breezy:<slug>` | `/json` feed | — (the feed has none) | ✅ | text |
| Teamtailor | `<slug>.teamtailor.com`, `<slug>.na.teamtailor.com`, a Teamtailor careers site on your own domain, `teamtailor:<slug>` | RSS feed (`/jobs.rss`) | ✅ | ✅ | — |
| BambooHR | `<slug>.bamboohr.com/careers`, `bamboohr:<slug>` | careers-page JSON | ✅ (1 extra request per job) | ✅ with details | text when published |
| Rippling | `ats.rippling.com/<slug>/jobs`, `rippling:<slug>` | the hosted board's JSON | ✅ (1 extra request per job) | ✅ with details | ✅ with details |
| Pinpoint | `<slug>.pinpointhq.com`, `pinpoint:<slug>` | `postings.json` + RSS | ✅ | ✅ | ✅ when published |
| Gem | `jobs.gem.com/<slug>`, `gem:<slug>` | Job Board API | ✅ | ✅ | from the posting text |
| JazzHR | `<slug>.applytojob.com`, `jazzhr:<slug>` | XML job feed | ✅ | ✅ creation time | — |
| Homerun | `<slug>.homerun.co`, `homerun:<slug>` | Atom job feed | ✅ | — (last update only) | text when published |
| Freshteam | `<slug>.freshteam.com/jobs`, `freshteam:<slug>` | careers widget JSON | ✅ | ✅ | — |

A company website or career page (e.g. `linear.app`, `webull.com`, `functionhealth.com`) works too: the board is detected. For any ATS, the salary columns are also filled from the posting's own pay-range text when the ATS has no salary fields (`salarySource: "description"`). Greenhouse boards that publish pay ranges (pay transparency) fill the salary columns from those structured ranges instead (`salarySource: "ats"`, `salarySourceDetail: "greenhouse_pay_transparency"`, every range in `payRanges`).

**Extra sources** (option **Extra sources**, same price per job, same filters):

| Source | What you get | Terms we follow |
|---|---|---|
| Remote OK | the latest remote jobs of the public Remote OK API (99 jobs on 30 September 2026) | Remote OK asks for a link back to the listing and to name Remote OK as the source: every row has `jobUrl` on remoteok.com and an `attribution` line |
| Hacker News "Who is hiring?" | top-level posts of the latest monthly thread, via the official HN API (300 posts in the September 2026 thread) | `jobUrl` links to the HN comment; email addresses in posts are removed |

### Who is it for?

- **Job boards and aggregators**: fill a niche board with fresh jobs from a list of companies every day, with only-new-jobs monitoring
- **Recruiters and talent teams**: watch competitors' and target companies' openings
- **Sales and market research**: hiring signals — who is hiring for which teams and where, which ATS a company uses
- **Compensation research**: published pay ranges by title and company, in separate min/max/currency/period columns
- **Job seekers and career coaches**: one daily list of matching jobs from dream companies
- **AI agents**: "find open ML engineer jobs in Berlin at these 50 startups"

### How much does it cost?

Price per 1,000 by Apify plan:

| Event | Free | Starter | Scale | Business and higher |
|---|---|---|---|---|
| Job | **$1.00** | **$1.00** | **$0.90** | **$0.80** |
| Company ATS detected (detection mode only) | $2.00 | $2.00 | $1.80 | $1.60 |

**No start fee.** Not charged: jobs removed by your filters, unchanged jobs with **Only new jobs**, duplicate jobs found on a second board, `closed` rows, companies without a readable job board, invalid input lines.

For comparison (checked September 2026, Apify Store prices on the Free plan): other ATS job scrapers charge $0.0015 per job (bovi/greenhouse-lever-ashby-job-scraper), $0.002 (fantastic-jobs Greenhouse, Ashby and Workday APIs), $0.004 (jobo.world ATS scrapers; memo23/career-site-ats-jobs-api) and $0.012 (fantastic-jobs/career-site-job-listing-api, a database of jobs collected in advance rather than a live read of your companies).

### Control your cost

- **What is charged:** each job returned (jobs mode) or each company whose ATS was identified (detection mode).
- **Max jobs** caps the whole run (default 10,000 when you call the API without it: at most $10 on the Free plan). **Max jobs per company** caps each company.
- **Filters are applied before charging**: title keywords, location, department, remote only and posted-in-the-last-N-days all reduce what you pay.
- **Only new jobs** makes daily schedules cheap: after the first run, you pay only for jobs that appeared since the previous run.
- At the start, the run logs its worst case (e.g. `Plan: 400 jobs × $0.001 = at most $0.4`) and warns when that is more than your **maximum charge per run**.
- When the maximum charge per run is reached, the run stops, keeps everything found so far, and says so. The `SUMMARY` record then has `status: "LIMIT_REACHED"` and `notProcessed` (up to 100 companies not processed or not finished). With **Only new jobs**, jobs that were not delivered because of a limit are reported as new again on the next run.

**Worked example (real runs, 30 September 2026, Free plan prices):**

| Run | Setting | Jobs returned | Cost |
|---|---|---|---|
| Engineering jobs at 20 AI startups (OpenAI, Anthropic, Perplexity, Cohere…) | titles `engineer`, `developer`, `scientist`; max 20 per company; only new jobs, first run | 329 | **$0.33** |
| Same monitor, run again 10 minutes later | only jobs posted since the previous run | 0 | **$0** |
| Remote jobs posted in the last 24 hours at 15 Greenhouse companies | remote only, posted in the last 1 day | 35 | **$0.035** |
| Which ATS do 17 companies use? | detection mode | 16 detected, 1 not found (free) | **$0.032** |

Without the title filter and caps, the same 20 AI startups had about 3,900 open jobs (about $3.90 for a full first run), so the filter and **Max jobs per company** are what keep the first run at $0.33. After that you pay only for new postings.

### How to use it

1. Enter your **companies**, one per line: job board URLs, career pages, websites or `ats:slug`.
2. Optional: set filters (title, location, department, remote, posted in the last N days) and **Max jobs per company**.
3. Optional: add **Extra sources** (Remote OK, Hacker News "Who is hiring?").
4. Optional: turn on **Only new jobs since the last run** and schedule the Actor daily.
5. Click **Start** and export as CSV, Excel or JSON, or call it from the API. The **Jobs (overview)** table has the columns most people need; **Jobs (all fields)** has everything.

#### Input example

```json
{
    "companies": [
        "https://job-boards.greenhouse.io/airbnb",
        "jobs.lever.co/palantir",
        "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
        "https://career.teamtailor.com",
        "ats.rippling.com/chess/jobs",
        "linear.app",
        "smartrecruiters:BoschGroup"
    ],
    "titleKeywords": ["engineer", "data scientist"],
    "excludeTitleKeywords": ["intern"],
    "locationKeywords": ["US", "Remote"],
    "postedWithinDays": 14,
    "maxJobsPerCompany": 100,
    "descriptionFormat": "text"
}
```

#### Output example (jobs mode)

A real result from `greenhouse:reddit` (description shortened), captured before structured pay was read. The salary was read from the posting's pay-range text; Reddit now publishes Greenhouse pay ranges for most jobs, so such jobs come with `salarySource: "ats"`, `salarySourceDetail: "greenhouse_pay_transparency"` and `payRanges`:

```json
{
    "input": "greenhouse:reddit",
    "inputIndex": 5,
    "success": true,
    "jobKey": "greenhouse:reddit:8147559",
    "jobId": "8147559",
    "title": "Front End Software Engineer, Consumer Engineering",
    "company": "Reddit",
    "department": "Consumer",
    "team": null,
    "location": "Remote - United States",
    "locations": ["Remote - United States"],
    "country": "US",
    "workplaceType": "remote",
    "remote": true,
    "employmentType": null,
    "employmentTypeText": null,
    "salaryMin": 164200,
    "salaryMax": 229900,
    "salaryCurrency": "USD",
    "salaryPeriod": "year",
    "salaryText": "$164,200—$229,900 USD",
    "salarySource": "description",
    "postedAt": "2026-09-17T17:07:34.000Z",
    "postedAtApproximate": false,
    "updatedAt": "2026-09-17T17:07:34.000Z",
    "jobUrl": "https://job-boards.greenhouse.io/reddit/jobs/8147559",
    "applyUrl": "https://job-boards.greenhouse.io/reddit/jobs/8147559",
    "requisitionId": "Pooled",
    "descriptionText": "Reddit is a community of communities. It’s built on shared interests, passion, and trust, and is home to the most open and authentic conversations on …",
    "source": "ats",
    "ats": "greenhouse",
    "atsSlug": "reddit",
    "boardUrl": "https://job-boards.greenhouse.io/reddit",
    "detectionMethod": "input",
    "charged": true,
    "scrapedAt": "2026-09-30T04:52:29.128Z"
}
```

Fields are the same for every ATS; a field the ATS does not publish is `null`. `jobKey` (`ats:slug:jobId`) is unique across boards and stable between runs. `workplaceType` is `remote`, `hybrid`, `onsite` or `null`; `employmentType` is the normalised value and `employmentTypeText` the ATS's own wording. `postedAtApproximate` is `true` when the date comes from Workday's "Posted N Days Ago". `detectionMethod` tells how the board was found: `input`, `career-page`, `careers-link`, `career-page-browser`, `slug-guess` or `source`.

A row from the Hacker News source (other fields as above):

```json
{ "input": "source:hackernews", "jobKey": "hackernews:49524580", "title": "Lead Product Manager, Security", "company": "Wikimedia Foundation", "location": "REMOTE (US + 18 countries)", "workplaceType": "remote", "employmentType": "full-time", "postedAt": "2026-09-01T16:52:15.000Z", "jobUrl": "https://news.ycombinator.com/item?id=49524580", "source": "hackernews", "attribution": "Job post from the Hacker News \"Ask HN: Who is hiring?\" thread (https://news.ycombinator.com), via the official HN API. Link back to jobUrl when you republish it." }
```

With **Only new jobs**, rows also have `changeType: "new"` and `firstSeenAt`, and jobs that disappeared get a free row with `changeType: "closed"`, `firstSeenAt` and `closedAt`.

#### Output example (ATS detection mode)

Real result for `webull.com` (lists shortened):

```json
{
    "input": "webull.com",
    "success": true,
    "company": "Webull",
    "companyWebsite": "https://webull.com",
    "ats": "rippling",
    "atsSupported": true,
    "atsSlug": "webull",
    "boardUrl": "https://ats.rippling.com/webull/jobs",
    "jobCount": 29,
    "remoteJobs": 0,
    "topDepartments": [{ "name": "Finance", "jobs": 8 }, { "name": "Institutional", "jobs": 4 }, { "name": "Clearing Operations", "jobs": 3 }],
    "topLocations": [{ "name": "Saint Petersburg, FL", "jobs": 19 }, { "name": "New York, NY", "jobs": 9 }, { "name": "Toronto, Canada", "jobs": 1 }],
    "detectionMethod": "careers-link",
    "foundOn": "https://www.webull.com/careers",
    "charged": true
}
```

In the same run, `stripe.com` → `greenhouse:stripe` (711 jobs), `functionhealth.com` → `gem:function-health` (33), `ioet.com` → `teamtailor:ioet.na` (5), `youlend.com` → `pinpoint:youlend` (25) and `getflowpath.com` → `freshteam:getflowpath` (3).

#### A company without a readable job board

Real output for `netflix.com` (its career site is not on a supported ATS; `pagesTried` shortened). Not charged:

```json
{
    "input": "netflix.com",
    "success": false,
    "errorType": "not_found",
    "error": "no supported ATS job board found on https://netflix.com or its career links (supported: greenhouse, lever, ashby, workday, smartrecruiters, workable, recruitee, personio, breezy, teamtailor, bamboohr, rippling, pinpoint, gem, jazzhr, homerun, freshteam). Tip: paste the job board URL instead (not charged)",
    "pagesTried": [{ "url": "https://netflix.com", "via": "direct" }, { "url": "https://jobs.netflix.com/jobs", "via": "browser" }, { "url": "greenhouse:netflix", "result": "no match" }],
    "charged": false
}
```

`errorType` is `unsupported` when the company uses an ATS without a public feed (listed in `atsDetected`), `not_found` when no board was found or a board URL does not exist, `no_jobs` when the board was read but has no open jobs right now (or the company id is wrong: SmartRecruiters ids are case-sensitive), `network`/`timeout`/`blocked` when a page or feed could not be loaded (rate limits and server errors are retried with backoff first), and `invalid_input` for lines that are not a company.

The `SUMMARY` record (key-value store) has `status` (`SUCCESS`, `PARTIAL_RESULTS`, `FAILED`, `NO_RESULTS`, `LIMIT_REACHED`), totals (including `duplicatesSkipped`), and `perCompany`: ATS, slug, company, `jobsFound` (open jobs on the board) and `jobsOutput` (after filters) for every company.

### Integrations

- **Schedules**: run daily with **Only new jobs** for a new-jobs feed.
- **API and webhooks**: start runs and read the dataset from any language; use a webhook to get notified when a run finishes.
- **Exports**: CSV, Excel, JSON, XML and HTML from the dataset (the overview view gives a flat table).
- **Apify integrations**: send results to Google Sheets, Slack, Zapier, Make, n8n and others from the Integrations tab.
- **AI agents (MCP)**: connect Apify's MCP server (https://mcp.apify.com?tools=tidytools/ats-career-site-jobs) to Claude, Cursor or any MCP client, then ask e.g. "List open data engineering jobs in Europe at linear.app, notion.so and ramp.com, posted in the last two weeks."

```json
{ "companies": ["linear.app", "notion.so", "ramp.com"], "titleKeywords": ["data"], "locationKeywords": ["Europe", "London", "Berlin"], "postedWithinDays": 14 }
```

### Tips and limitations

- **Pasting the board URL is the most reliable input.** Detection from a website works for most startups and scale-ups; big companies often use career sites on ATSs without a public feed (they get an `unsupported` or `not_found` row, free).
- **Guessed boards** (`detectionMethod: "slug-guess"`) are accepted only when the board has open jobs, its newest job is less than a year old, and its company name or job texts match the domain. Turn **Guess the board** off in Advanced settings if you only want boards linked from the website.
- **Workday** lists at most 2,000 jobs per search. For bigger employers, a single title keyword is sent to Workday as a search, so you still reach the matching jobs.
- **Extra requests per job**: Workday, SmartRecruiters, Rippling and BambooHR need one request per job for descriptions; choose **Job description: None** for faster runs. Rippling and BambooHR then have no posting date (and Rippling no salary) unless **Posted in the last N days** is set.
- **Homerun** feeds have no publication date (only `updatedAt`), so **Posted in the last N days** drops Homerun jobs. **JazzHR** dates are the job's creation time. **Freshteam** feeds have no employment type.
- **Salary from the posting text** is read only when a currency and a plausible range are written (e.g. `$164,200—$229,900`); single amounts and vague text are ignored. In a real run on 92 software engineer jobs from 10 companies, 49 salaries came from the ATS and 35 from the posting text.
- **Greenhouse pay ranges**: when a job has several ranges (location zones such as "Zone 1 (New York…)", or two currencies), `salaryMin`/`salaryMax` span the ranges in the first range's currency and `payRanges` lists each one (`title`, `min`, `max`, `currency`, `interval`). A range without a stated period is read as yearly from 10,000 and hourly up to 200.
- **Career page requests from: Our second server only** sends career pages and job board API requests through our second server (another IP), for boards that refuse data-center networks. Workday searches are POST requests and still go from Apify's network. SUMMARY shows `requestsViaSecondServer`.
- **Hacker News** titles and locations are read from the first line of each post (`Company | Role | Location | …`); posts that do not follow that pattern use their first line or the first role mentioned. Only the latest monthly thread is read.
- **SmartRecruiters** company ids are case-sensitive (`BoschGroup`); an unknown id returns 0 jobs.
- **Only new jobs** remembers jobs per monitor name. Use a different monitor name when you change the company list or filters.
- Job boards change their feeds from time to time; if a board stops working, open an issue with the URL.

### Responsible use

The Actor reads only public job postings from the job feeds that ATS vendors provide for publishing jobs on career sites and job boards, and from public job feeds whose terms allow reuse with attribution. It does not log in, does not collect applicant data and does not extract recruiters' personal contact details (email addresses in Hacker News posts are removed). If you republish jobs, link to the original posting (`jobUrl`), keep the `attribution` of Remote OK and Hacker News rows, and follow the rules of the site where you publish them.

### FAQ

**How fresh are the jobs?** As fresh as the company's board: each run reads it live.

**Why did I get fewer jobs than the board shows?** Check your filters, **Max jobs per company** and **Max jobs**, duplicates removed across boards (`SUMMARY.duplicatesSkipped`), and `SUMMARY.perCompany[].jobsFound`.

**Why is `company` different from the slug?** The name comes from the ATS feed, or from the board page's title when the feed has none (Lever, Ashby, Gem, BambooHR, Freshteam): `jobs.lever.co/palantir` gives "Palantir Technologies". Workday feeds have no company name, so the tenant name is used ("Nvidia").

**Can I run it on a schedule?** Yes. Turn on **Only new jobs since the last run**, set a monitor name, and create a daily schedule in Apify Console; each run returns only the jobs posted since the previous one.

**Which ATSs can't it read?** iCIMS, Taleo, SuccessFactors, Jobvite, Oracle Recruiting, UKG, Paylocity, ADP, Paycom, Dayforce, Zoho Recruit, HiBob, Comeet, JOIN and a few others are recognised (detection mode tells you which one a company uses) but not read, because they publish no public job feed we can use.

**My company's ATS is not supported.** Detection mode tells you which ATS it uses; open an issue and it may be added.

# Actor input Schema

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

Main input (fill this, or `sources`). One company per line: a job board URL (https://job-boards.greenhouse.io/airbnb, https://jobs.lever.co/leverdemo), a career page or website (stripe.com: the board is detected) or ats:slug (greenhouse:airbnb). 17 ATSs: Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Recruitee, Personio, Breezy HR, Teamtailor, BambooHR, Rippling, Pinpoint, Gem, JazzHR, Homerun, Freshteam. Invalid lines and companies without a readable board get a free error row.

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

Jobs mode charges per job returned. Detection mode charges per company whose ATS was identified, and returns no job rows. Values: jobs = Jobs: one row per open job; detect = ATS detection only: one row per company (which ATS, board URL, job count, top departments and locations).

## `sources` (type: `array`):

Add jobs from public job feeds whose terms allow reuse with attribution. Same price per job and same filters as company jobs. Rows have source and attribution; link back to jobUrl when you republish them. Works with or without companies.

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

Stop taking jobs from one company after this many (after filters). 0 = all open jobs.

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

Stop after this many jobs in total. You pay only for jobs returned: 10,000 jobs cost at most $10 on the Free plan.

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

Same price either way. Values: text = Plain text (descriptionText); html = HTML (descriptionHtml); both = Both; none = None (faster; Workday, SmartRecruiters, Rippling and BambooHR need one extra request per job for descriptions).

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

Keep jobs whose title contains at least one of these words or phrases (case-insensitive), e.g. engineer, data scientist. Whole words and their common forms match (engineer finds Engineering, java does not find JavaScript); use \* for part of a word (\*entwickler). Filtered jobs are not charged.

## `excludeTitleKeywords` (type: `array`):

Drop jobs whose title contains any of these, e.g. intern, senior (intern drops Intern and Internship, not International).

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

Keep jobs whose location, other locations or country contain one of these, e.g. London, Germany, US, Remote.

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

Keep jobs whose department or team contains one of these, e.g. Engineering, Sales.

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

Keep jobs marked remote by the ATS or with "remote" in the location.

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

Keep jobs first published in the last N days. 0 = any date. Jobs without a publication date are dropped when this is set (Homerun feeds have no publication date; Workday gives "Posted N Days Ago"; JazzHR dates are the job's creation time).

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

Remember the jobs of every company and return only jobs that were not there on the previous run of the same monitor (the first run returns everything and becomes the baseline). Unchanged jobs are not output and not charged. Ideal for daily schedules.

## `reportClosedJobs` (type: `boolean`):

With only-new-jobs on: add a free row (changeType "closed") for every job that disappeared since the last run.

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

Runs with the same name are compared with each other (letters, digits, dashes). Use different names for different company lists or filters. Default: "default".

## `guessBoards` (type: `boolean`):

If a company website does not link its job board, try boards named after the domain (e.g. stripe.com → greenhouse:stripe) on Greenhouse, Lever, Ashby, Workable, Recruitee, SmartRecruiters, Personio, Teamtailor, Pinpoint, BambooHR, Homerun and Gem, and accept one only when its company name or job texts match the domain.

## `dedupeAcrossBoards` (type: `boolean`):

When the same company, title and location appear on two different boards or sources in one run (e.g. a company's own board and Remote OK), only the first is returned. Duplicates are free.

## `httpVia` (type: `string`):

How company websites are read to find the job board. Job board APIs are read from Apify's network, except with "Our second server only": then career pages and job board API requests go through our second server (a different IP, for boards that refuse data-center networks); Workday searches (POST requests) still go from Apify's network.

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

How many companies are processed at the same time.

## `companyTimeoutSecs` (type: `integer`):

A company that takes longer gets a timeout error row (not charged) so one slow board cannot hold the run.

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

Only used for requests sent from Apify's network. Apify proxy usage is billed to your Apify account. Not needed for the supported job boards.

## Actor input object example

```json
{
  "companies": [
    "https://job-boards.greenhouse.io/airbnb",
    "jobs.lever.co/leverdemo",
    "https://apply.workable.com/huggingface/"
  ],
  "mode": "jobs",
  "maxJobsPerCompany": 10,
  "maxJobs": 10000,
  "descriptionFormat": "text",
  "remoteOnly": false,
  "postedWithinDays": 0,
  "onlyNewJobs": false,
  "reportClosedJobs": true,
  "guessBoards": true,
  "dedupeAcrossBoards": true,
  "httpVia": "auto",
  "maxConcurrency": 5,
  "companyTimeoutSecs": 600
}
```

# Actor output Schema

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

No description

## `full` (type: `string`):

No description

## `companies` (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": [
        "https://job-boards.greenhouse.io/airbnb",
        "jobs.lever.co/leverdemo",
        "https://apply.workable.com/huggingface/"
    ],
    "maxJobsPerCompany": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidytools/ats-career-site-jobs").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "companies": [
        "https://job-boards.greenhouse.io/airbnb",
        "jobs.lever.co/leverdemo",
        "https://apply.workable.com/huggingface/",
    ],
    "maxJobsPerCompany": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("tidytools/ats-career-site-jobs").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "companies": [
    "https://job-boards.greenhouse.io/airbnb",
    "jobs.lever.co/leverdemo",
    "https://apply.workable.com/huggingface/"
  ],
  "maxJobsPerCompany": 10
}' |
apify call tidytools/ats-career-site-jobs --silent --output-dataset

```

## MCP server setup

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

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/k95BFAjcwk9AX61BE/builds/8xCF25zZzkqBmd5aP/openapi.json
