# Job Postings ✅ Company Careers Pages and ATS Boards, One Schema (`pvalyou/job-postings`) Actor

Every open role of a company from its own job board: give a domain, careers URL, ATS URL or LinkedIn company URL. 24 ATS systems (Greenhouse, Lever, Ashby, Workday, Workable, SmartRecruiters and more), one schema, hiring summary per company, new and closed roles on a schedule. $0.0008 per job.

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

## Pricing

from $0.80 / 1,000 job rows

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?

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

## 💼 Job Postings: every open role of a company, from its own job board

**Give a company and get its open roles, straight from the job board the company itself publishes.** $0.002 per company plus $0.0008 per job row, for sales teams reading hiring signals, recruiters, competitive and market research, and investors. Where a LinkedIn Jobs or Indeed scrape gives you an aggregator's copy of a vacancy, this job postings API reads the employer's own applicant tracking system, so a role that closed is gone and a role that is open carries its real apply link.

A domain (`stripe.com`), a website or careers URL, an ATS board URL or a LinkedIn company URL goes in. Out comes every open role in one schema: title, locations, department, seniority, workplace type, employment type, salary when published, the provider's posted date, the full description and the apply link, plus one summary row per company with the counts by department, seniority and country. Open one posting URL in full, or run the same list on a schedule and get each role flagged new or closed. Try it now with the prefilled input: Notion, Stripe and a Lever board.

The roles come from the applicant tracking systems companies publish their jobs on, read through those systems' public job-board APIs and the JobPosting data on career pages: 23 applicant tracking systems (Greenhouse, Lever, Ashby, Workday, Workable, SmartRecruiters and 17 more, listed below) plus any careers page that publishes JobPosting JSON-LD, all in one schema. No job aggregators, no data brokers.

**What you get**

- ✅ **The company's own board, found for you.** Give a domain and the Actor reads the homepage and careers page, recognises the board behind it, and when the careers site hides its board behind JavaScript it tries the public ATS APIs directly. Stripe and Datadog both answer that way: 617 and 449 open roles from Greenhouse in under 15 seconds.
- ✅ **One schema across 23 applicant tracking systems.** Every row has the same fields whether it came from the Greenhouse jobs API, a Lever board, an Ashby board or Workday, with a stable `job_id`, the provider, and `board_resolution` telling you how the board was found.
- ✅ **A hiring summary per company.** Open roles, counts by department, seniority and country, remote share, newest and oldest posting dates, and what the filters dropped. One row per company, charged once as `company_search` at $0.002, whether or not a board was found. Even a company with no board returns that row with the careers URL we saw and the steps taken.
- ✅ **New and closed roles on a schedule.** Monitor mode keeps the previous run's roles and flags what appeared and what closed.
- ✅ **Priced per row.** $0.002 per company searched, $0.0008 per job row, $0.003 per posting opened, plus $0.00005 to start the run. 100 companies with 20 roles each cost $1.80.

### 👥 Who uses company job postings?

- 📈 **Sales and outbound.** Companies hiring for a function are buying for it. Run a target list weekly, filter titles by keyword, and read the summary's counts by department as a buying signal.
- 🧑‍💼 **Recruiters and staffing.** Live vacancies with apply links and full descriptions from the employer's own board, not a copy on an aggregator that closed last month.
- 🔭 **Competitive and market intelligence.** A competitor's open roles by office, team and seniority, month over month, from the same source every time.
- 💸 **Investors.** Headcount plans read from the hiring page: which functions, which countries, how many, how recent.
- 🧑‍💻 **Job seekers and career tools.** A list of companies you like, run daily in monitor mode, with only the new roles surfaced.
- 🤖 **AI agents and pipelines.** Structured JSON with a stable id per role, callable from the Apify API, the MCP server and the Python and JavaScript clients. Events only, so it is eligible for agentic payment.

### ⬇️ Input: a mode and a list of companies or URLs

```json
{
  "mode": "company",
  "queries": ["notion.so", "stripe.com", "https://jobs.lever.co/anchorage"],
  "max_jobs": 200
}
```

| Field | What it does |
|---|---|
| `mode` | `company` (every open role, one summary per company), `job` (one posting URL in full), `monitor` (company mode with new and closed flags since the previous run). |
| `queries` | One per line. Company and monitor modes: a domain, a website URL, a careers page URL, an ATS board URL or a LinkedIn company URL. Job mode: the URL of one posting on the employer's careers site or ATS. Each entry may also be an object with `query`, its own `mode` and any filter below, so one run can mix companies and postings. |
| `max_jobs` | Rows per company after the filters, up to 500. |
| `include_description` | Full description text and html on every row (default on). |
| `keywords`, `exclude` | Title must contain any of the first list and none of the second. |
| `country` | Two-letter code. Keeps roles with a location in that country. |
| `remote_only`, `department` | Roles the employer marks remote. Department contains the word. |
| `posted_within_days` | Roles the provider dates within this many days. Undated rows are dropped when this is set, and the summary says how many. |

`max_jobs` and the filters apply in company and monitor modes only. Job mode reads the one posting at the URL you give and returns it whole, description included.

A LinkedIn company URL is used only to find the company's website: the board is read there, LinkedIn itself is not read. When the website cannot be found from the LinkedIn page name, the summary says so and asks for the domain.

An empty entry, a company that is not a domain or URL, a job URL without a scheme, or a LinkedIn job URL is rejected before any paid step, as an uncharged error item, and the run still ends normally.

### ⬆️ Output: one dataset item per open role

Every item carries `type`, the `query` it came from, and `charged`.

| `type` | Charged | Contents |
|---|---|---|
| `job` | once per row (`job_record`) | one open role |
| `company_summary` | once per company (`company_search`), whether or not a board was found | how the board was found, the counts, what the filters dropped, and in monitor mode the new and closed roles |
| `job_detail` | once (`job_detail`), when the posting exists | one posting in full |
| `error` | never | an `error` code and a message |

Every `error` item is free, and its code says which stage refused it:

| Where it comes from | Codes |
|---|---|
| Your input, before any call | `invalid_input` (not a domain or a URL, or an entry that is neither a string nor an object with `query`), `empty_query`, `unknown_mode`, `invalid_url` (job mode without `https://`), `linkedin_not_supported` (a LinkedIn job page) |
| The jobs service | `no_match` (job mode: no posting at that URL), `rate_limited`, `unauthorized`, `upstream_error`, `api_error` |
| The connection | `timeout` (no answer within 90 seconds), `unreachable` |

A job row from the first prefilled query:

```json
{
  "type": "job", "query": "notion.so", "charged": true, "charged_event": "job_record",
  "job_id": "8d87834209ebbbf21e6d", "title": "Software Engineer, Developer Platform",
  "company_name": "Notion", "provider": "ashby", "source_job_id": "1fc309c8-da20-4ff2-84c7-8b863ece2b0a",
  "source_url": "https://jobs.ashbyhq.com/notion/1fc309c8-da20-4ff2-84c7-8b863ece2b0a",
  "apply_url": "https://jobs.ashbyhq.com/notion/1fc309c8-da20-4ff2-84c7-8b863ece2b0a/application",
  "locations": [{"raw": "San Francisco, California", "city": "San Francisco", "region": "California", "country_code": "US"},
                {"raw": "New York, New York", "city": "New York", "region": null, "country_code": null}],
  "workplace_type": "hybrid", "employment_types": ["FullTime"],
  "department": "Engineering", "team": "Engineering", "office": null, "seniority": null,
  "salary": {"min": 213000, "max": 320000, "currency": "USD", "period": "year", "raw": "salary range for this role is $213,000 - $320,000 per year"},
  "posted_at": "2026-08-24T14:44:49.699000+00:00", "updated_at": null, "valid_through": null, "language": "en",
  "board_resolution": "careers_page",
  "description_text": "Who We Are Notion is the collaborative AI workspace where te ...", "description_html": "<h1>Who We Are</h1><p>Notion is the ..."
}
```

Its company summary:

```json
{
  "type": "company_summary", "query": "notion.so", "charged": true, "charged_event": "company_search",
  "status": "matched", "resolved_domain": "notion.so", "resolution": "domain",
  "board_provider": "ashby", "board_identifier": "notion", "board_url": "https://jobs.ashbyhq.com/notion",
  "board_resolution": "careers_page", "board_completeness": "complete",
  "open_roles": 130, "returned": 130, "dropped_by_filter": {},
  "by_department": {"Sales": 44, "Engineering": 17, "Marketing": 11, "Outcomes Architect": 11, "People": 10, "Early Career": 8, "Finance": 5, "Customer Experience": 5, "Security": 3},
  "by_seniority": {"manager": 33, "lead": 12, "intern": 3, "director": 2, "executive": 1},
  "by_country": {"US": 76, "JP": 16, "IE": 15, "FR": 7, "IN": 4, "KR": 4, "DE": 3, "GB": 3, "AU": 2},
  "remote_share": 0.0, "rows_with_posted_date": 130,
  "newest_posted_at": "2026-09-04T20:24:59.411000+00:00", "oldest_posted_at": "2025-07-25T22:00:31.199000+00:00",
  "careers_url_seen": "https://www.notion.com/careers", "truncated": false, "elapsed_s": 3.8
}
```

`board_resolution` says how the board was found: `careers_page` (recognised on the company's site), `ats_url` (you gave the board), `board_token_guess` (found by trying the public ATS APIs with the company's name and confirmed against the company), or `verified_board` (confirmed on an earlier lookup). `posted_at` is the provider's own published date and is null where the provider does not publish one.

In monitor mode each job row also carries `new` (true when the role was not in the previous run) and the summary carries `new_roles`, `closed_roles` and `closed_job_ids`. The first run of a company sets the baseline and flags nothing.

Export the dataset as JSON, CSV, Excel or XML, or read it through the Apify API. Lists and objects (`locations`, `salary`, `by_department`) are kept as JSON in CSV exports.

### 💰 How much do company job postings cost?

| Event | Price | When it fires |
|---|---|---|
| `company_search` | **$0.002** | once per company processed, board found or not |
| `job_record` | **$0.0008** | once per job row returned after the filters |
| `job_detail` | **$0.003** | once per posting opened in job mode, when it exists |
| Actor start | **$0.00005** | once per run |
| Invalid input, a posting that does not exist, rate limit, upstream failure | **free** | |

The worked examples below are the event charges only. The Actor start adds $0.00005 a run, so the prefilled three-board run is $0.29 of events and $0.29005 in total:

- The prefilled input, three boards with 130, 617 and 28 roles at the default cap of 200: 3 x $0.002 + (130 + 200 + 28) x $0.0008 = **$0.29**.
- One company with 20 open roles: **$0.018**.
- 100 companies averaging 20 roles: **$1.80**.
- 1,000 domains where 400 have a board and 12,000 roles come back: $2.00 + $9.60 = **$11.60**.
- A monitor run on 50 companies every morning, 1,000 roles: **$0.90** a day, with only the new ones to read.
- One posting opened: **$0.003**.

Filters are applied before charging: rows a filter drops are not job rows. Set a spending limit on the run for a hard cap.

### 🗺️ What is covered

- **Systems (23).** Greenhouse, Lever, Ashby, Workday, Workable, SmartRecruiters, Recruitee, Breezy, Teamtailor, BambooHR, iCIMS, Eightfold, Rippling, HiBob, Comeet, Zoho Recruit, Oracle Recruiting, SuccessFactors, Paylocity, GoHire, Dueto, TeamMe and Base44. On top of those, any careers page that publishes JobPosting JSON-LD is read the same way, which is not one of the 23.
- **Companies.** Any company whose careers page links to one of those systems or carries JobPosting data, plus companies whose board can be found by name on the public ATS APIs (Greenhouse, Lever, Ashby, Recruitee, Breezy). A careers site that renders its roster only with JavaScript and is on none of them comes back `no_board_found`, with the careers URL we saw.
- **Fields.** Whatever the board publishes. Descriptions are on almost every board. Posted dates are published by most ATS systems and by JSON-LD pages, not by all. Salary appears only where the employer prints it.

### 🔎 How it works

1. The input is read: a bare domain, a website URL, a careers URL, an ATS board URL, or a LinkedIn company URL (resolved to the website first).
2. For a domain or website, the homepage and careers page are read and the board behind them recognised from its URL patterns, embedded configuration or JobPosting data. Pages are read as published, without browser rendering.
3. If no board was recognised, the public ATS APIs are tried with the company's name as the board token. A board found this way is kept only when its postings point at the company's domain or name the company in most descriptions, so a namesake's board is never returned as yours.
4. The board is read in full through its public API or page, the rows are normalised into one schema, the filters are applied, and the summary is computed over all open roles before the cap.
5. In job mode the posting's provider is recognised from the URL and the posting read from the provider's own API, or from the JobPosting data on the page.
6. Nothing is estimated or inferred and no language model is involved: every field is the employer's published value.

### ⏱️ How long does a company take?

A board recognised on the careers page answers in 2 to 6 seconds. A board found by token guess takes 10 to 15 seconds. Each company has a 60 second budget in the service. Companies run one after another, so 100 companies take 5 to 15 minutes.

### 🔌 Job postings API, clients and integrations

Run the Actor from the Apify Console, from the [Apify API](https://docs.apify.com/api/v2), from the Python or JavaScript client, from the Apify MCP server, or from Make, Zapier and n8n through the Apify integrations. Schedule monitor mode daily and set a webhook on run completion to pull the new roles into your own system.

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_API_TOKEN>")
run = client.actor("pvalyou/job-postings").call(run_input={
    "mode": "company",
    "queries": ["notion.so", "stripe.com", {"query": "https://jobs.lever.co/anchorage", "keywords": ["engineer"]}],
    "max_jobs": 100,
    "include_description": False,
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["type"], item.get("company_name"), item.get("title"), item.get("posted_at"), item.get("charged"))
```

### 🔐 Data, licences and privacy

- **The employer's own board.** Roles are read from the job-board APIs the applicant tracking systems publish for exactly this purpose (an employer switches them on so its jobs can be embedded and syndicated) and from the JobPosting data career pages publish for search engines. No job aggregators, no data brokers.
- **Not LinkedIn.** LinkedIn's terms forbid automated collection, so LinkedIn job pages are not read. A LinkedIn company URL is only used to find the company's website.
- **Pass-through.** Each company is read from its board and passed through to your run. The results live in your own Apify dataset. Operational logs keep the query, timing and the billing outcome. Monitor mode keeps the job ids of your previous run in a named key-value store in your own account, nothing else.
- **No applicant or recruiter data.** Only the published vacancy: title, description, location, department, dates and links. Never applications, candidates or internal ATS fields.
- **Not an offer of employment.** A row is the employer's published vacancy at the moment of the run. Apply on the employer's site.
- **No access controls bypassed.** No CAPTCHA solving, no logins, no browser rendering, robots directives respected.

### ❓ FAQ

#### Is the data live?

Yes. Every company is read from its board at the moment of the run, whichever way the board was found.

#### Why did a company come back with no board?

Its careers site renders the roster with JavaScript and is on none of the supported systems, or the domain has no careers page we could find. The summary carries the careers URL we saw and the steps taken. If you know the board URL, send it as the company input.

#### Why is `posted_at` empty on some rows?

The provider does not publish a posting date for that board (a few systems and some career pages). Rows with a date carry the provider's own date, never the date we first saw the role.

#### How is the monitor baseline kept?

In a named key-value store in your own account, `job-postings-monitor-<task id>`, under the key `MONITOR_STATE`: a list of job ids per company. The first run of a company sets the baseline. Run the same saved task on a schedule and the baseline carries over. Delete the store to start over.

#### Is it legal to use?

The job-board APIs are published by the applicant tracking systems for employers to syndicate their vacancies, and career pages publish JobPosting data for search engines. Job postings are public by design. Use them in line with the laws of your jurisdiction.

#### Is there a free tier or a trial?

Every rejected input, every company without a board and every failed query is free, and Apify's free plan credit covers the first paid lookups. There is no subscription.

### 📏 Limits

- Up to 1,000 entries per run, 500 rows per company, 60 seconds per company in the service and 90 in the Actor.
- Pages are read as published, without browser rendering.
- LinkedIn, job aggregators and staffing boards are not read.
- Descriptions are capped at 20,000 characters of text and 40,000 of html.
- Entries run one after another. Exact duplicates within a run are collapsed. The run's spending limit is honoured.

### 🔗 Works well with

The same company domain opens the other five: [Company Enrichment API](https://apify.com/pvalyou/company-record) for firmographics, classification and funding, [Website Liveness Check](https://apify.com/pvalyou/website-liveness-check) to drop the dead and parked domains before you run a list here, [Company Registry Lookup](https://apify.com/pvalyou/company-registry-lookup) for the official registry record, [Patent Lookup](https://apify.com/pvalyou/patent-lookup) for the company's patents, and [LinkedIn Person Enrichment](https://apify.com/pvalyou/linkedin-person-enrichment) for the career record of the hiring manager named on a posting.

### 💬 Support and feedback

Found a company whose board was missed, a system you need, or a row that looks wrong? Open an issue on this Actor's page with the company, the mode and what you expected. Issues are answered by the developer.

### 📝 Changelog

- **0.1 (2026-09-08)** First public release: company, job and monitor modes, 23 applicant tracking systems and JSON-LD career pages, three priced events.

# Actor input Schema

## `mode` (type: `string`):

Company: every open role of each company, from its own job board. Job: one posting in full from its URL. Monitor: company mode on a schedule, with each role flagged new or closed since the previous run.

## `queries` (type: `array`):

One per line. Company and monitor modes take a domain (acme.com), a website URL, a careers page URL, an ATS board URL (Greenhouse, Lever, Ashby, Workable and the others) or a LinkedIn company URL. Job mode takes the URL of one posting on the employer's careers site or ATS. For programmatic use each entry may also be an object with 'query' and any filter below.

## `max_jobs` (type: `integer`):

Cap on the rows one company returns, after the filters. Each row is one job\_record event. Up to 500. Company and monitor modes only, because a job URL is one posting.

## `include_description` (type: `boolean`):

Full description text and html on every row. Off keeps rows small (title, locations, department, dates, links). Company and monitor modes only. Job mode always returns the whole posting, description included, and this switch does not change it.

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

Keep roles whose title contains any of these words (case-insensitive).

## `exclude` (type: `array`):

Drop roles whose title contains any of these words.

## `country` (type: `string`):

Two-letter code (US, GB, IL, DE). Keeps roles with a location in that country.

## `remote_only` (type: `boolean`):

Keep roles the employer marks remote.

## `department` (type: `string`):

Keep roles whose department contains this word (Engineering, Sales, Marketing).

## `posted_within_days` (type: `integer`):

Keep roles the provider dates within this many days. Rows without a provider date are dropped when this is set, and the summary says how many.

## Actor input object example

```json
{
  "mode": "company",
  "queries": [
    "notion.so",
    "stripe.com",
    "https://jobs.lever.co/anchorage"
  ],
  "max_jobs": 200,
  "include_description": true,
  "remote_only": false
}
```

# Actor output Schema

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

Every item of the run with every field it carries: job rows, company summaries, job details and error items.

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

Every item of the run, shown as the role columns: company, title, locations, department, seniority, workplace, posted date, apply link, open roles, returned, charged. A view picks the columns and never drops rows, so company summaries and error items are in this table too, with the per-role columns left blank. The summary row carries how many roles are open against how many this run returned.

## `summaries` (type: `string`):

Every item of the run, shown as the company-summary columns: board resolved and how, board URL and completeness, open roles against returned, newest and oldest posting, and in monitor mode the roles opened and closed since the last run. A view picks the columns and never drops rows, so role rows are in this table too, with the summary columns left blank.

# 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 = {
    "queries": [
        "notion.so",
        "stripe.com",
        "https://jobs.lever.co/anchorage"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("pvalyou/job-postings").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 = { "queries": [
        "notion.so",
        "stripe.com",
        "https://jobs.lever.co/anchorage",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("pvalyou/job-postings").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 '{
  "queries": [
    "notion.so",
    "stripe.com",
    "https://jobs.lever.co/anchorage"
  ]
}' |
apify call pvalyou/job-postings --silent --output-dataset

```

## MCP server setup

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

```

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/iBsbG6XclfZdpwQHw/builds/9cEoah2sb1zoQ2c4R/openapi.json
