# Company Career Page Jobs Scraper (`fabro.dev/company-career-page-jobs`) Actor

Give it company domains or names (stripe.com, notion.so, Scale AI) and get their live job openings, normalized into one schema. It resolves each company's real job board on its own: Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee and Rippling.

- **URL**: https://apify.com/fabro.dev/company-career-page-jobs.md
- **Developed by:** [Fabrizio Zaffarana](https://apify.com/fabro.dev) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 4 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 jobs

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/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 Career Page Jobs Scraper

Give it a list of **company domains or names** and get back their **live job openings**, normalized
into one schema.

You don't need to know where a company hosts its careers page. Paste `stripe.com`, `notion.so`,
`ramp.com`, and the Actor figures out each company's real job board on its own, then pulls the
openings. Supported boards: Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee, Rippling.

### Why this one is different

Almost every jobs scraper asks you for a board you already had to find yourself
(`greenhouse.io/stripe`, `jobs.lever.co/acme`). That's fine for one company and useless for three
hundred.

**Here the input is the company.** Give it a domain or a name; the resolution is the product.

It also isn't a job-aggregator scrape: the data comes from each company's **own careers board**, so
you get roles as the employer publishes them, including the ones that never reach the big job
sites.

### Supported job boards

Greenhouse · Lever · Ashby · Workable · SmartRecruiters · Recruitee · Rippling

Roughly 670 well-known companies resolve instantly from a built-in registry; everything else is
resolved live. Every company your run resolves is remembered, so repeat runs get faster.

### How many companies actually resolve (measured, not promised)

Read this before you buy. Coverage depends heavily on **who** is in your list, because it depends on
which ATS the company runs. Measured on a 50-company sample (2026-08-10):

| Your list looks like | Returned openings | Board found but empty | No supported board |
|---|---|---|---|
| **Tech / dev-tools** (Vercel, Linear, Supabase, Sentry…) | **72%** | 6% | 22% |
| **Scale-ups & SaaS** (Monzo, Wise, Miro, Personio…) | **75%** | 0% | 25% |
| **Large / non-tech** (Nestlé, Siemens, IKEA, banks…) | **6%** | 13% | 81% |
| Mixed sample overall | 52% | 6% | 42% |

**Large enterprises mostly won't work**, and it isn't a bug: they run Workday, Taleo, iCIMS or
SuccessFactors, which aren't supported here. If your list is Fortune-500 domains, this Actor is the
wrong tool. If it's startups and scale-ups, expect roughly 3 in 4.

Every company that doesn't return openings tells you which of the three cases it is, so you always
know whether to retry, use another source, or accept that the company simply isn't hiring.

### Input

```json
{
  "companies": ["stripe.com", "notion.so", "Scale AI"],
  "maxJobsPerCompany": 100,
  "remoteOnly": false,
  "titleFilter": "engineer"
}
```

| Field | Type | Default | What it does |
|---|---|---|---|
| `companies` | array | — | Domains (`stripe.com`), URLs (`https://acme.com/careers`) or names (`Scale AI`). Up to 500. |
| `maxJobsPerCompany` | integer | 100 | Cap per company. Lower it to control cost on large boards. |
| `remoteOnly` | boolean | false | Keep only roles the board flags remote or hybrid. |
| `titleFilter` | string | — | Case-insensitive substring on the job title. |
| `departmentFilter` | string | — | Case-insensitive substring on the department. |
| `locationFilter` | string | — | Case-insensitive substring on the location text. |
| `includeDescription` | boolean | false | Adds the full plain-text description. Much larger output. |
| `maxConcurrency` | integer | 5 | Companies processed at once. |

**Shortcut:** if you already know the board, force it with `ats:slug`, e.g. `greenhouse:stripe`.

### Output

One row per job. Unknown fields are `null`, never missing, so the shape is safe to load straight
into a table.

```json
{
  "id": "greenhouse_8023928",
  "title": "Account Executive, Bridge",
  "department": null,
  "team": null,
  "location": {
    "raw": "London",
    "city": "London",
    "region": null,
    "country": null,
    "is_remote": false,
    "workplace_type": null
  },
  "employment_type": null,
  "posted_at": "2026-07-30T10:59:38.000Z",
  "updated_at": "2026-08-06T16:10:12.000Z",
  "compensation": null,
  "apply_url": "https://stripe.com/jobs/search?gh_jid=8023928",
  "job_url": "https://stripe.com/jobs/search?gh_jid=8023928",
  "description_text": null,
  "source": "greenhouse",
  "company_input": "stripe.com",
  "company_name": "Stripe",
  "company_ats": "greenhouse",
  "company_slug": "stripe",
  "company_careers_url": "https://job-boards.greenhouse.io/stripe",
  "resolve_confidence": "registry",
  "scraped_at": "2026-08-09T19:27:31.590Z"
}
```

`resolve_confidence` tells you how sure the match is: `registry` and `exact` are solid, `variant` is
corroborated by the board's own company name, `fuzzy` is a loose match worth eyeballing, `provided`
means you passed the board yourself.

Locations are parsed where the board gives enough to parse. When a board lists several sites at once
(`"SF, NYC, SEA"`), you get the first site and `null` for region/country rather than an invented one.

### What it costs you

Billing is per job returned. **Companies that resolve to nothing produce no rows and cost nothing.**
A bad domain in your list is free.

**The run's maximum charge is respected.** If you cap what a run may spend, the Actor stops as soon as
that cap is reached instead of pushing against it: the companies it never got to are listed as
`skipped_charge_limit` in the run summary, so you know exactly where to resume.

### Run summary

Diagnostics never land in your billed dataset. After each run, the **`run-summary`** record in the
key-value store has a per-company breakdown: what resolved, to which board, how many roles the board
actually has, and why anything failed.

```json
{
  "companies_requested": 3,
  "companies_resolved": 1,
  "companies_board_empty": 1,
  "companies_not_resolved": 1,
  "companies_board_error": 0,
  "total_jobs": 2,
  "outcomes": [
    { "input": "stripe.com", "status": "ok", "ats": "greenhouse", "slug": "stripe",
      "confidence": "registry", "jobs_returned": 2, "total_on_board": 412, "truncated": false },
    { "input": "deel.com", "status": "board_empty", "ats": "ashby", "slug": "deel",
      "jobs_returned": 0,
      "error": "A board named \"deel\" exists on Ashby but publishes no openings…" }
  ]
}
```

Each company lands in exactly one state, and the difference is the point:

| `status` | What it means | Worth retrying? |
|---|---|---|
| `ok` | Openings returned. | — |
| `board_empty` | A board with that slug exists but publishes nothing: hiring freeze, listings turned off, or an unrelated board with the same name. | No |
| `not_resolved` | No supported board found. Almost always an ATS outside the seven above. | No |
| `board_error` | The board answered with an error or timed out. | Yes |
| `skipped_charge_limit` | The run hit the maximum charge you set for it before reaching this company, so it was never fetched. | Yes, with a higher cap |

### Good to know

- **A result is never silently empty.** A company that returns no openings always tells you which of
  the three cases above it is, with the boards that were tried.
- **`board_empty` is not proof of identity.** With zero openings there is no company name on the
  board to check against, so we report that a board with that slug exists, not that it is
  definitely that company's.
- **Filters are applied after fetching**, because the boards don't expose them. Setting one pulls the
  whole board, which is slower but complete.
- **`departmentFilter` is the expensive one** on Greenhouse boards, so only set it when you need it.
- Boards with more than 1000 open roles are capped; `truncated: true` tells you when that happened.
- **Location is parsed conservatively: an uncertain field is `null`, never a guess.** A role listed
  for a continent or a bloc ("North America", "EMEA") reports that as the region and leaves `country`
  empty, because a multi-country scope is not one country. A token we can't confirm is a country
  becomes the region instead, so `"Bengaluru, Karnataka"` gives you the region, not a fictional
  country. Where a job lists several sites in one comma-separated string, the first is used as the
  primary; `location.raw` always carries the board's original text.

### Typical uses

Recruiting and sourcing pipelines · sales and market intelligence (who's hiring for what) · tracking
hiring at a portfolio or a competitor set · building a niche job board from real employer boards.

# Actor input Schema

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

One company per line: a domain (stripe.com), a URL (https://www.notion.so) or a name (Scale AI). If you already know the board, you can force it with ats:slug, e.g. greenhouse:stripe.

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

Upper bound of openings returned per company. Lower it to control cost on companies with very large boards.

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

Keep only roles flagged remote or hybrid by the board.

## `titleFilter` (type: `string`):

Case-insensitive substring match on the job title, e.g. "engineer".

## `departmentFilter` (type: `string`):

Case-insensitive substring match on the department. Note: on Greenhouse boards this makes the fetch heavier, so only set it when you need it.

## `locationFilter` (type: `string`):

Case-insensitive substring match on the raw location text, e.g. "Berlin" or "United States".

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

Adds the plain-text job description to every row. Much larger payloads and slower runs, so leave it off unless you need the text.

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

How many companies to process at once. Raise it for long lists; lower it if a board starts rate-limiting.

## Actor input object example

```json
{
  "companies": [
    "stripe.com",
    "notion.so"
  ],
  "maxJobsPerCompany": 100,
  "remoteOnly": false,
  "includeDescription": false,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

One row per live job opening, normalized across every supported board. This is also the billing unit: companies that return no openings add no rows.

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

Per-company breakdown: what resolved, to which board, how many roles that board actually has, and why anything returned nothing. Diagnostics live here rather than in the dataset so they can never inflate a bill.

# 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",
        "notion.so",
        "ramp.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("fabro.dev/company-career-page-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": [
        "stripe.com",
        "notion.so",
        "ramp.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("fabro.dev/company-career-page-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": [
    "stripe.com",
    "notion.so",
    "ramp.com"
  ]
}' |
apify call fabro.dev/company-career-page-jobs --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,fabro.dev/company-career-page-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/a3JXYemKseqglj6cW/builds/gaTdx6oyZfC8P0IsE/openapi.json
