# Company Careers & ATS Job Scraper (`munin/ats-job-scraper`) Actor

Scrape open roles straight from company job boards on Greenhouse, Lever, Ashby and Workday. Charges only for jobs it actually delivers.

- **URL**: https://apify.com/munin/ats-job-scraper.md
- **Developed by:** [munin](https://apify.com/munin) (community)
- **Categories:** Jobs, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$4.00 / 1,000 job delivereds

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

## Company Careers & ATS Job Scraper

Give it a company career page and get back every open role, with the full job description, as structured data.

It talks to the company's own job board — **Greenhouse, Lever, Ashby and Workday** — instead of scraping a job aggregator. That means the data is first-hand: the same postings the company publishes, with their own departments, locations, requisition IDs and apply links, and no duplicates or stale reposts from an aggregator's index.

**You are charged per job delivered.** Companies that fail, boards that return nothing, and jobs removed by your filters cost you nothing.

### What you put in

You do not need to know which ATS a company uses. Any of these work:

| Input | Example |
|---|---|
| Company career page | `https://www.anthropic.com/careers` |
| Greenhouse board | `https://boards.greenhouse.io/stripe` |
| Lever board | `https://jobs.lever.co/spotify` |
| Ashby board | `https://jobs.ashbyhq.com/openai` |
| Workday site | `https://nvidia.wd5.myworkdayjobs.com/en-US/NVIDIAExternalCareerSite` |
| Short form | `greenhouse:stripe`, `workday:nvidia/wd5/NVIDIAExternalCareerSite` |

When you give a plain company page, the Actor reads that page once to find the board embedded in it, and if the page hides it behind JavaScript it falls back to looking the company up on each ATS directly.

### What you get

One row per job:

```json
{
  "platform": "greenhouse",
  "company": "stripe",
  "jobId": "6349832",
  "requisitionId": null,
  "title": "Software Engineer, Payments",
  "url": "https://job-boards.greenhouse.io/stripe/jobs/6349832",
  "applyUrl": "https://job-boards.greenhouse.io/stripe/jobs/6349832#app",
  "location": "San Francisco, New York, Remote (US)",
  "locations": ["San Francisco", "New York", "Remote (US)"],
  "department": "Engineering",
  "team": "Payments",
  "employmentType": "Full time",
  "compensation": "$180,000 - $250,000 USD",
  "isRemote": true,
  "postedAt": "2026-09-12T04:35:19.000Z",
  "updatedAt": "2026-09-15T04:55:55.000Z",
  "descriptionText": "About Stripe\n\nStripe is a financial infrastructure platform...",
  "scrapedAt": "2026-09-19T04:11:07.552Z"
}
```

`descriptionText` is the description as readable plain text (bullet lists kept as `- ` lines). Set **Description format** to `html` for the original markup, or `both` for each.

### Filters

All of them are applied before you are charged.

- **Title keywords** / **Exclude keywords** — matched against the job title. Turn on **Match keywords in descriptions** to match the department, team and full description as well.
- **Locations** — substring match against every location on the posting.
- **Remote only** — uses the board's own remote flag and workplace type, not just the word "remote".
- **Posted within days** — jobs whose board publishes no date are kept rather than silently dropped.
- **Max jobs per company** / **Max jobs in total** — hard stops on volume and spend.

### What it cannot do

- **Only these four ATSes.** SmartRecruiters, Workable, iCIMS, Taleo, SAP SuccessFactors, BambooHR and in-house boards are not supported. A company on one of those returns a failure row naming the URL, and costs you nothing.
- **Workday sites must be public and unauthenticated.** Some tenants gate their board behind a login or a non-standard site path; those return an error row.
- **Compensation is only there when the company publishes it.** In practice that is Ashby boards and US roles covered by pay-transparency laws — roughly one board in seven.
- **Dates are ISO 8601 (UTC).** Posted dates on Workday are approximate: Workday's list only says "Posted 30+ Days Ago"; the exact date is taken from the job detail when the company exposes it, otherwise converted from that relative text.
- **No personal data.** The Actor returns job postings only — no recruiter names, no emails, no candidate data.
- It reads public job board APIs. It does not log in, and it does not bypass anti-bot protection.

### Failure rows

A company that cannot be resolved does not stop the run. It is written to the dataset so you can see exactly what happened:

```json
{ "input": "https://www.hubspot.com/careers", "jobsFound": 0, "success": false, "error": "Could not find a supported job board (Greenhouse, Lever, Ashby or Workday) for this URL." }
```

### Pricing

Pay per event: **you pay for each job delivered to the dataset**, and for nothing else. There is no charge for starting a run, no charge per dataset write, and no charge for companies that fail.

### Tips

- Run it on a schedule with **Posted within days = 1** to get a daily feed of new roles at the companies you care about.
- Feed the output into an LLM for screening — `descriptionText` is already clean text, so you are not paying tokens for HTML.
- A whole Greenhouse or Ashby board comes back in one request, so scraping 50 companies takes seconds. Workday is slower: it serves 20 jobs per page and one request per description.

# Actor input Schema

## `companyUrls` (type: `array`):

Job board URLs (Greenhouse, Lever, Ashby, Workday) or plain company career page URLs. If you give a company page, the Actor looks for the job board embedded in it. You can also use short forms like greenhouse:stripe or workday:nvidia/wd5/NVIDIAExternalCareerSite.

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

Keep only jobs matching any of these words. Leave empty to get every open role.

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

Drop jobs matching any of these words, e.g. intern, contract.

## `searchDescriptions` (type: `boolean`):

By default keywords are matched against the job title only. Turn this on to match the department, team and full description as well.

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

Keep only jobs whose location contains any of these, e.g. London, Germany, New York.

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

Keep only roles flagged as remote by the job board or labelled remote in the location.

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

Keep only jobs posted in the last N days. 0 = no date filter. Jobs whose board does not publish a date are always kept.

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

Turn off for a fast list of titles, locations and links. On Workday this also avoids one extra request per job.

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

Plain text is what you want for spreadsheets and LLM prompts. HTML keeps the original formatting.

## `maxItemsPerCompany` (type: `integer`):

0 = every open role. Use this to sample large boards.

## `maxItems` (type: `integer`):

0 = no limit. A hard stop on how many jobs this run delivers and charges for.

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

How many companies to process at once.

## `useApifyProxy` (type: `boolean`):

Off by default. These job boards serve public APIs and do not need a proxy. Turn it on only if you scrape very large Workday boards and start seeing rate limits.

## Actor input object example

```json
{
  "companyUrls": [
    "https://jobs.ashbyhq.com/openai",
    "https://boards.greenhouse.io/stripe",
    "https://jobs.lever.co/spotify"
  ],
  "keywords": [],
  "excludeKeywords": [],
  "searchDescriptions": false,
  "locations": [],
  "remoteOnly": false,
  "postedWithinDays": 0,
  "includeDescription": true,
  "descriptionFormat": "text",
  "maxItemsPerCompany": 0,
  "maxItems": 50,
  "maxConcurrency": 5,
  "useApifyProxy": false
}
```

# Actor output Schema

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

All scraped roles with location, department, compensation when published, and the job description.

## `jobsCsv` (type: `string`):

Title, company, location, posting date and link. Ready for a spreadsheet or a CRM import.

## `descriptions` (type: `string`):

Job text only, for LLM matching and screening prompts.

# 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 = {
    "companyUrls": [
        "https://jobs.ashbyhq.com/openai",
        "https://boards.greenhouse.io/stripe",
        "https://jobs.lever.co/spotify"
    ],
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("munin/ats-job-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 = {
    "companyUrls": [
        "https://jobs.ashbyhq.com/openai",
        "https://boards.greenhouse.io/stripe",
        "https://jobs.lever.co/spotify",
    ],
    "maxItems": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("munin/ats-job-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 '{
  "companyUrls": [
    "https://jobs.ashbyhq.com/openai",
    "https://boards.greenhouse.io/stripe",
    "https://jobs.lever.co/spotify"
  ],
  "maxItems": 50
}' |
apify call munin/ats-job-scraper --silent --output-dataset

```

## MCP server setup

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