# Hiring Signals & ATS Jobs Scraper — Greenhouse, Lever, Ashby (`owlabee/job-postings-api`) Actor

Hiring signals from company career pages: new, removed and changed roles per company, plus normalized job postings from Greenhouse, Lever, Ashby, SmartRecruiters, Workable, 7 more ATSs, USAJOBS and any careers page with JobPosting markup. Pay per job and signal.

- **URL**: https://apify.com/owlabee/job-postings-api.md
- **Developed by:** [Owlabee](https://apify.com/owlabee) (community)
- **Categories:** Jobs, Lead generation
- **Security:** Limited permissions
- **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 records

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Hiring Signals & ATS Jobs Scraper — Greenhouse, Lever, Ashby

![Hiring Signals & ATS Jobs Scraper — Greenhouse, Lever, Ashby: banner with the main output fields](https://api.apify.com/v2/key-value-stores/r8pLvIribNPHYI9gX/records/ats-hiring-signals-banner.png)

**Hiring signals** for sales, recruiting and investment teams: point this Actor at company career pages on **Greenhouse, Lever, Ashby, SmartRecruiters, Workable, Rippling, Gem, Breezy HR, Teamtailor, Personio, Manatal or JobScore**, at US federal agencies on **USAJOBS** (with your own free API key), or at any careers site that publishes **schema.org JobPosting** markup, and it returns every open role as normalized JSON, then, on scheduled runs, the new, removed and changed roles per company with a hiring-velocity score. It reads public job-board APIs, feeds and the employers' own pages, so there is no login, browser or proxy.

> **Renamed on 2026-11-07:** this Actor's address changed from `owlabee/ats-hiring-signals` to `owlabee/job-postings-api`. Saved tasks, schedules, webhooks and integrations keep working, because they point to the Actor's ID, which did not change. Update anything that calls it by name: API URLs (`owlabee~job-postings-api`), MCP tool lists (`owlabee/job-postings-api`) and bookmarks.

### What it does

This Actor scrapes job postings straight from the **public job-board APIs and feeds** that company career pages are built on: **Greenhouse**, **Lever**, **Ashby**, **SmartRecruiters** and **Workable**, and since November 2026 also **Rippling**, **Gem**, **Breezy HR**, **Teamtailor**, **Personio**, **Manatal** and **JobScore**. Give it a list of boards (`greenhouse:stripe`, `https://jobs.lever.co/spotify`, `personio:tado`, …) and it returns one normalized record per open job: title, department, team, location (raw plus best-effort city/country), a remote flag, employment type, posted/updated dates, apply URL, optional plain-text description and salary data where the source exposes it (Ashby, Lever, Personio, Breezy HR, USAJOBS and JobPosting markup).

Two more kinds of source fill the gaps between ATSs. **USAJOBS** (`usajobs:VA`) returns every open federal job of an agency through the official Search API, with pay range and closing date; it needs your own free API key, because the government's terms do not allow a key to be shared. **schema.org JobPosting markup** (`jsonld:https://www.example.com/careers`, or automatically with `extendedDetection`) covers careers sites on any other system (iCIMS, Jobvite, JazzHR, Phenom, Avature, custom sites) as long as they publish the structured data search engines read; the Actor honors robots.txt and reads one page at a time.

Switch to **signals mode** and it becomes a hiring monitor. Each board is compared with the snapshot from the previous run and you get change events — `new-role`, `removed-role`, `title-changed`, `location-changed` — plus one `board-summary` per company with open-role count, net change, top departments, top locations, remote share and a 0–100 **hiring velocity score**. That is the data sales teams, recruiters, investors and researchers use to spot companies that are expanding, freezing or shifting focus, without touching LinkedIn.

Because it reads official job-board APIs over plain HTTP, it needs no browser, no proxies and no login, which keeps it fast and cheap: **1,000 jobs cost about $1.00**. Results are available as JSON, CSV, Excel, through the Apify API, webhooks and the Apify MCP server.

Once you know who is hiring, [SPF, DMARC & DKIM Checker — Bulk Domain WHOIS, DNS & SSL](https://apify.com/owlabee/spf-dmarc-dkim-checker) adds each company's registrar, mail provider and tech stack in one more run.

### Why this Actor

| | This Actor | Typical scraper in this category | Do it yourself |
|---|---|---|---|
| Data sources covered | Twelve ATS providers (Greenhouse, Lever, Ashby, SmartRecruiters, Workable, Rippling, Gem, Breezy HR, Teamtailor, Personio, Manatal, JobScore) through their public job-board APIs and feeds, USAJOBS with your own key, and schema.org JobPosting markup on any other careers site, mixed freely in one run | One to three providers, often a single one, frequently scraped from the rendered careers page | One API client per provider, each with its own pagination, field names and quirks |
| Modes | `jobs` export and `signals` monitoring: new, removed, retitled and relocated roles plus one summary per company with a hiring velocity score | Export only; change detection, when offered, is a flag that marks unseen jobs | Store snapshots yourself and write the diff, the summary and the scoring |
| Output fields | One normalized record per job: title, department, team, raw and parsed location, remote flag, employment type, dates, apply URL, optional description text, compensation where published | Provider-specific raw fields, or a normalized subset without compensation or parsed location | Whatever you map, re-mapped whenever a provider changes its payload |
| Pricing unit | Per job record, per hiring signal and per board request; an error item costs the same as the record it replaces | Per job result, sometimes with a per-run fee or a monthly plan on top | Your engineering time plus the compute that runs it |
| Proxies or browsers needed | None: plain HTTPS to official APIs, no login | Browser automation and residential proxies when the careers page is scraped instead of the API | None if you use the APIs, a browser farm if you do not |
| Agent / MCP ready | Yes: pay-per-event, limited permissions, pin link, prompts and a call example below | Sometimes | You write and host the tool definition |
| Limits | Public boards only; Workable sits behind a WAF and may be blocked; time budget per board, request deadlines, caps per board and per run | Varies; caps are usually per run only | Rate limits and retry handling are yours to tune |

- Signals mode is a hiring monitor, not just an export: one summary per company with net change and a 0–100 velocity score, which is the field sales and investor teams sort by.
- Billing follows the work: a board request is charged even when it returns nothing or times out, so the cost of an empty or failing board is known before the run and nothing is billed in bulk at the end.
- Every abandoned unit of work is visible: a blocked or stalled board becomes a charged error item with a code, never a silent gap in the dataset.
- Honest fields: `department`, `postedAt` and `compensation` stay `null` when a provider does not publish them; nothing is inferred.
- The quick-start input is the Store's daily automated test, so the Actor is verified every day against live boards.

### How to use

1. Enter your **Job boards**, one per line, either as `provider:boardId` or by pasting the career-page URL. The provider is detected from the hostname (`boards.greenhouse.io`, `job-boards.greenhouse.io`, `jobs.lever.co`, `jobs.ashbyhq.com`, `careers.smartrecruiters.com`, `apply.workable.com`, `<company>.workable.com`, `ats.rippling.com`, `jobs.gem.com`, `<company>.breezy.hr`, `<company>.teamtailor.com`, `<company>.jobs.personio.de`, `careers-page.com`, `careers.jobscore.com`). For US federal jobs add `usajobs:<agency code>` and fill in the **USAJOBS** section with your own key. You can also paste the company's own careers URL (`https://www.example.com/careers`): the Actor fetches that page once, follows redirects and detects the ATS board behind it. Or pick a **Company preset** (`ai-labs`, `developer-tools`, `fintech`) to add a curated, verified list of boards.
2. Pick a **Mode**: `jobs` for a full export, `signals` to detect changes since the last run. Optionally enable **Include job description**, lower **Max jobs per board**, set a **Webhook URL**.
3. Click **Start**. For signals, schedule the same task (daily or weekly); the first run writes a baseline, every later run writes only what changed plus a summary per board.

#### Quick start input

The same input the Store runs in its daily automated test, so it is cheap and always available; paste it into the JSON tab of the input form or send it as the body of an API or MCP call:

```json
{
  "boards": [
    "greenhouse:stripe",
    "lever:spotify"
  ],
  "mode": "jobs",
  "includeDescription": false,
  "maxJobsPerBoard": 50
}
```

#### Ready-to-run inputs

Seven inputs you can paste as they are; each validates against the input schema.

**Every open role on one Greenhouse board, with description text**

```json
{
  "boards": ["greenhouse:stripe"],
  "mode": "jobs",
  "includeDescription": true,
  "maxJobsPerBoard": 500
}
```

**A Lever board pasted as its careers URL**

```json
{
  "boards": ["https://jobs.lever.co/spotify"],
  "mode": "jobs",
  "includeDescription": false,
  "maxJobsPerBoard": 1000
}
```

**Weekly hiring signals for several companies, summary POSTed to a webhook** (schedule it weekly; the first run writes the baseline)

```json
{
  "boards": ["greenhouse:stripe", "lever:spotify", "ashby:openai"],
  "mode": "signals",
  "maxJobsPerBoard": 2000,
  "stateStoreName": "hiring-signals-weekly",
  "webhookUrl": "https://example.com/hooks/hiring-signals"
}
```

**A company careers URL instead of a board URL** (the Actor fetches the page once, detects the ATS behind it and scrapes that board; the careers page fetch counts as one board request)

```json
{
  "boards": ["https://www.anyscale.com/careers"],
  "mode": "jobs",
  "includeDescription": false,
  "maxJobsPerBoard": 500
}
```

**Weekly hiring signals for a whole sector with a company preset** (59 verified fintech and crypto boards; add your own boards to the list, duplicates are merged)

```json
{
  "boards": ["greenhouse:stripe"],
  "companyPreset": "fintech",
  "mode": "signals",
  "maxJobsPerBoard": 2000,
  "stateStoreName": "fintech-weekly"
}
```

**Boards on the newer ATSs plus careers sites without a supported ATS** (`extendedDetection` also looks for Rippling, Gem, Breezy HR, Teamtailor, Personio, Manatal and JobScore behind a careers URL, then reads the site's JobPosting markup)

```json
{
  "boards": ["personio:tado", "https://career.teamtailor.com/jobs", "rippling:rippling", "https://www.example.com/careers"],
  "extendedDetection": true,
  "mode": "jobs",
  "maxJobsPerBoard": 500
}
```

**Weekly federal hiring signals for two agencies on USAJOBS** (your own key and the email it was registered with; the key is a secret input and never appears in the output or the log)

```json
{
  "boards": ["usajobs:VA", "usajobs:HE38"],
  "usajobsApiKey": "<your USAJOBS API key>",
  "usajobsEmail": "you@example.com",
  "mode": "signals",
  "stateStoreName": "federal-weekly"
}
```

### Input

| Field | Type | Required | Description |
|---|---|---|---|
| `boards` | string\[] | yes, unless `companyPreset` is set | `provider:boardId` (greenhouse, lever, ashby, smartrecruiters, workable, rippling, gem, breezy, teamtailor, personio, manatal, jobscore, usajobs with an agency code, or `jsonld:<careers URL>`), a public board URL, or a company's own careers URL (`https://www.example.com/careers`): that page is fetched once (redirects followed, up to 5 MB read, 20 seconds including retries) and the Greenhouse, Lever, Ashby, SmartRecruiters or Workable board behind it is detected and scraped like any other board. The careers page fetch counts as one board request; a page with no detectable board becomes one `ATS_NOT_DETECTED` error item. Duplicates are merged. |
| `companyPreset` | `ai-labs` | `developer-tools` | `fintech` | no | Adds a curated list of boards to `boards` (duplicates by provider and board id are merged): `ai-labs` 50 boards, `developer-tools` 57, `fintech` 59, every one verified on 2026-10-27 against the public job-board APIs from the companies' own careers pages or ATS board URLs; the full list with sources is in `docs/presets.md` in the Actor's repository. Default none. |
| `extendedDetection` | boolean | no | Default `false`, so careers URLs behave exactly as before. When `true`, a careers URL with none of the original five boards is also checked for a Rippling, Gem, Breezy HR, Teamtailor, Personio, Manatal or JobScore board, and then for schema.org JobPosting markup (the page itself, the job pages it links on the same site, or the job URLs in the site's sitemap). robots.txt is honored. |
| `mode` | `jobs` | `signals` | no | `jobs` (default) lists every open job; `signals` diffs against the previous snapshot. |
| `includeDescription` | boolean | no | Add `descriptionText` (HTML → text, max 3,000 chars). Default `false`. SmartRecruiters and Workable need one extra request per job for this; Rippling and Breezy HR listings carry no description, so it stays `null` there. |
| `maxJobsPerBoard` | integer | no | Cap per board (default 2000). Boards over the cap are marked `truncated`; removed-role signals are suppressed for them. |
| `stateStoreName` | string | no | Named key-value store holding snapshots for signals mode (default `ats-hiring-signals-state`). |
| `webhookUrl` | string | no | POST target for the end-of-run summary (per-board summaries, counts, charges). |
| `maxConcurrency` | integer | no | Boards fetched in parallel (default 5). Same-host requests are additionally limited to ~2/s. |
| `maxSecondsPerBoard` | integer | no | Time budget per board in seconds (default 900, 30 to 3600). See "Limits and fair use". |
| `usajobsApiKey` | string (secret) | for `usajobs:` boards | Your own USAJOBS Search API key (free at developer.usajobs.gov). Sent only to `data.usajobs.gov` in a header; never logged or written to the output. The Actor ships no key. |
| `usajobsEmail` | string | for `usajobs:` boards | The email the key was registered with; USAJOBS requires it as the User-Agent of every request. |

### Output example

![Sample output of Hiring Signals & ATS Jobs Scraper — Greenhouse, Lever, Ashby as a table: the dataset overview columns from the quick-start run](https://api.apify.com/v2/key-value-stores/r8pLvIribNPHYI9gX/records/ats-hiring-signals-output.png)

A `job` record (`mode: jobs`, or baseline run in signals mode):

```json
{
  "type": "job",
  "provider": "greenhouse",
  "boardId": "stripe",
  "companyName": "Stripe",
  "jobId": "4012345007",
  "title": "Senior Backend Engineer, Payments",
  "department": "Engineering",
  "team": "Payments Platform",
  "location": "San Francisco, CA",
  "city": "San Francisco",
  "country": "United States",
  "remote": false,
  "employmentType": "full-time",
  "postedAt": "2026-09-01T16:00:00.000Z",
  "updatedAt": "2026-10-10T22:02:11.000Z",
  "applyUrl": "https://boards.greenhouse.io/stripe/jobs/4012345007",
  "jobUrl": "https://boards.greenhouse.io/stripe/jobs/4012345007",
  "descriptionText": null,
  "compensation": null,
  "fetchedAt": "2026-10-16T12:00:00.000Z"
}
```

Ashby and Lever jobs carry `compensation` as `{ "min": 295000, "max": 440000, "currency": "USD", "interval": "1 YEAR", "summary": "$295K - $440K" }` when the company publishes it.

Records from the newer sources have the same fields, with `provider` set to `rippling`, `gem`, `breezy`, `teamtailor`, `personio`, `manatal`, `jobscore`, `usajobs` or `jsonld`. USAJOBS and JobPosting records add `closesAt`, the application deadline when the source publishes one. On USAJOBS, `companyName` is the hiring organization, `department` the federal department and `team` the job series; on a JobPosting record, `boardId` is the careers URL the crawl started from. A USAJOBS record (pay as published by OPM):

```json
{
  "type": "job",
  "provider": "usajobs",
  "boardId": "VA",
  "companyName": "Veterans Health Administration",
  "jobId": "845100200",
  "title": "Registered Nurse - Medical Surgical",
  "department": "Department of Veterans Affairs",
  "team": "Nurse",
  "location": "Denver, Colorado",
  "city": "Denver",
  "country": "United States",
  "remote": false,
  "employmentType": "full-time",
  "postedAt": "2026-11-02T00:00:00.000Z",
  "updatedAt": null,
  "applyUrl": "https://www.usajobs.gov:443/GetJob/ViewDetails/845100200?PostingChannelID=",
  "jobUrl": "https://www.usajobs.gov:443/GetJob/ViewDetails/845100200",
  "descriptionText": null,
  "compensation": { "min": 84520, "max": 129855, "currency": "USD", "interval": "year", "summary": "Per Year" },
  "closesAt": "2026-11-16T23:59:59.997Z",
  "fetchedAt": "2026-11-07T12:00:00.000Z"
}
```

A `new-role` signal (the other signal types share the shape; `title-changed` adds `previousTitle`, `location-changed` adds `previousLocation`):

```json
{
  "type": "new-role",
  "provider": "lever",
  "boardId": "spotify",
  "jobId": "a1b2c3d4-0000-4000-8000-000000000001",
  "title": "Staff Software Engineer, Platform",
  "department": "Engineering",
  "location": "New York, NY",
  "remote": false,
  "applyUrl": "https://jobs.lever.co/spotify/a1b2c3d4-0000-4000-8000-000000000001/apply",
  "postedAt": "2026-10-14T10:26:40.000Z",
  "job": { "jobId": "a1b2c3d4-…", "title": "Staff Software Engineer, Platform", "team": "Platform", "city": "New York", "country": "United States" },
  "detectedAt": "2026-10-16T12:00:00.000Z"
}
```

A `board-summary` record (one per board per signals run):

```json
{
  "type": "board-summary",
  "provider": "lever",
  "boardId": "spotify",
  "companyName": null,
  "baseline": false,
  "openRoles": 312,
  "previousOpenRoles": 298,
  "newSinceLastRun": 21,
  "removedSinceLastRun": 7,
  "changedSinceLastRun": 3,
  "netChange": 14,
  "topDepartments": [{ "name": "Engineering", "count": 140 }, { "name": "Sales", "count": 45 }],
  "topLocations": [{ "name": "Stockholm, Sweden", "count": 120 }, { "name": "New York, NY", "count": 80 }],
  "remoteShare": 0.18,
  "hiringVelocityScore": 58,
  "truncated": false,
  "previousSnapshotAt": "2026-10-09T12:00:00.000Z",
  "detectedAt": "2026-10-16T12:00:00.000Z"
}
```

Error items look like `{ "type": "error", "provider": "lever", "boardId": "nonexistent", "error": "BOARD_NOT_FOUND", "errorMessage": "HTTP 404 ..." }`. Error codes: `BOARD_NOT_FOUND`, `UNKNOWN_PROVIDER`, `UNRECOGNIZED_URL`, `ATS_NOT_DETECTED` (a careers URL whose page names no supported board), `BLOCKED`, `WORKABLE_BLOCKED`, `RATE_LIMITED`, `TIMEOUT`, `FETCH_FAILED`, and for the newer sources `API_KEY_MISSING` and `INVALID_API_KEY` (USAJOBS), `ROBOTS_DISALLOWED`, `ROBOTS_UNAVAILABLE` and `NO_JOBPOSTING_MARKUP` (JobPosting crawl), `SOURCE_NOT_SUPPORTED` (Workday, Oracle and SuccessFactors sites). They are charged like a job record because the work to discover the problem was done.

The run's key-value store also gets a `SUMMARY` record with counts, charges and all board summaries; the same payload is POSTed to `webhookUrl` when set.

If the output saves you time, a review on the Store page helps other buyers find this Actor; the Store invites you to leave one after you have run it a few times.

### Alerts and delivery

Signals mode is built to run on a schedule; this is how the results reach the place you work in.

- **Schedule in the Console.** Save the input as a Task so it is stored, then create a schedule under **Schedules** (daily or weekly presets, or a cron expression) and attach the Task. Every run compares against the snapshot of the previous one; use a different `stateStoreName` for each schedule that watches the same boards.
- **The Actor's own webhook.** Set `webhookUrl` and the Actor POSTs one JSON document when the run finishes (10 second timeout, two retries; a failed delivery is recorded as `webhookDelivered: false` in the `SUMMARY` record and never fails the run). The body is the run summary: `actor`, `runId`, `mode`, `boards`, the counts `jobs`, `signals`, `summaries` and `errors`, `charges`, `limitReached`, `stateStoreName`, `results` (one entry per board: provider, board id, company name, baseline job count, signal count, truncation or error) and, in signals mode, `summaries` with the full `board-summary` records. Individual `job` and signal records are not in the body; read them from the dataset.
- **Apify run webhooks.** On the Actor's **Integrations** tab add a webhook for run events such as `ACTOR.RUN.SUCCEEDED`, `ACTOR.RUN.FAILED` or `ACTOR.RUN.TIMED_OUT`; Apify POSTs the run id and the default dataset id to your URL, which is all your own code needs to fetch the items.
- **Slack, Make, Zapier, Google Sheets, email.** The same **Integrations** tab lists Apify's ready-made integrations: a Slack message when a run finishes, Make and Zapier modules that start runs and read the dataset, an export of each run's dataset to a Google Sheet, and an email notification. They are configured per Actor or per task, without code.
- **API and dataset endpoints.** Start a run with `POST https://api.apify.com/v2/acts/owlabee~job-postings-api/runs`, read the newest results with `GET https://api.apify.com/v2/acts/owlabee~job-postings-api/runs/last/dataset/items?format=json` (also `csv` or `xlsx`, and `fields` to pick columns), or read any run's dataset at `https://api.apify.com/v2/datasets/<datasetId>/items`. For short runs `run-sync-get-dataset-items` starts the run and returns the items in one call. Filter on the `type` field (`new-role`, `removed-role`, `board-summary`, …) on your side.

### Pricing

Pay per event. You only pay for what the Actor produces; set a **maximum cost per run** in the run options to cap spend.

| Event | Price | Notes |
|---|---|---|
| Job record (`job-scraped`) | $0.001 | Per `job` record, including baseline job records on the first signals run and error items |
| Hiring signal (`hiring-signal`) | $0.005 | Per `new-role`, `removed-role`, `title-changed`, `location-changed` and per `board-summary` record |
| Board request (`board-fetched`) | $0.008 | Per request to a job board's listing API (one per page of up to 100 jobs on Lever and SmartRecruiters, one for the whole board on Greenhouse, Ashby and Workable), including requests that time out, are blocked or return no jobs; ATS job detail requests are not charged |
| Job page request (`job-page-fetched`) | $0.0005 | Per job page fetched while reading a careers site's schema.org JobPosting markup (`jsonld:` boards and `extendedDetection` fallbacks only), with or without a job on it, including pages that time out or carry no markup; never charged on ATS boards |
| Actor start | $0.00005 | Apify platform event |

**Examples:** 1,000 jobs ≈ **$1.00**. A weekly check of 50 companies ≈ 50 summaries = **$0.25** + $0.005 per detected change, after a one-off baseline charged at the per-job price (50 companies × ~100 jobs ≈ $5.00). A quiet week with 50 companies and 60 changes costs ≈ $0.55 plus about $0.40 for 50 board requests (≈ $0.95). A single-board run of 100 jobs ≈ $0.10 + $0.008 = **$0.108**.

**Board requests on the newer sources** use the same events: one board request for the whole board on Rippling, Gem, Breezy HR, Teamtailor, Personio and JobScore; one per page of 20 jobs on Manatal; one per page of up to 500 postings on USAJOBS. For JobPosting markup the careers page and each sitemap read (at most three) are board requests, and each job page fetched is one job page request ($0.0005) whether or not it carries a job, so a JobPosting job costs $0.0015 all-in (one request per job, against one request per board or per 100 jobs on an ATS API). robots.txt is never charged. A careers site with 50 jobs read from its markup ≈ $0.008 + 55 × $0.0005 + 50 × $0.001 ≈ **$0.09**; a signals check of that site with nothing changed ≈ 50 page requests + one summary ≈ **$0.04**.

Store pricing discounts: users on the Scale plan (Silver) pay 10% less per event, Business (Gold) 20% less.

### Limits and fair use

- Only the providers' **public, unauthenticated job-board APIs and feeds** are used: `boards-api.greenhouse.io`, `api.lever.co` (and `api.eu.lever.co`), `api.ashbyhq.com/posting-api`, `api.smartrecruiters.com/v1/companies`, `apply.workable.com/api`, `api.rippling.com/platform/api/ats/v1/board`, `api.gem.com/job_board/v0`, `<company>.breezy.hr/json`, `<company>.teamtailor.com/jobs.rss`, `<company>.jobs.personio.de/xml`, `www.careers-page.com/api/v1.0/c`, `careers.jobscore.com/jobs/<company>/feed.json`; plus `data.usajobs.gov/api/search` with your own key. Private or password-protected boards are not supported.
- **USAJOBS** needs your own key and its registration email (`usajobsApiKey`, a secret input, and `usajobsEmail`). The Actor never ships or shares a key; without one, each `usajobs:` board becomes one `API_KEY_MISSING` error item and nothing is requested. Each `usajobs:` entry is one agency code (`VA`, `HE38`, `AF`); USAJOBS's own rate limits apply to your key.
- **JobPosting markup** is read only from the careers site you name (the same registrable domain), never from job boards, aggregators, Workday, Oracle Recruiting Cloud or SAP SuccessFactors (those three are not supported yet). robots.txt is checked before the first page and for every page after it (a robots.txt that cannot be read because the server fails counts as "disallowed"), its Crawl-delay is honored, and pages are fetched one at a time. Job pages are read up to 512 KB with a 10 second deadline; after 5 job pages without markup, or once the crawl has spent more than 15 seconds plus 3 seconds per job found, it stops and keeps what it found (`truncated`). Careers sites that publish no markup give one `NO_JOBPOSTING_MARKUP` error item.
- **Not covered**, by design: Indeed, LinkedIn, Glassdoor, ZipRecruiter, Monster, Dice, Wellfound, Upwork and the remote-job aggregators. Their own rules forbid reuse of their listings, so their hosts are never fetched.
- Requests to each provider host are rate-limited to about 2 per second with exponential backoff on 429/5xx; very large boards or `includeDescription` on SmartRecruiters/Workable (one extra request per job) take proportionally longer.
- Workable's career-page API sits behind a WAF. The Actor first tries the v3 search endpoint, then falls back to the v1 widget endpoint; if both are blocked you get a `WORKABLE_BLOCKED` error item for that board and the run continues. No residential proxies are used.
- `city`/`country` are best-effort parses of free-text locations and may be `null`; `remote` is `true` when the provider flags it or the title/location says so.
- Snapshots in signals mode store only the fields needed to diff (no descriptions), so a board with 2,000 roles uses well under 1 MB of key-value storage. If a board hits `maxJobsPerBoard` the summary is marked `truncated` and removals are not reported for it.
- **Careers URL detection** reads one page per URL with a plain request (no browser), up to 5 MB and 20 seconds including retries, inside the board's time budget; without `extendedDetection` it never follows links beyond the page, and it never fetches aggregator or social-network hosts.
- **Time budget per board.** Each board gets `maxSecondsPerBoard` seconds (default 900) and is cut off earlier when it has produced nothing for 60 seconds (a stalling, timing-out or blocking provider). Every request, retries included, is limited to 20 seconds and a `Retry-After` longer than 10 seconds is not waited for. A cut-off board keeps the jobs already fetched (`truncated: "TIME_BUDGET"` in the run summary; in signals mode removals are not reported for it); a board that produced nothing becomes one `TIME_BUDGET` error item, charged like every error item. Remaining descriptions are left empty when the budget ends during detail requests.
- Default run memory is 512 MB (the platform default is set in the Actor's run options); Ashby and Lever boards are downloaded whole, so very large boards may need more.
- The Store's daily automated test runs with the prefilled boards (`greenhouse:stripe`, `lever:spotify`) in `jobs` mode and `maxJobsPerBoard: 50`.

### Related Actors

Other pay-per-event Actors from the same account, all limited-permission and agent-friendly:

- [SPF, DMARC & DKIM Checker — Bulk Domain WHOIS, DNS & SSL](https://apify.com/owlabee/spf-dmarc-dkim-checker) — enrich each hiring company's domain with registrar, mail provider, tech stack and TLS status.
- [Website Change Monitor — Sitemap Monitor & Page Diff](https://apify.com/owlabee/website-change-monitor) — watch a company's own careers or pricing pages for content changes between runs.
- [Grants.gov API & SAM.gov Scraper — Federal Grants Feed](https://apify.com/owlabee/grants-gov-api) — see which target accounts are bidding for or winning federal grants and contracts.

### Guide and code examples

- Guide: [Greenhouse, Lever and Ashby Jobs API and Hiring Signals](https://owlabee.com/job-board-api-hiring-signals/), covering the source, its fields and limits, and curl and MCP examples.
- Runnable examples (curl, Python, Node.js and an MCP config): [owlabee/owlabee on GitHub](https://github.com/owlabee/owlabee/tree/main/examples/job-postings-api).

### Use from AI agents (MCP)

Pin this Actor in the Apify MCP server with one link: `https://mcp.apify.com?tools=owlabee/job-postings-api`, or let the agent find it with the `search-actors` tool. It is pay-per-event with limited permissions, so agents can discover it, inspect the input schema with `fetch-actor-details` and pay per run through agentic payments. Prompts that map directly onto the input:

1. "Pull every open role from Stripe's Greenhouse board and Spotify's Lever board, including department, location and whether it is remote, as a CSV."
2. "Watch these 40 companies' career pages (Greenhouse, Lever, Ashby and SmartRecruiters URLs attached) every Monday and tell me which ones opened new sales or engineering roles since last week, with a hiring velocity score per company."
3. "Which of our target accounts started hiring for 'Head of Data' or 'Data Platform' roles in the last 30 days? Use their public job boards, not LinkedIn."

The agent receives the same dataset records shown in the output example above.

To call it from an agent that already has the Apify MCP server connected, copy this tool call and change only the `input` object (shown with the quick-start input; any input that validates in the Console works here too). The server returns the run's dataset items, and its per-call maximum total charge (`maxTotalChargeUsd`) caps spend the same way the run option does in the Console.

```json
{
  "tool": "call-actor",
  "arguments": {
    "actor": "owlabee/job-postings-api",
    "input": {
      "boards": [
        "greenhouse:stripe",
        "lever:spotify"
      ],
      "mode": "jobs",
      "includeDescription": false,
      "maxJobsPerBoard": 50
    }
  }
}
```

### FAQ

**Does this need proxies or an API key?** No. Greenhouse, Lever, Ashby and SmartRecruiters publish job-board APIs that need no credentials and work from datacenter IPs. Workable is the exception; see "Limits".

**How is cost controlled?** Set `maxJobsPerBoard`, keep `includeDescription` off unless you need the text, and set the run's maximum total charge. The Actor charges each record as it is written and stops cleanly when the limit is reached; in signals mode it then leaves the snapshot untouched so the next run re-detects the same changes.

**How do I use USAJOBS?** Request a free API key at developer.usajobs.gov, put it in **USAJOBS API key** (a secret input: it is stored encrypted, sent only to `data.usajobs.gov` and never written to the log or the output) and the email you registered with in **USAJOBS registration email**, then add one `usajobs:<agency code>` line per agency (`usajobs:VA`, `usajobs:HE38`). Each agency is treated as one board, so signals mode reports its new and closed openings run to run. The Actor does not ship a key of its own because the government's rules for the API do not allow keys to be shared.

**What is JobPosting markup and when is it used?** Many careers sites embed schema.org `JobPosting` data so search engines can show their openings; it carries the title, location, employment type, posted date, salary and deadline. The Actor reads it when you add `jsonld:<careers URL>`, or automatically with `extendedDetection` when a careers URL has no supported ATS board. It checks robots.txt first, then reads the careers page, the job pages it links on the same site (or the job URLs in the site's sitemap), one at a time; each job page read is one job page request ($0.0005) on top of the job records it yields. Workday, Oracle Recruiting Cloud and SAP SuccessFactors sites are not read yet.

**How do I find a company's board id?** Open the company's careers page and look at the URL of a job: `boards.greenhouse.io/<token>`, `jobs.lever.co/<company>`, `jobs.ashbyhq.com/<org>`, `jobs.smartrecruiters.com/<Company>/...`, `apply.workable.com/<subdomain>/...`. Paste that URL as-is; the Actor extracts the id. Or skip the lookup and paste the careers page itself; see the next question.

**Can I give a company website instead of a board URL?** Yes. Paste the company's own careers URL (`https://www.example.com/careers`) into `boards`. The Actor fetches that page once with a plain request, follows redirects (a `careers.` subdomain often lands straight on the ATS), and looks for a Greenhouse, Lever, Ashby, SmartRecruiters or Workable board in the final URL or the HTML: board and job links, the Greenhouse `for=` embed, the Ashby embed script, the Workable `whr_embed` snippet. The board it finds is then scraped exactly like a board you listed yourself, with the same records and events. The careers page fetch counts as one board request. Careers pages that render their job list only in the browser, without any link to the ATS in the HTML, cannot be detected: you get one `ATS_NOT_DETECTED` error item, charged like any error item, and the run continues; paste the board URL for those, or turn on `extendedDetection`, which also looks for the seven newer ATSs and then reads the site's JobPosting markup. Aggregator and social-network hosts are never fetched.

**What is in a company preset?** A curated list of boards for one theme: `ai-labs` (50 boards), `developer-tools` (57) and `fintech` (59). Every board was taken from the company's own careers page or the public ATS board URL it exposes, no directory or list site, and verified on 2026-10-27 to answer with at least one open job; the per-company table with sources is in `docs/presets.md` in the Actor's repository. Presets merge with your own `boards`, duplicates are dropped, and each board is charged like any other board. Boards move between ATS vendors over time, so a preset board that stops answering shows up as a `BOARD_NOT_FOUND` error item until the list is refreshed.

**What happens on the first signals run?** There is no previous snapshot, so the Actor writes every current job as a `job` record (charged at $0.001 each) plus a `board-summary` with `baseline: true`. Change events start with the second run. Use a different `stateStoreName` per schedule if you monitor the same board at different cadences.

**Can an AI agent use this?** Yes; see "Use from AI agents (MCP)" above for the pin link and example prompts. Schedules and webhooks work the same way from the Console or the API.

**Does it include closed or unlisted jobs?** No. Only jobs the provider currently lists publicly are returned; Ashby jobs with `isListed: false` are skipped. A job that disappears between runs shows up as `removed-role`.

**Why is `department` or `postedAt` null for some jobs?** Providers expose different fields: Greenhouse has no posted date on some boards (`first_published` can be null), Lever has no `updatedAt`, SmartRecruiters departments are optional. The Actor never guesses these.

**Can I try it for free?** Yes. The Apify free plan comes with monthly platform credit, and the quick-start input above costs a few cents per run, so you can test it on your own inputs before paying anything.

**How do I find this Actor again?** Bookmark it on the Store page (the bookmark icon next to the title); bookmarked Actors are listed in your Console, and the Store page shows new versions as they ship.

### Support

Questions, bugs or a source you'd like added? Open an issue on the Actor's **Issues** tab, or email jacob@owlabee.com.

### Changelog

- **2026-11-08** — Pricing: new `job-page-fetched` event ($0.0005; Silver $0.00045, Gold $0.0004) per job page fetched while reading schema.org JobPosting markup, with or without a job on it. Only `jsonld:` boards and `extendedDetection` fallbacks charge it; every ATS board, USAJOBS and the prefill cost exactly what they did before.
- **2026-11-07** — More sources, all opt-in or new board types, so saved inputs return the same results at the same cost: seven more ATSs (Rippling, Gem, Breezy HR, Teamtailor, Personio, Manatal, JobScore) as `provider:boardId` or board URL; USAJOBS agencies with your own API key (`usajobs:<agency code>`, new secret input `usajobsApiKey` and `usajobsEmail`); schema.org JobPosting markup from any careers site (`jsonld:<careers URL>`), honoring robots.txt; new `extendedDetection` input (off by default) that looks for the newer ATSs and then for JobPosting markup behind a careers URL. New optional `closesAt` field on USAJOBS and JobPosting records. More job-board and aggregator hosts are refused without a request (one `UNRECOGNIZED_URL` error item, as for LinkedIn and Indeed). Events and prices unchanged.
- **2026-11-07** — Renamed from `owlabee/ats-hiring-signals` to `owlabee/job-postings-api` so the address matches what people search for. Input, output and pricing are unchanged; calls by Actor ID, saved tasks and schedules are unaffected. Name-based calls to the old address stop resolving: update API URLs and MCP tool lists.
- **2026-10-27** — `boards` accepts a company's own careers URL: one plain fetch, ATS detected from the redirect or the HTML, then scraped like any board (the careers page fetch counts as one board request; `ATS_NOT_DETECTED` error item otherwise). New `companyPreset` input with three verified lists (`ai-labs` 50, `developer-tools` 57, `fintech` 59 boards). README: comparison table, ready-to-run inputs per ATS, alerts and delivery, MCP call example, changelog.
- **2026-10-25** — Pricing round: the board request became its own work-based event (charged even when a board returns nothing), event titles pluralized, Store discount tiers for the Scale and Business plans described; applied in the Console on 2026-10-26.
- **2026-10-23** — Time budget per board (`maxSecondsPerBoard`), 20 second request deadline including retries, idle cut-off, charged `TIME_BUDGET` error items, worst-case margin tests. Listing: new title, SEO fields, MCP section, quick start, task examples; Related Actors use the new display titles.
- **2026-10-17** — Public task examples published next to the Actor.
- **2026-10-16** — First release on the Store: five ATS providers, `jobs` and `signals` modes, end-of-run webhook summary.

### Legal

Only public job postings published by the companies themselves through their applicant-tracking systems are accessed, with no authentication and at polite rates. Use the data in compliance with each provider's terms and applicable law. This Actor is not a consumer reporting agency; its output describes job openings, not people, and must not be used for employment, credit, insurance or tenant screening decisions.

# Actor input Schema

## `boards` (type: `array`):

Company job boards to scrape, one per line. Use `provider:boardId` (greenhouse:stripe, lever:spotify, ashby:openai, smartrecruiters:Visa, workable:acme, rippling:acme, gem:acme, breezy:acme, teamtailor:acme, personio:acme, manatal:acme, jobscore:acme), paste the public board URL (boards.greenhouse.io/<token>, job-boards.greenhouse.io/<token>, jobs.lever.co/<company>, jobs.ashbyhq.com/<org>, careers.smartrecruiters.com/<Company>, apply.workable.com/<subdomain>, ats.rippling.com/<slug>/jobs, jobs.gem.com/<slug>, <company>.breezy.hr, <company>.teamtailor.com, <company>.jobs.personio.de, careers-page.com/<slug>, careers.jobscore.com/careers/<company>), or paste the company's own careers URL (for example https://www.example.com/careers): the Actor fetches that page once, detects the Greenhouse, Lever, Ashby, SmartRecruiters or Workable board behind it and scrapes it like any other board. The careers page fetch is charged as one board request; a page with no detectable board becomes one ATS_NOT_DETECTED error item unless `extendedDetection` is on. Also: `usajobs:<agency code>` (for example usajobs:VA) for US federal jobs with your own USAJOBS API key, and `jsonld:<careers URL>` to read a site's schema.org JobPosting markup directly.

## `companyPreset` (type: `string`):

Optional curated list of verified company boards added to `boards` (duplicates by provider and board id are merged): `ai-labs` (AI labs and model companies), `developer-tools` (developer tools and cloud infrastructure) or `fintech` (fintech and crypto). Each list has 50 or more boards verified against the public job-board APIs; the companies and the verification date are listed in the README and docs/presets.md.

## `extendedDetection` (type: `boolean`):

Off by default (careers URLs behave exactly as before). When on, a careers URL with no Greenhouse, Lever, Ashby, SmartRecruiters or Workable board is also checked for a Rippling, Gem, Breezy HR, Teamtailor, Personio, Manatal or JobScore board, and if there is none, the site's own schema.org JobPosting markup is read: from the careers page itself, from the job pages it links, or from job URLs in the site's sitemap. robots.txt is honored (a disallowed site becomes a ROBOTS_DISALLOWED error item), pages are fetched one at a time, and job pages are not charged, only the job records they yield (sitemaps are board requests). Workday, Oracle and SuccessFactors sites are not read.

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

`jobs` writes one normalized record per open job. `signals` compares each board with its previous snapshot (stored in the key-value store named below) and writes new-role, removed-role, title-changed and location-changed records plus one per-board summary. The first signals run for a board is a baseline and writes job records instead.

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

Add `descriptionText` (HTML converted to plain text, trimmed to 3,000 characters). Greenhouse, Lever, Ashby, Gem, Teamtailor, Personio, Manatal, JobScore, USAJOBS and JobPosting markup return descriptions in the same request; SmartRecruiters and Workable need one extra request per job, so large boards take longer. Rippling and Breezy HR listings carry no description.

## `maxJobsPerBoard` (type: `integer`):

Upper bound on job records (and snapshot size) per board. Boards that exceed it are marked `truncated` and removed-role signals are suppressed for them so a partial fetch never looks like mass closures.

## `stateStoreName` (type: `string`):

Named key-value store that holds the previous snapshot of each board for signals mode. Use a different name per schedule or customer to keep baselines separate.

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

If set, the Actor POSTs one JSON payload at the end of the run with per-board summaries and counts. Delivery failures are logged, never fatal.

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

How many boards are fetched in parallel. Requests to the same provider host are additionally rate-limited to about 2 per second.

## `maxSecondsPerBoard` (type: `integer`):

Time budget for one board (all pages and detail requests). A board that has produced nothing for 60 seconds, or uses up this budget, is cut off: the jobs already fetched are kept and the board is marked truncated (TIME_BUDGET); with nothing fetched it becomes one error item.

## `usajobsApiKey` (type: `string`):

Your own free USAJOBS Search API key (request one at developer.usajobs.gov), needed only for `usajobs:<agency code>` boards. It is sent only to data.usajobs.gov in the Authorization-Key header, never logged or written to the output. OPM's terms do not allow sharing a key, so the Actor never ships one.

## `usajobsEmail` (type: `string`):

The email address your USAJOBS API key was registered with. USAJOBS requires it as the User-Agent of every request; it is sent only to data.usajobs.gov.

## Actor input object example

```json
{
  "boards": [
    "greenhouse:stripe",
    "lever:spotify"
  ],
  "extendedDetection": false,
  "mode": "jobs",
  "includeDescription": false,
  "maxJobsPerBoard": 50,
  "stateStoreName": "ats-hiring-signals-state",
  "maxConcurrency": 5,
  "maxSecondsPerBoard": 900
}
```

# Actor output Schema

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

One item per job posting or hiring signal in the default dataset (type, provider, boardId, title, department, location, remote, salary, applyUrl, postedAt; error items carry error and errorMessage). Fields are described in dataset_schema.json.

## `summary` (type: `string`):

SUMMARY record in the run's default key-value store: boards processed, jobs and signals produced, charges and webhook delivery status, written when the run finishes.

# 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 = {
    "boards": [
        "greenhouse:stripe",
        "lever:spotify"
    ],
    "mode": "jobs",
    "includeDescription": false,
    "maxJobsPerBoard": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("owlabee/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 = {
    "boards": [
        "greenhouse:stripe",
        "lever:spotify",
    ],
    "mode": "jobs",
    "includeDescription": False,
    "maxJobsPerBoard": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("owlabee/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 '{
  "boards": [
    "greenhouse:stripe",
    "lever:spotify"
  ],
  "mode": "jobs",
  "includeDescription": false,
  "maxJobsPerBoard": 50
}' |
apify call owlabee/job-postings-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,owlabee/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/BdgjEVFQcT7yed9nv/builds/LOZPvAJwbNCwPEKut/openapi.json
