# Company Jobs Scraper — Greenhouse, Lever, Ashby, Workday + more (`kjel/company-jobs-scraper`) Actor

Give company names, domains, or careers-page URLs; get every open job as clean JSON (title, location, remote, apply link, date). Auto-detects 9 ATS — Greenhouse, Lever, Ashby, Workable, Workday & more. Pay only per job returned. Built for hiring signals, recruiting, and AI agents (MCP tool).

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

## Pricing

$1.00 / 1,000 job results

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Company Jobs Scraper — get every open job at any company

Give it a list of **company names, website domains, or careers-page URLs** — it finds each company's job board automatically and returns **every open position as clean, structured data**: title, department, location, remote flag, employment type, direct apply link, and posting date.

No configuration, no per-company setup, no API keys. Paste companies, get jobs.

- ✅ **Detects 9 applicant tracking systems (ATS)** — Greenhouse, Lever, Ashby, Workable, Recruitee, SmartRecruiters, Personio, BambooHR, and Workday
- ✅ **Any input format** — `stripe.com`, `OpenAI`, or `https://jobs.lever.co/spotify` all just work, mixed in one run
- ✅ **Built-in filters** — title keywords, department, remote-only, posted-after date
- ✅ **One normalized schema** across every ATS, so your spreadsheet, CRM, or agent pipeline doesn't care where the jobs came from

### Four things you're not charged for

Job data is easy to sell and hard to trust. Most complaints about scrapers in this category are variations of *"I paid for rows I didn't ask for."* So the billing rules here are deliberately narrow:

1. **Rows your filters excluded.** Filtering happens before billing, not after. There's no "we returned everything and flagged the non-matches" behaviour.
2. **Jobs that aren't really the company you asked for.** Every row carries a `matchConfidence`. If a board can't be independently tied to your company, it's excluded by default and **never billed** — you don't pay to discover that a guess was wrong.
3. **Hybrid roles dressed up as remote.** Some boards set a remote flag on hybrid postings — on one board we tested, 446 of 748. `remoteOnly` filters on the stated `workplaceType`, so hybrid stays out.
4. **Jobs you already saw.** In monitoring mode you're billed for genuinely new postings only — not for re-reading the board.

And the run tells you where every number came from: per company, the `SUMMARY` reports `boardTotalJobs` → `matchedFilters` → `jobs` returned, plus a `truncationReason` if those differ. If a count looks low, you can see exactly why instead of guessing.

### Sample output

```json
{
    "company": "Anthropic",
    "ats": "greenhouse",
    "title": "Engineering Manager, Inference",
    "department": "Engineering",
    "seniority": "manager",
    "location": "San Francisco, CA",
    "workplaceType": "hybrid",
    "remote": false,
    "employmentType": "Full-time",
    "url": "https://job-boards.greenhouse.io/anthropic/jobs/5183044008",
    "publishedAt": "2026-04-09T20:28:23-04:00",
    "scrapedAt": "2026-07-27T14:02:11.000Z",
    "matchConfidence": "verified"
}
```

### What can I use it for?

**Sales prospecting & hiring signals.** A company that is hiring is a company that is growing and buying. Track open roles across your target accounts to time outreach: engineering hires signal new tooling budgets, sales hires signal expansion, a first-ever "Head of X" signals a brand-new budget line.

**Recruiting & talent intelligence.** Monitor which companies are hiring for the roles you place, spot hiring surges, and build outreach lists of companies with live openings — straight from their own job boards, always current.

**Job aggregation & job boards.** Pull normalized postings from hundreds of companies' career pages in one run, on a schedule, without writing a parser per ATS.

**Market & competitor research.** Watch competitors' job boards to see what they're building next — new market entries, new team formations, and headcount shifts show up in job postings months before they show up in the news.

**AI agents & RAG pipelines.** Output is flat, clean JSON — ready for LLM consumption. This Actor works out of the box as an MCP tool, so your agent can answer "who's hiring platform engineers among these 50 companies?" with live data.

### How it works

For each company you provide, the Actor resolves the right job board in three steps:

1. **Direct URL recognition** — if you pasted an ATS or careers URL, it's parsed immediately.
2. **Smart probing** — company names and domains are converted to likely board identifiers and checked against every supported system in parallel.
3. **Careers-page scan** — if probing misses, the Actor visits the company's website and finds the ATS link on its careers page.

Companies whose ATS can't be identified are reported in the run summary (see the `SUMMARY` record in the key-value store) — you always know exactly what was and wasn't covered, per company.

### Input

```json
{
    "companies": ["stripe.com", "OpenAI", "https://jobs.lever.co/spotify"],
    "titleKeywords": ["engineer", "developer"],
    "remoteOnly": true,
    "postedAfter": "2026-07-01"
}
```

| Field | Type | Description |
|---|---|---|
| `companies` | array | Company names, domains, or careers/job-board URLs — mix freely |
| `titleKeywords` | array | Only jobs whose title contains one of these keywords (case-insensitive) |
| `departmentKeywords` | array | Only jobs in matching departments/teams |
| `remoteOnly` | boolean | Only **fully remote** jobs — hybrid roles are excluded, not quietly counted as remote |
| `postedAfter` | string | Only jobs published on/after this date (jobs without a date are kept) |
| `includeDescription` | boolean | Include the full job description (HTML + plain text). Available for Greenhouse, Lever, Ashby, Recruitee; other ATSs return the posting without body text |
| `includeUnconfirmed` | boolean | Also return name-guessed boards we couldn't independently confirm (flagged `probable`, never charged). Off by default |
| `onlyNewJobs` | boolean | **Monitoring mode** — return and charge for only postings not seen on a previous run. Off by default |
| `monitorId` | string | Names the history `onlyNewJobs` compares against, so you can keep several independent monitors |
| `maxJobsPerCompany` | integer | Cap jobs returned per company; `0` = no limit |
| `maxJobs` | integer | Cap total jobs for the whole run — your spend ceiling; the run stops when it's reached. `0` = no limit |

**The cost-effective choice: a flat $1 per 1,000 jobs** — no subscription, no per-company fee, no actor-start fee. **You're only charged for jobs that pass your filters** (filtering happens before billing), and `maxJobs` lets you set a hard ceiling on any run's cost up front.

### Output

One dataset record per open job, normalized to the same shape across every ATS:

| Field | Type | Description |
|---|---|---|
| `company` | string | The company exactly as you passed it in |
| `ats` | string | Source ATS: `greenhouse`, `lever`, `ashby`, `workable`, `recruitee`, `smartrecruiters`, `personio`, `bamboohr`, or `workday` |
| `atsSlug` | string | The company's board identifier on that ATS |
| `jobId` | string | null | Job ID on the ATS |
| `title` | string | null | Job title |
| `department` | string | null | Department |
| `team` | string | null | Team, when the ATS separates it from department |
| `location` | string | null | Location(s) as listed |
| `workplaceType` | string | null | `remote`, `hybrid`, `onsite`, or `null` — the working arrangement as the board states it |
| `remote` | boolean | null | `true` only for fully remote, `false` for hybrid/on-site, `null` not stated |
| `employmentType` | string | null | e.g. `Full-time`, `Contract` |
| `url` | string | null | Direct link to the job posting / apply page |
| `applyUrl` | string | null | Direct link to the application form, where the board exposes one separately (Ashby, Lever) |
| `seniority` | string | null | Inferred from the title: `intern`, `entry`, `senior`, `staff`, `principal`, `lead`, `manager`, `director`, `vp`, `executive` — `null` rather than a guessed default |
| `publishedAt` | string | null | Publish date, ISO 8601 |
| `updatedAt` | string | null | Last-updated date, ISO 8601 (few ATSs) |
| `descriptionHtml` / `descriptionText` | string | null | Full description, only with `includeDescription` and only on ATSs that expose it |
| `scrapedAt` | string | Fetch time, ISO 8601 (always present) |
| `matchConfidence` | string | How the board was tied to the company: `url`, `site`, `verified`, or `probable` (see FAQ) |

Export as JSON, CSV, or Excel from the dataset tab, or fetch via the API. A per-run `SUMMARY` record (which companies resolved, via which ATS, how many jobs, and which came back `not_found`) is stored in the run's key-value store.

### Monitoring mode — get only what's new

Set **`onlyNewJobs: true`** and put the Actor on a schedule: each run returns only the postings that weren't there last time. That turns a company list into a live hiring-signal feed — and because you're billed per returned job, a daily monitor over a big account list costs a fraction of a full re-scrape.

```json
{
    "companies": ["stripe.com", "figma.com", "databricks.com"],
    "onlyNewJobs": true,
    "monitorId": "target-accounts"
}
```

- **First run** has no history, so it returns everything currently open and becomes your baseline. **Later runs return only new postings** (often zero — that's a working monitor, not a broken one).
- **Closed roles age out** of the baseline automatically, so a genuinely re-opened role is reported again.
- **A company that fails to resolve keeps its history**, so a transient outage doesn't cause a flood of false "new" jobs on the next run.
- **You're billed for the new postings only** — not for re-reading the board each run, which is what makes a daily monitor over a big account list cheap.
- The summary reports `closedSinceLastRun` per company, so roles disappearing is itself a signal you can track.
- Use **`monitorId`** to run several independent monitors (e.g. `target-accounts` vs `competitors`) without them interfering.
- Works with every filter, so you can monitor a narrow slice: *"new sales roles at these 40 accounts."*

### For AI agents & MCP

This Actor is a **live jobs API / ATS API** and works out of the box as an [MCP](https://modelcontextprotocol.io) tool — any MCP-capable agent can call it to answer questions like *"which of these accounts posted a sales-leadership role in the last 30 days?"* against real-time data.

The contract is simple and deterministic:

- **Input:** one required array, `companies` (names, domains, or board URLs — mixable). Everything else is an optional filter (`titleKeywords`, `departmentKeywords`, `remoteOnly`, `postedAfter`) or limit (`maxJobs`, `maxJobsPerCompany`). Full JSON Schema is published with the Actor, so tools introspect the parameters automatically.
- **Output:** a flat JSON array, one object per job, always the same fields regardless of source ATS (see the table above) — no per-ATS branching in your prompt or code.
- **Grounding:** every row carries `matchConfidence` so an agent can trust or down-weight a result; unconfirmed name guesses are excluded by default and never billed. For an exact, unambiguous lookup, pass the board URL.
- **Cost is legible:** a flat $1 per 1,000 jobs, charged only for returned jobs after filters, with `maxJobs` as a hard per-call ceiling.

### FAQ

**Which applicant tracking systems are supported?**
Greenhouse, Lever, Ashby, Workable, Recruitee, SmartRecruiters, Personio, BambooHR, and Workday. Together these power the careers pages of most startups and scaleups and a large share of enterprises. One note on Workday: its boards can't be guessed from a company name, so provide the careers URL (e.g. `https://company.wd5.myworkdayjobs.com/External`) or make sure the company links its board from its website. More systems are added based on user requests — open an issue and it goes on the list.

**What if a company isn't on any supported ATS?**
It's reported as `not_found` in the run summary rather than silently skipped, and you aren't charged for it. Companies using fully custom careers pages (most large enterprises) aren't covered by this Actor.

**Can I run it on a schedule?**
Yes — use Apify Schedules to run it daily or weekly and feed the results anywhere via integrations (Google Sheets, webhooks, Make, Zapier, n8n) or the API. Job postings change fast; most users run recurring monitors on a fixed company list.

**Is this legal?**
The Actor only accesses publicly available job postings that companies deliberately publish to attract applicants — no logins, no personal data. Still, always make sure your use case complies with applicable laws and the target sites' terms.

**How fresh is the data?**
It's fetched live from each company's own job board at run time — the same data a visitor to their careers page sees, at the moment you run it.

**How do I know the jobs really belong to the company I asked for — and what am I charged for?**
Every job row carries a `matchConfidence` field, and by default you're only charged for the confirmed tiers:

- `url` — you provided the board URL (exact match, no ambiguity).
- `site` — the board was found linked on the company's own website (strong).
- `verified` — the board's own details match the company. On ATSs that publish a website (Workable, SmartRecruiters) this is a domain match; on ATSs that expose only a company name (Greenhouse, Lever, Ashby) it's a **company-name match** — reliable for distinctive names, but a very generic one-word name can collide with a different company that uses the same word (e.g. two unrelated firms both called "Vector"). Household-name enterprises (Walmart, Microsoft, etc.) mostly run custom career sites this Actor doesn't cover, so a generic-word domain is the main place to double-check.
- `probable` — a name-guessed board we couldn't independently confirm; **skipped and never charged** unless you set `includeUnconfirmed`.

For a generic single-word company, an ambiguous name, or absolute certainty, paste the careers/board URL — that always resolves to exactly the board you gave.

**A company I need isn't detected — what do I do?**
Paste its careers-page or job-board URL directly instead of the bare name/domain (this always wins), and open an issue so detection improves for everyone. Issues are typically answered same day.

***

*This Actor is an independent tool and is not affiliated with or endorsed by Greenhouse, Lever, Ashby, Workable, Recruitee, SmartRecruiters, Personio, BambooHR, or any of the companies whose public job postings it retrieves.*

# Actor input Schema

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

The companies to fetch open jobs for. Each entry can be a company name (e.g. <code>Stripe</code>), a website domain (e.g. <code>stripe.com</code>), or a careers-page / job-board URL (e.g. <code>https://jobs.lever.co/spotify</code>) — you can mix all three in one run. The Actor auto-detects which ATS each company uses. Tip: for a generic one-word company name, or for Workday boards (which can't be guessed from a name), paste the board URL to guarantee an exact match.

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

Optional. Keep only jobs whose title contains at least one of these keywords (case-insensitive substring match, OR semantics), e.g. <code>engineer</code>, <code>sales</code>. Omit or leave empty to return jobs of every title.

## `departmentKeywords` (type: `array`):

Optional. Keep only jobs whose department or team contains at least one of these keywords (case-insensitive, OR semantics). Omit or leave empty to return every department.

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

Optional. If true, keep only <b>fully remote</b> jobs. Hybrid roles are excluded — some boards report hybrid postings with a remote flag set, so this filters on the stated workplace type instead of trusting that flag. Every job also carries a <code>workplaceType</code> field (remote/hybrid/onsite) if you'd rather filter yourself. Default false (all jobs).

## `postedAfter` (type: `string`):

Optional. Keep only jobs published on or after this date, in ISO format <code>YYYY-MM-DD</code> (e.g. <code>2026-07-01</code>). Jobs whose board doesn't expose a publish date are always kept. Omit for no date filter.

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

Optional. If true, add the full job description as <code>descriptionHtml</code> and <code>descriptionText</code> to each job (increases output size significantly). Descriptions are available for Greenhouse, Lever, Ashby, and Recruitee; other ATSs return the posting without body text (these fields will be null). Default false.

## `includeUnconfirmed` (type: `boolean`):

Optional. By default the Actor returns and charges for a company only when the board is confirmed to be theirs (you pasted the URL, the board's own details match, or the link was found on the company's own website). Enable this to ALSO return name-guessed boards that couldn't be confirmed: they are flagged with <code>matchConfidence: "probable"</code>, are never charged for, and may occasionally be a demo or same-name board. For common one-word company names, pasting the careers URL is the reliable path. Default false.

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

Optional. Return — and charge for — only postings that haven't been seen on a previous run of this monitor. Ideal on a schedule: run it daily and get just the day's new openings. The first run has no history, so it returns everything currently open and becomes the baseline. Closed postings drop out of the baseline automatically. Default false (return all open jobs every run).

## `monitorId` (type: `string`):

Optional. Only used when "Only new jobs" is on. Names the history this run compares against, so you can keep several independent monitors (e.g. <code>target-accounts</code> and <code>competitors</code>) without them interfering. Leave as <code>default</code> if you only run one.

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

Optional. Cap the number of jobs returned per company (applied after filters). Use 0 for no per-company limit. Default 0.

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

Optional. Hard cap on the total number of jobs returned across all companies — effectively your spend ceiling, since you are charged per job. The run stops once the cap is reached. Use 0 for no limit. Default 0.

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

Optional. Route requests through a proxy to avoid rate-limiting on very large runs. The target endpoints are public JSON APIs, so small and medium runs don't need this; enable Apify Proxy for big sweeps if you hit throttling. Off by default.

## Actor input object example

```json
{
  "companies": [
    "stripe.com",
    "OpenAI",
    "https://jobs.lever.co/spotify"
  ],
  "titleKeywords": [],
  "departmentKeywords": [],
  "remoteOnly": false,
  "includeDescription": false,
  "includeUnconfirmed": false,
  "onlyNewJobs": false,
  "monitorId": "default",
  "maxJobsPerCompany": 0,
  "maxJobs": 0,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

One record per open job posting, normalized across every supported ATS: company, ats, title, department, team, location, remote, employmentType, url (direct apply link), publishedAt, scrapedAt, and matchConfidence. This is the main output.

## `runSummary` (type: `string`):

Per-company outcome for the run: which ATS and board slug each company resolved to, the match confidence, how many jobs were returned, whether it was billed, whether the board was only partially fetched, and which companies came back not\_found or unconfirmed. Useful for checking coverage without scanning the dataset.

# 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": [
        "stripe.com",
        "OpenAI",
        "https://jobs.lever.co/spotify"
    ],
    "titleKeywords": [],
    "departmentKeywords": [],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("kjel/company-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": [
        "stripe.com",
        "OpenAI",
        "https://jobs.lever.co/spotify",
    ],
    "titleKeywords": [],
    "departmentKeywords": [],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("kjel/company-jobs-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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": [
    "stripe.com",
    "OpenAI",
    "https://jobs.lever.co/spotify"
  ],
  "titleKeywords": [],
  "departmentKeywords": [],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call kjel/company-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=kjel/company-jobs-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/lwWwp7RXP7nt6tgrb/builds/6X0TnCVcZ9xjcM2kR/openapi.json
