# Career Page Jobs Scraper: Live Jobs by Company Name or Domain (`workware/job-postings-api`) Actor

Get every open job at any company, read live from its own career page. Give a company name, domain, or board URL; get clean JSON from Greenhouse, Lever, Ashby and Workable. Built for tracking target accounts, portfolio companies, and competitors. $1 per 1,000 jobs.

- **URL**: https://apify.com/workware/job-postings-api.md
- **Developed by:** [Workware](https://apify.com/workware) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Features:** Limited permissions
- **Stats:** 4 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 job posting 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

## Career Page Jobs Scraper: Live Jobs by Company Name or Domain

**Know which companies you care about? Get every job they have open right now.** Give this Actor a list
of companies, by name, website, or careers-page URL, and it returns every live posting on each one's
official career board as clean JSON. It reads the board at the moment you run it, so you see what a
candidate sees on the careers page today, not what an index scraped last week.

**Who uses it**

- **Sales and RevOps:** a target account posting five sales roles is budgeting to grow. Run your account
  list weekly and route the hiring ones to reps.
- **VC and PE:** track hiring across a portfolio or a watchlist without visiting fifty careers pages.
- **Competitive intelligence:** see what competitors are hiring for, by team and location, as it happens.
- **Recruiting and job sites:** pull complete, current boards for the employers you cover.

**Why this one**

- **Live, not indexed.** Each run reads the employer's own Greenhouse, Lever, Ashby or Workable board.
- **Honest salary data.** Every salary says whether the employer stated it or we parsed it from text.
- **$1 per 1,000 jobs**, plus $0.005 per company. A 50-company weekly sweep is typically under $2.
- **One schema** across all four systems, with one status row per company so you know which ones have
  no public board.
- **Boosted by the Apify platform:** full REST API access, scheduling, monitoring, webhooks and
  integrations, and dataset export to JSON, CSV, or Excel.

### What is the Job Postings API and what can it do?

The Job Postings API is a **company-first jobs API**: you ask about employers, not about keywords. Point
it at a company and it works out which applicant tracking system that company uses, reads the public
board, and returns every open role on it as structured JSON. It is the tool you want when you already
know whose jobs you care about (the accounts in your CRM, the portfolio companies in your fund, the
competitors you track) and you want their current openings on demand or on a schedule.

Supported applicant tracking systems: **Greenhouse, Lever, Ashby, and Workable**. Together these cover a
large share of software, fintech, biotech, and growth-stage employers worldwide, including Lever's
EU-hosted cluster. Every one of them is read through its official, public, no-authentication JSON API:
the most stable class of source that exists for jobs data, and the reason this Actor needs no browser, no
residential proxy, and no anti-bot workarounds.

What it deliberately does **not** do: it does not search across companies by keyword, it does not
maintain an index or a history, and it does not enrich postings with AI-inferred salaries, skills, or
taxonomies. Those are separate concerns, and inferred fields are exactly where indexed competitors
accumulate quiet errors. This Actor's promise is narrower and testable: *the live board, correctly
normalized, honestly labelled.*

How a company gets resolved, in order: if you supply a board URL, it is used directly. Otherwise the
Actor reads the company's own careers page and looks for a board link: evidence straight from the
employer. If that finds nothing, it probes each ATS with candidate slugs derived from the name and
domain, and a similarity guard rejects near-misses so `acme` never resolves to `acme-health`. If every
step misses, you get a definitive `no_board` answer, which is a real result: some employers genuinely
have no public board.

### What data does it extract? (output fields)

Every record is a canonical `JobPosting`: a stable id, the source and its native id, the posting URL and
apply URL, raw and normalized titles, company, structured location, department, employment type, salary,
description in HTML and plain text, and the dates the role was posted and observed.

Two fields deserve a note because they are where jobs data usually goes wrong:

- **`salary`** carries a `source` label: `stated` only when the employer set a structured salary the ATS
  returns as data (Ashby compensation, Lever `salaryRange`), and `parsed` when it was read out of a
  description. If a role's pay cannot be established honestly, the field is absent. It is never guessed.
- **`location.remoteScope`** is the geography a remote candidate must be eligible to work in: ISO
  country codes and macro-region labels such as `["US","CA"]` or `["EU"]`, populated when the employer
  states a restriction and absent when they state none. Absent means unstated, never "worldwide".

Source-specific fields that do not fit the canonical schema (Greenhouse offices and departments, Ashby
compensation tiers) are preserved under `extras.<source>` rather than dropped or forced into a field
where they do not belong. Personal contact details are dropped at a single normalization chokepoint and
never reach the dataset.

Alongside the job records, each company produces one `company-status` row: `{ company, resolvedSource,
boardToken, jobCount, status }`, where status is `found`, `no_board`, or `error`. This is what makes an
empty result unambiguous: you always know whether a company has no jobs, no board, or simply could not
be reached.

| Field | Type | Always present | Description |
|---|---|---|---|
| `id` | `string` | Always | Stable id: `sha1(source + sourceId)` (computed by normalizers/actors, not here). |
| `source` | `greenhouse` | `lever` | `ashby` | `workable` | `wellfound` | `seek` | `stepstone` | `dice` | `ziprecruiter` | `bayt` | `xing` | `reed` | Always | This Actor reads Greenhouse, Lever, Ashby and Workable; the other values are reserved for sibling Actors. |
| `sourceId` | `string` | Always | Native id at the source. |
| `url` | `string` | Always | Canonical public posting URL. |
| `applyUrl` | `string` | Optional | Direct apply link when distinct from `url`. |
| `title` | `string` | Always | Raw title as posted. |
| `titleNormalized` | `string` | Always | Cleaned title (no req-ids, no ALL CAPS, trimmed seniority tags): normalized in M1b. |
| `company.name` | `string` | Always | Display name. |
| `company.website` | `string` | Optional | Company website when known. |
| `company.sourceSlug` | `string` | Always | Source-native company slug/identifier. |
| `location.raw` | `string` | Always | Raw location string as provided by the source. |
| `location.city` | `string` | Optional |  |
| `location.region` | `string` | Optional |  |
| `location.country` | `string` | Optional | ISO-3166 country code when resolvable. |
| `location.remote` | `full` | `hybrid` | `onsite` | `unknown` | Always | Remote-work posture; `unknown` when it cannot be determined. |
| `location.remoteScope` | `string`\[] | Optional | For remote roles: the geography a remote candidate must be eligible to work in (visa / right-to-work), e.g. `['US','CA']`, `['EU']`, `['GLOBAL']`. Values are ISO-3166-1 alpha-2 country codes and/or canonical macro-region labels (`GLOBAL\|EU\|EMEA\|APAC\|LATAM\|MENA\|ANZ\|DACH\| AMERICAS\|NORAM`). Absent when the source states no restriction: never assumed global. |
| `department` | `string` | Optional | Source-provided team/department. |
| `employmentType` | `full_time` | `part_time` | `contract` | `intern` | `temporary` | `unknown` | Optional |  |
| `salary.raw` | `string` | Optional | Original salary display string when present. |
| `salary.min` | `number` | Optional |  |
| `salary.max` | `number` | Optional |  |
| `salary.currency` | `string` | Optional | ISO-4217 currency code when known. |
| `salary.period` | `year` | `month` | `day` | `hour` | Optional |  |
| `salary.source` | `stated` | `parsed` | Optional |  |
| `descriptionHtml` | `string` | Optional | Sanitized HTML description when available. |
| `descriptionText` | `string` | Always | Plain-text description. |
| `postedAt` | `string` | Optional | When the role was published at the source (not scrape time). |
| `firstSeenAt` | `string` | Always | Our observation window. In live-fetch actors both are simply the run's scrape time: do NOT build cross-run observation tracking here; that is new-job-postings-api's (#18) concern. |
| `lastSeenAt` | `string` | Always |  |
| `extras` | `object` | Optional | Source-specific fields, namespaced under `extras.<source>.*`; never breaks the schema. |

### How to use the Job Postings API (tutorial)

1. Click **Start** with the example input already filled in: one company, no options.
2. Watch the run finish (a single company typically completes in a few seconds).
3. Open the **Dataset** tab. You will see the job records plus one `company-status` row.
4. Export as JSON, CSV, or Excel, or call the run from your own code through the Apify API.

To run it on your own list, replace the `companies` array. Each entry needs at least one of `name`,
`domain`, or `atsUrl`; supplying `domain` resolves most reliably, and supplying `atsUrl` skips detection
entirely and is the fastest path if you already know the board.

Companies in a batch are processed **one after another**, at a deliberately polite request rate to the
ATSs: a single company typically takes a few seconds, and a long list takes roughly that per company.
If you are polling on a schedule, prefer several small runs over one very large one.

### How much does it cost?

Pay-per-event, with two events:

- **$0.005 per company processed**: charged once per company that reached a definitive answer, whether
  that answer is "here is the board" or "this company has no public board". The detection work has value
  either way.
- **$0.001 per job posting returned**: charged per validated record.

**Failed lookups are free.** A company whose status is `error` is never charged, and neither are the
records of a company whose fetch failed part-way through: they are discarded rather than delivered. We
charge for answers, not for our own failures.

A worked example: 100 companies averaging 40 open roles each costs `100 × $0.005 + 4,000 × $0.001 =
$4.50`. The comparable indexed API charges $4.00 per 1,000 jobs, which would be $16.00 for the same
4,000 postings. Platform usage costs are absorbed, not passed through.

| Event | What it is | Charged for | Price |
|---|---|---|---|
| `company-processed` | Company processed (definitive answer) | each run | $5.00 / 1,000 |
| `job-result` | Job posting returned | each record delivered | $1.00 / 1,000 |

### Input and output examples

Input: the simplest useful run:

```json
{ "companies": [{ "name": "Anthropic" }] }
```

Input: a filtered batch:

```json
{
  "companies": [
    { "domain": "stripe.com" },
    { "atsUrl": "https://jobs.ashbyhq.com/ramp" }
  ],
  "titleFilter": "engineer",
  "remoteOnly": true,
  "maxJobsPerCompany": 25
}
```

Output: one job record (trimmed) and its company's status row:

```json
{
  "id": "8f2c…",
  "source": "greenhouse",
  "url": "https://job-boards.greenhouse.io/acme/jobs/4001",
  "title": "Senior Backend Engineer",
  "titleNormalized": "Senior Backend Engineer",
  "company": { "name": "Acme", "sourceSlug": "acme" },
  "location": { "raw": "Remote — US", "country": "US", "remote": "full", "remoteScope": ["US"] },
  "employmentType": "full_time",
  "salary": { "min": 180000, "max": 220000, "currency": "USD", "period": "year", "source": "parsed" },
  "postedAt": "2026-06-20T10:00:00Z"
}
```

```json
{ "type": "company-status", "company": { "domain": "acme.com" }, "resolvedSource": "greenhouse",
  "boardToken": "acme", "jobCount": 42, "status": "found" }
```

`titleFilter` is a plain, case-insensitive substring match, not a regular expression. `remoteOnly` keeps
roles whose posture is remote or hybrid; it does not filter on eligibility geography, so a remote role
restricted to one country is still returned and you can filter on `location.remoteScope` yourself.

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `companies` | `array` | Yes | - | The companies to look up: one run handles the whole batch, one company at a time. Each entry needs at least one of `name`, `domain`, or `atsUrl`. `domain` resolves most reliably; `atsUrl` skips detection altogether. |
| `includeDescription` | `boolean` | No | `true` | Include the full job description in each record. Descriptions are always FETCHED (salary parsing and normalization read them): turning this off strips them from the output to keep datasets small. |
| `remoteOnly` | `boolean` | No | `false` | Return only roles whose remote posture is remote or hybrid. This filters the posture, not the eligibility geography: a remote role open only to US residents is still returned. |
| `titleFilter` | `string` | No | - | Keep only roles whose title contains this text, case-insensitively. Plain substring matching: not a regular expression. |
| `maxJobsPerCompany` | `integer` | No | `0` | Caps the jobs returned per company. 0 means no cap: return the whole board. |

### Other Actors you might need

| Actor | Use it when |
|---|---|
| [Greenhouse Jobs Scraper](https://apify.com/workware/greenhouse-jobs-scraper) | You only care about Greenhouse and already have board tokens. |
| [Ashby Jobs Scraper](https://apify.com/workware/ashby-jobs-scraper) | You only care about Ashby boards. |

If you do not know which ATS a company uses, or your list spans several of them, this Actor is the one
you want: resolving the board is the part it does for you.

### FAQ, legality, and support

#### How is this different from indexed job APIs?

An indexed API crawls many career sites on its own schedule and serves you a copy. That is the right
shape for "show me every Python job posted this week", and the wrong shape for "what is on this
company's board right now": the copy is as fresh as the last crawl, and crawls run hours apart. This
Actor holds no index. It reads the employer's board when you ask, which means no indexing delay and no
per-run cap on how much of a board you can retrieve.

#### Which applicant tracking systems are supported?

Greenhouse, Lever (including the EU cluster), Ashby, and Workable, all through their official public
JSON APIs. If a company uses an ATS outside this list, you get a `no_board` status rather than a silent
empty result.

#### What if a company has no public board?

You get a `company-status` row with `status: "no_board"`, and that counts as a definitive answer. Many
employers post only to their own bespoke careers page or to job boards rather than running a public ATS
board; "this company has no public ATS board" is genuinely useful information, and it is priced as one
processed company with no job results.

#### What happens when a source changes its API?

Every response is shape-checked before it is normalized. A change that breaks the shape fails the
affected company loudly instead of emitting silently wrong records, and a scheduled smoke test runs
against known-stable boards so we usually find out before you do.

#### Is this legal to use?

The data is public, employer-published job advertising, retrieved through the ATS vendors' own public
APIs. Job postings are not personal data, attribution and apply URLs are preserved on every record, and
recruiter and contact details are dropped rather than collected. As always, how you use the data is your
responsibility.

#### Can I get a custom variant?

Yes: open an issue on the Actor or contact us through the Apify platform. Cross-company keyword search,
per-posting detail enrichment, and new-jobs-only change tracking are all being built as separate Actors.

# Actor input Schema

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

The companies to look up: one run handles the whole batch, one company at a time. Each entry needs at least one of `name`, `domain`, or `atsUrl`. `domain` resolves most reliably; `atsUrl` skips detection altogether.

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

Include the full job description in each record. Descriptions are always FETCHED (salary parsing and normalization read them): turning this off strips them from the output to keep datasets small.

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

Return only roles whose remote posture is remote or hybrid. This filters the posture, not the eligibility geography: a remote role open only to US residents is still returned.

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

Keep only roles whose title contains this text, case-insensitively. Plain substring matching: not a regular expression.

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

Caps the jobs returned per company. 0 means no cap: return the whole board.

## Actor input object example

```json
{
  "companies": [
    {
      "name": "Anthropic"
    }
  ],
  "includeDescription": true,
  "remoteOnly": false,
  "maxJobsPerCompany": 0
}
```

# Actor output Schema

## `results` (type: `string`):

Every record this run produced, as JSON. Also available as CSV, Excel and XML.

# 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": [
        {
            "name": "Anthropic"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("workware/job-postings-api").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": [{ "name": "Anthropic" }] }

# Run the Actor and wait for it to finish
run = client.actor("workware/job-postings-api").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": [
    {
      "name": "Anthropic"
    }
  ]
}' |
apify call workware/job-postings-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,workware/job-postings-api"
        }
    }
}
```

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/Yi98JObBJxmC1gOcK/builds/YU9XfJQ2mnYJ8i0gQ/openapi.json
