# Company Career Page Jobs Scraper (`frank_conifer_af4/company-careers-jobs-scraper`) Actor

Give it company domains or careers URLs: it detects the ATS (Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Recruitee, Personio) and returns every open job in one normalized schema with salary, remote flag and dates, plus an only-new-jobs mode for monitoring.

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

## Pricing

from $1.50 / 1,000 job listing returneds

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 Career Page Jobs Scraper

Paste company domains or careers page URLs and get every open job in one clean, normalized table, whatever applicant tracking system (ATS) the company uses.

### What does Company Career Page Jobs Scraper do?

You give it a list of companies: a domain like `linear.app`, a careers page like `https://acme.com/careers`, or a direct job board URL. For each company it finds the ATS behind the careers page (Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Recruitee or Personio) and reads the jobs from that ATS's public job-board feed. Every job comes back in the same schema: title, department, locations, remote/hybrid/on-site, employment type, salary range, posted date, job and apply URLs, and the description. Turn on **Only new jobs** and schedule it, and each run returns just the jobs that appeared since the last run, with a webhook ping when there is something new.

### Why use it

- **One schema across eight ATSs.** No more separate scrapers per ATS with different field names. A Workday job and a Greenhouse job look the same in your spreadsheet, CRM or database.
- **You don't need to know the ATS.** Give it a domain and it works out where the jobs live. The detection is explained per company (`detectionMethod` plus the evidence in the run summary), so you can see why a board was picked.
- **Guards against wrong boards.** A domain guess like `example.com` → board `example` is only accepted when the board's website matches the domain or the job ads mention it. Lookalike boards are rejected and listed in the notes.
- **Salary where it exists.** Structured pay ranges from Ashby, Lever and Recruitee (and Greenhouse pay ranges when a company publishes them) are used first. Otherwise US/EU pay-transparency sentences ("The base salary range is $136,000 USD - $212,750 USD") are parsed conservatively. Stipends, bonuses and funding amounts are ignored.
- **Built for monitoring.** Only-new-jobs mode keeps the job IDs per company between runs. It reports new and removed jobs per company and can POST a summary to Slack or any webhook.
- **Cheap and fair.** You pay per job row only. Companies without an ATS, empty boards, failures and filtered-out jobs are free.
- **A hiring signal for outbound.** A company that is hiring for sales, engineering or a new region is a strong buying signal. Feed the companies with new jobs straight into our **B2B Outreach Route Finder** Actor to get the best contact route for each one.

### Supported ATSs

| ATS | Example input | How jobs are read | Notes |
|---|---|---|---|
| Greenhouse | `vercel.com`, `boards.greenhouse.io/vercel`, `job-boards.greenhouse.io/vercel` | Public board API, one request per company | Descriptions included; pay ranges when the company publishes them |
| Lever | `spotify.com`, `jobs.lever.co/zoox`, `jobs.eu.lever.co/x` | Public postings API, one request | Structured `salaryRange` when present; workplace type |
| Ashby | `linear.app`, `jobs.ashbyhq.com/openai` | Public job-board API with compensation | Structured compensation; company website used for verification |
| Workday | `https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite`, any `*.myworkdayjobs.com` or `myworkdaysite.com` URL | Public CXS JSON API, 20 jobs per page | Descriptions need one request per job (toggle **Include job description**) |
| SmartRecruiters | `jobs.smartrecruiters.com/Ubisoft2` | Public postings API, 100 jobs per page | Descriptions need one request per job |
| Workable | `huggingface.co`, `apply.workable.com/huggingface` | Public widget and board APIs | Descriptions included |
| Recruitee | `channable.recruitee.com`, or a Recruitee careers site on the company's own domain (`https://jobs.channable.com`) | Public offers API | Structured salary when published |
| Personio | `ottonova.jobs.personio.de`, `ottonova.de` | Public XML job feed | German-only ads are filled from the German feed |

### How ATS detection works

For each input the Actor tries the cheapest method first and records which one worked in `detectionMethod`:

1. **`direct_ats_url`**: you gave a board, job or API URL of a supported ATS. It is parsed directly, including the Workday tenant, data center (`wd1`...`wd12`) and site.
2. **`careers_page_scan`** (for careers URLs): the page you gave is scanned for ATS links, iframes, embed scripts and `?gh_jid=` / `?ashby_jid=` job links.
3. **`domain_slug_guess`**: the company name is derived from the domain (`linear.app` → `linear`, plus a hyphen-free variant and a variant without prefixes like `get`/`try` or suffixes like `hq`/`app`). It is tried on Greenhouse, Lever and Ashby. A guess is accepted only if the board has jobs **and** passes a sanity check: the board's own website is the domain, or the job ads mention the domain. When only the board's name matches, the guess is accepted only if the company homepage confirms that name and does not redirect elsewhere.
4. **`homepage_scan` / `careers_page_scan`**: the homepage and likely careers pages (links containing careers, jobs, join us, karriere and so on, plus `/careers`, `/jobs`, `/company/careers`) are scanned for ATS links, embeds and job-ID parameters. Up to 6 pages per company are scanned. Recruitee careers sites on a custom domain and Personio widgets are recognized too.

Example from a real run: `ramp.network` → the Ashby board `ramp` was **rejected** because it belongs to ramp.com, and the company's own Greenhouse board `rampnetwork` was then found on its `/join-us` page. For `example.com`, the unrelated Greenhouse board `example` ("Democorp") was rejected.

### What data you get

One row per job (`type: "job"`), plus one uncharged status row per company where no jobs could be returned (`type: "company_status"`).

| Field | Description |
|---|---|
| `type` | `job` or `company_status` |
| `company`, `companyDomain` | Company name (from the ATS when available) and registrable domain |
| `ats`, `atsBoardUrl` | `greenhouse`, `lever`, `ashby`, `workday`, `smartrecruiters`, `workable`, `recruitee` or `personio`, and the board URL |
| `detectionMethod` | `direct_ats_url`, `domain_slug_guess`, `careers_page_scan` or `homepage_scan` |
| `jobId` | The ATS job ID (stable between runs, used for only-new-jobs mode) |
| `title`, `department`, `team` | As published on the board |
| `locations[]` | All locations of the job |
| `isRemote`, `workplaceType` | `remote`, `hybrid`, `onsite` or `null` when the ATS doesn't say |
| `employmentType` | `full_time`, `part_time`, `contract`, `internship`, `temporary`, `permanent`... |
| `salary` | `{ min, max, currency, interval, raw, source }`. `interval` is `year`, `month`, `week`, `day` or `hour`. `source` is `ats` (structured field) or `description` (parsed pay-transparency text). `null` when no pay is published |
| `postedAt`, `updatedAt` | ISO timestamps (Workday without descriptions: day precision from "Posted 3 Days Ago") |
| `jobUrl`, `applyUrl` | Job page and application link |
| `descriptionText`, `descriptionHtml` | Plain-text description (default on); HTML only when **Also include description HTML** is on |
| `isNew` | `true` in only-new-jobs mode, `null` otherwise |
| `input`, `scrapedAt` | The input line this row came from, and the time of the scrape |
| `status`, `message`, `notes` | Status rows only: `no_ats_detected`, `no_jobs` or `failed`, with an explanation and rejected guesses |

The run's key-value store also has a `SUMMARY` record with one entry per company: ATS, board, detection method and evidence, jobs on the board, jobs matched and pushed, `newJobsCount` and `removedJobsCount`.

### How to use

1. Paste your companies into **Companies**, one per line: domains, careers pages or ATS board URLs.
2. Set **Max jobs per company**. It caps both the result size and the cost per company.
3. Optionally add filters: title keywords, excluded title keywords, locations, remote only, departments, posted within N days. Filtered-out jobs are not charged.
4. Keep **Include job description** on for full text and salary parsing. Turn it off to make Workday and SmartRecruiters boards much faster.
5. Click **Start**, then export the **Jobs** view as CSV, Excel or JSON.

```json
{
  "companies": [
    "vercel.com",
    "https://linear.app/careers",
    "jobs.lever.co/zoox",
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite",
    "huggingface.co",
    "ottonova.de"
  ],
  "maxJobsPerCompany": 50,
  "titleKeywords": ["engineer", "sales"],
  "excludeTitleKeywords": ["intern"],
  "locations": ["United States", "Remote"],
  "postedWithinDays": 30,
  "includeDescription": true,
  "onlyNewJobs": false
}
```

### Output example

Real rows from a local test run (descriptions trimmed):

```json
{
  "type": "job",
  "company": "Vercel",
  "companyDomain": "vercel.com",
  "ats": "greenhouse",
  "atsBoardUrl": "https://job-boards.greenhouse.io/vercel",
  "jobId": "5999792004",
  "title": "Account Executive, Majors",
  "department": "Account Executive",
  "team": null,
  "locations": ["Hybrid - London", "London, England, United Kingdom"],
  "isRemote": false,
  "workplaceType": "hybrid",
  "employmentType": null,
  "salary": {
    "min": 200000, "max": 270000, "currency": "GBP", "interval": "year",
    "raw": "The London based pay range for this role is 200,000 - 270,000 GBP OTE.",
    "source": "description"
  },
  "postedAt": "2026-05-21T13:26:09.000Z",
  "updatedAt": "2026-09-24T21:12:05.000Z",
  "jobUrl": "https://job-boards.greenhouse.io/vercel/jobs/5999792004",
  "applyUrl": "https://boards.greenhouse.io/embed/job_app?for=vercel&token=5999792004",
  "descriptionText": "About Vercel:\n\nVercel is the agentic infrastructure company, freeing people and agents to ship what's next...",
  "isNew": null,
  "detectionMethod": "domain_slug_guess",
  "input": "vercel.com",
  "scrapedAt": "2026-10-16T10:37:23.997Z"
}
```

```json
{
  "type": "job",
  "company": "Channable",
  "companyDomain": "channable.com",
  "ats": "recruitee",
  "atsBoardUrl": "https://jobs.channable.com/",
  "jobId": "2773400",
  "title": "Talent Acquisition Partner",
  "department": "Operations",
  "locations": ["Utrecht, Netherlands"],
  "isRemote": false,
  "workplaceType": "hybrid",
  "employmentType": "full_time",
  "salary": { "min": 4250, "max": 5000, "currency": "EUR", "interval": "month", "raw": null, "source": "ats" },
  "postedAt": "2026-10-08T08:03:01.000Z",
  "jobUrl": "https://jobs.channable.com/o/talent-acquisition-partner",
  "applyUrl": "https://jobs.channable.com/o/talent-acquisition-partner/c/new",
  "detectionMethod": "careers_page_scan"
}
```

A company without a detectable ATS gets a free status row:

```json
{
  "type": "company_status",
  "status": "no_ats_detected",
  "message": "No supported ATS found via domain guesses or 5 scanned page(s)",
  "input": "example.com",
  "companyDomain": "example.com",
  "notes": ["Rejected greenhouse board \"example\": nothing links board \"Democorp\" to example.com"]
}
```

### Monitoring new jobs (schedule + webhook)

1. Turn on **Only new jobs since the last run** and pick a **Snapshot store name** for this watchlist, for example `sales-targets-q4`.
2. Run it once. The first run returns every current job: this is your baseline.
3. Add a **Schedule** in Apify Console (daily or weekly) with the same input.
4. Optional: paste a Slack incoming-webhook URL (or a Zapier/Make/n8n URL) into **Webhook URL for new jobs**. After each run with at least one new job, the Actor POSTs a JSON summary. It includes a Slack-ready `text` line per company, `totals`, and up to 50 new jobs per company with title, URL, locations and salary.

Each later run outputs and charges only jobs whose ID was not seen before. The `SUMMARY` record and the run's status message also report `removedJobsCount` per company (jobs that disappeared since the previous run, often a sign the role was filled). Use separate store names for separate watchlists. Jobs that were new but not delivered because of the cap or a spending limit are kept unseen, so they show up in the next run.

### Pricing

**$0.0015 per job row** (event `job-listing`), which is $1.50 per 1,000 jobs. Per-1,000-job prices for career-site job data commonly range from about $2 to $12, so this sits at the low end.

| Run | Jobs returned | Cost |
|---|---|---|
| 10 companies × 50 jobs | 500 | $0.75 |
| 100 companies × 30 jobs | 3,000 | $4.50 |
| 1,000 companies, 20 jobs each after filters | 20,000 | $30.00 |
| Weekly monitoring of 200 companies, about 3 new jobs each | 600 per week | $0.90 per week (after the baseline run) |

**Not charged:** company status rows (no ATS detected, empty board, failed or invalid input), jobs removed by your filters, jobs already seen in only-new-jobs mode, duplicate inputs, and the webhook. **Max jobs per company** and the filters let you control spend precisely. If you set a maximum cost per run, the Actor stops cleanly when it is reached and records that in the summary.

### Integrations / scheduling

- **Schedules:** run daily or weekly with only-new-jobs mode for a hiring-signal feed.
- **Webhooks:** the built-in webhook posts new jobs to Slack, Zapier, Make or n8n. Apify's own run webhooks can trigger any downstream job when a run finishes.
- **API:** start runs and read the dataset from any language with the Apify API or the JavaScript and Python clients. Use `?view=overview` for the compact table.
- **MCP and AI agents:** the Actor can be called as a tool through the Apify MCP server, so an agent can ask "which of these 50 companies are hiring data engineers?"
- **B2B Outreach Route Finder:** pass `companyDomain` from companies with new relevant jobs to that Actor to find the best sales inbox, form or phone line for each.

### FAQ & limitations

**Who is this for?** Recruiters and sourcers tracking target employers, sales teams using hiring as a buying signal, job boards and aggregators that need clean feeds from company boards, and market researchers following hiring trends by role, location or pay.

**Why was no ATS detected for a company?** The company may use an ATS that is not supported yet (iCIMS, SuccessFactors, Teamtailor, JOIN and others), host jobs on its own site, or load the job widget with JavaScript that hides the board link. If you know the board URL, paste it directly.

**Why is a domain guess sometimes rejected?** Generic names collide: the Ashby board `ramp` belongs to ramp.com, not to ramp.network. A guess is only accepted when the board and the domain are clearly linked. Rejected guesses are listed in the row's `notes`, and you can paste the board URL directly if it is the right one.

**Workday.** Results are paged 20 at a time until **Max jobs per company** matching postings are collected (with filters or only-new-jobs mode, up to 5 times that number of postings are scanned, at least 500 and at most 2,000 per company). Departments are not exposed by Workday's public API, so the department filter excludes Workday jobs. Postings with several locations show all of them only when descriptions are on. Without descriptions, `postedAt` comes from Workday's "Posted N Days Ago" text, and postings older than 30 days have no date.

**Salary coverage** depends on what the company publishes. Structured fields are used when present. The text parser only accepts ranges with an explicit currency, a pay keyword nearby and plausible values. Single numbers, bonuses and stipends are skipped on purpose, so it misses some ranges rather than invent them.

**Speed and politeness.** Requests to any one host are limited to about 2 per second. Greenhouse, Lever, Ashby, Workable, Recruitee and Personio boards take one or two requests per company. Workday and SmartRecruiters descriptions take one request per job.

**Blocked websites.** Some company homepages block automated visits. Detection then relies on the domain guesses, and the row explains what happened. ATS APIs are not affected. A proxy can be set in the input.

**Responsible use.** The Actor reads public job postings that companies publish through their ATS job-board feeds. It does not log in and does not collect applicant data. Job ads can contain recruiter names or contact emails written by the company. Process them in line with the GDPR and local law, and respect each site's terms when you republish content. Link back to the original `jobUrl` when you show jobs to others.

# Actor input Schema

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

One company per line. Accepts a company domain (linear.app), a careers page URL (https://acme.com/careers) or a direct ATS board URL: boards.greenhouse.io/x, job-boards.greenhouse.io/x, jobs.lever.co/x, jobs.ashbyhq.com/x, x.wd5.myworkdayjobs.com/Site, jobs.smartrecruiters.com/X, apply.workable.com/x, x.recruitee.com, x.jobs.personio.de. Direct ATS URLs are fastest. For domains the Actor guesses the board (Greenhouse, Lever, Ashby) with a sanity check, then scans the homepage and careers pages for ATS links.

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

Upper limit of job rows per company after filters. This caps your cost: each job row is one charged event. On Workday and SmartRecruiters it also limits how many result pages are requested.

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

Keep only jobs whose title contains at least one of these words or phrases (case-insensitive), for example engineer, sales, product manager. Leave empty for all jobs. Filtered-out jobs are not charged.

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

Drop jobs whose title contains any of these words (case-insensitive), for example intern, senior, director.

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

Keep only jobs where at least one location contains one of these texts (case-insensitive substring), for example Berlin, United Kingdom, CA, Remote. The value remote also matches jobs flagged as remote.

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

Keep only jobs marked remote by the ATS (or with Remote in the location). Hybrid jobs are excluded.

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

Keep only jobs whose department or team contains one of these texts, for example Engineering, Sales. Not available for Workday boards, whose public API has no department field.

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

Keep only jobs first published in the last N days. Jobs without a publish date are dropped when this is set. Leave empty for all dates.

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

Adds descriptionText to every job. Free on most ATSs. On Workday and SmartRecruiters it needs one extra request per job, so turning it off makes those boards much faster. Salary parsing from pay-transparency text also needs the description on those two ATSs.

## `includeDescriptionHtml` (type: `boolean`):

Adds descriptionHtml (the original formatted description) next to the plain text. Makes the dataset larger.

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

Monitoring mode. Job IDs per company are stored in the named key-value store below. Each run outputs (and charges) only jobs that were not seen before, and the SUMMARY record reports new and removed jobs per company. The first run outputs every job as the baseline. Schedule the Actor daily or weekly with the same store name.

## `snapshotStoreName` (type: `string`):

Named key-value store that keeps the job IDs between runs (only used with Only new jobs). Use a different name per watchlist or schedule to keep separate baselines. Lowercase letters, digits and hyphens.

## `webhookUrl` (type: `string`):

Optional. After the run, a JSON summary of the jobs found per company (new jobs in monitoring mode) is POSTed here, only when there is at least one job. The payload has a Slack-compatible text field, so a Slack incoming webhook URL works directly. Not charged.

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

How many companies are handled at the same time. Requests to any single host are always limited to about 2 per second, so very high values mostly help with long lists of different companies.

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

Optional. ATS APIs work without a proxy. A proxy can help when scanning company websites that block data-center traffic. Leave empty to use direct requests.

## Actor input object example

```json
{
  "companies": [
    "vercel.com",
    "linear.app"
  ],
  "maxJobsPerCompany": 20,
  "remoteOnly": false,
  "includeDescription": true,
  "includeDescriptionHtml": false,
  "onlyNewJobs": false,
  "snapshotStoreName": "company-careers-jobs-snapshots",
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `results` (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": [
        "vercel.com",
        "linear.app"
    ],
    "maxJobsPerCompany": 20,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("frank_conifer_af4/company-careers-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": [
        "vercel.com",
        "linear.app",
    ],
    "maxJobsPerCompany": 20,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("frank_conifer_af4/company-careers-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": [
    "vercel.com",
    "linear.app"
  ],
  "maxJobsPerCompany": 20,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call frank_conifer_af4/company-careers-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,frank_conifer_af4/company-careers-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/yNswG9a3x9BBP8J7R/builds/18z1ofsGhaGgmZBZS/openapi.json
