# Career Page Job Postings Scraper & API — Any Company, 14 ATS (`vonsensey/career-page-job-postings-scraper-api`) Actor

Job postings API for 14 ATS: Greenhouse, Lever, Ashby, Workday, Workable, BambooHR, SmartRecruiters, Teamtailor, Recruitee, Personio, Rippling, Breezy, Pinpoint, Dover. Paste a company domain — it finds the board and returns every open role in one schema, with structured salary and seniority.

- **URL**: https://apify.com/vonsensey/career-page-job-postings-scraper-api.md
- **Developed by:** [Blackcube Agency AB](https://apify.com/vonsensey) (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 $4.00 / 1,000 jobs

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

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Career Page Job Postings Scraper & API — Any Company, 14 ATS

**Give it a company domain. Get back every open role from that company's own career page — live, in one normalized schema.**

Paste `robinhood.com` and this Actor works out that Robinhood runs Greenhouse, finds the board, and returns all 121 open roles with salary ranges attached. Paste `ramp.com` and it finds Ashby. Paste `huggingface.co` and it finds Workable. Paste a list of 500 companies from your CRM and it does that for every one of them, then hands you a single flat table where a Greenhouse job, a Workday job and a Recruitee job all have exactly the same 34 columns.

No board slugs to look up. No "view page source and open the Network tab" instructions. No pre-built database that might be hours stale.

> **You pay per job returned. A company we can't resolve is free. A board with no open roles is free. A run that returns nothing costs nothing** — there is no start fee on any plan. Every company that produces no jobs still comes back as a row telling you why, so a gap in your list is explained rather than silent.

> **Unofficial.** Not affiliated with, endorsed by, or connected to any applicant tracking system or employer whose board it reads. It reads only the vacancies employers publish for the public on their own career pages.

***

### Why this one

#### 1. It finds the board for you

Every other multi-ATS scraper makes company discovery *your* problem — you supply the board slug, and if you don't know it, you go read the page source. That single friction point is why they stay small.

This one takes whatever you already have:

| You paste | What happens |
|---|---|
| `robinhood.com` | Detects Greenhouse, resolves the slug, returns the board |
| `https://figma.com/careers` | Reads the careers page, finds the linked board |
| `https://jobs.ashbyhq.com/ramp` | Uses the board directly |
| `greenhouse:figma` | Exact board, no detection needed |

On a 20-company benchmark of well-known employers, a bare domain was resolved to the right board **90% of the time** — including for the many companies whose careers page is a JavaScript app with no visible link to the board behind it. Where a company matters and resolution misses it, pass its board URL directly and it always works.

**A plausible match is not good enough, and you are never billed one.** Company names collide across platforms: an obvious-looking guess at one well-known employer's board lands on a live board of the same name belonging to somebody's unrelated test account, with real postings on it. Every resolved board is confirmed to belong to the company you actually asked for before a single row is billed. Where that cannot be confirmed you get a free `atsNotFound` row — because returning nothing is honest, and putting a stranger's postings on your invoice is not.

#### 2. Live from the source, not a database snapshot

The large incumbents in this category are not scrapers — they are pre-crawled databases you query, with a stated indexing delay and expired postings purged on a daily cycle. That is a reasonable design, and it means their rows can describe jobs that are already closed.

This Actor reads the employer's own board at the moment you run it. Every row carries `scrapedAt`, and every row was live on that board at that timestamp. If a job was pulled down an hour ago, it isn't here.

That is the difference between "a job that was open when we last crawled" and **a job that is open now**. No ghost jobs, no expired reposts, no aggregator duplicates — because the data never passes through an aggregator.

#### 3. Repeat runs only charge for what's new

A jobs feed gets polled. If you track 200 companies daily, naive per-row billing charges you the full price for the same 4,000 unchanged postings every single day.

Turn on **Only new or changed jobs since my last run** and the Actor returns — and bills for — only what actually moved. The first run returns the full board; every run after it returns the delta.

| | Day 1 | Day 2 | Day 30 |
|---|---|---|---|
| Full mode | 4,000 jobs | 4,000 jobs | 4,000 jobs |
| **Only new/changed** | 4,000 jobs | **~40 jobs** | **~40 jobs** |

**"Changed" is defined, not vague.** A posting counts as changed when its title, department, location, salary, employment type or workplace type differs from last time — so a role that *gains a published salary range* comes back to you instead of being silently withheld forever. Description edits deliberately do **not** count: employers re-render and re-word descriptions constantly, and billing a whole board again for a typo fix is precisely the surprise invoice this design exists to avoid.

Every row also carries `isNew`, `isChanged` and `firstSeenAt` whether or not the filter is on, so a scheduled run doubles as a hiring-signal feed: a new job requisition is a buying trigger, and this tells you the exact day it appeared.

Each feed keeps its **own** memory, derived from your company list, so a second schedule over an overlapping list does not start out reporting zero new jobs. Set **Feed name** explicitly to control that grouping yourself.

#### 4. One schema across all fourteen platforms

Fourteen ATS platforms publish fourteen completely different shapes of data. Some nest departments inside offices, some bury pay in free text, some date a posting only as "30+ days ago", some publish no dates at all — and normalizing that by hand is weeks of work that breaks again every time one of them changes.

Every one of them comes out of here with the identical 34 keys. That is the whole product — and it is verified by a test asserting the key sets are byte-identical across all fourteen, plus a live run across all fourteen that produced exactly one distinct key set over 579 jobs.

#### 5. Fields the boards don't actually publish

Roughly half the value is enrichment, because the raw feeds are thin:

- **`salaryMin` / `salaryMax` / `salaryCurrency` / `salaryPeriod`** — taken from structured pay data wherever a platform publishes it, and recovered from the posting itself where it doesn't. On Greenhouse, where most tools report no salary at all, **74% of rows in a live run carried a real range**. Hourly and annual pay are told apart correctly, so `$20.40/hour` and `$222,800/year` never end up in the same column pretending to be the same thing — the single most common way a salary filter silently lies to you. `salaryRaw` always keeps the original text, so a parse you disagree with is visible rather than hidden.
- **`seniority`** — normalized to 11 comparable levels (`intern` … `executive`), because almost no ATS publishes it at all. "Senior Staff Engineer" comes back as `staff`, not `senior`. Null where there is no real signal, never a guess.
- **`isRemote` / `workplaceType`** — normalized from whatever the board provides. Hybrid is deliberately **not** remote. `null` means the posting said nothing either way, which is a different answer from `false`.
- **`employmentType`** — one enum, so the same job is the same value on every platform instead of three spellings you have to reconcile yourself.
- **`applyUrl`** — the real application link on the employer's own ATS. Not a job-board mirror.

#### 6. Filters run before billing

Every filter is applied before a row is ever charged for. Pull only senior remote engineering roles posted in the last week and you pay for those, not for the 4,000 you discarded.

`maxJobs` is honoured **exactly** — never over, never silently under. When a cap is the reason results were truncated, the run log says so explicitly, so "not enough data" and "we throttled you" are never ambiguous.

***

### What each platform actually gives you

Published rather than glossed over, because a coverage claim you can't check is worth nothing. Measured on a real 579-job run across all fourteen platforms — these are field fill rates, not promises:

| Platform | Dept | Seniority | Location | Remote | Type | Salary | Posted | Apply URL |
|---|---|---|---|---|---|---|---|---|
| **Breezy** | 100% | 0% | 100% | 33% | 67% | **100%** | 100% | 100% |
| **Ashby** | 100% | 62% | 100% | 89% | 100% | **97%** | 100% | 100% |
| **Pinpoint** | 100% | 46% | 100% | 100% | 100% | **94%** | 0% | 100% |
| **Greenhouse** | 100% | 72% | 100% | 5% | 1% | **74%** | 100% | 100% |
| **Lever** | 100% | 80% | 100% | 100% | 50% | 62% | 100% | 100% |
| **Recruitee** | 100% | 90% | 100% | 100% | 0% | 50% | 100% | 100% |
| **Workable** | 100% | 83% | 100% | 100% | 87% | 48% | 100% | 100% |
| **SmartRecruiters** | 100% | 100% | 100% | 100% | 100% | 0% | 100% | 100% |
| **Personio** | 100% | 100% | 100% | 0% | 100% | 0% | 100% | 100% |
| **BambooHR** | 95% | 5% | 100% | 0% | 100% | 0% | 0% | 100% |
| **Dover** | 0% | 40% | 100% | 100% | 0% | 0% | 100% | 100% |
| **Teamtailor** | 0% | 66% | 86% | 0% | 3% | 0% | 100% | 100% |
| **Rippling** | 100% | 25% | 100% | 29% | 0% | 0% | 0% | 100% |
| **Workday** | 0% | 86% | 54% | 6% | 0% | 0% | 100% | 100% |

The zeroes are the platforms' limits, not shortcuts here: Rippling, Pinpoint and BambooHR publish no dates at all, and Teamtailor and Workday publish only a handful of fields per role. Where a field is unavailable you get an explicit `null` rather than a missing key or a fabricated value.

***

### How to scrape Greenhouse jobs

Set `companies` to any Greenhouse employer — `greenhouse:figma`, `https://job-boards.greenhouse.io/vercel`, or just `figma.com`.

Greenhouse is one of the boards where a naive read quietly loses most of the data: department, office and structured pay can all come back empty on a board that is in fact publishing every one of them. On one 567-posting board the difference was 0 rows with department and office versus all 567. Here you get the full record every time, whether or not you asked for descriptions — which is why the salary coverage in the matrix above is 74% rather than the 0% a simpler reader reports.

### How to scrape Lever jobs

Pass `lever:ro`, `https://jobs.lever.co/ro`, or the company domain. Lever is strong on workplace type, commitment and country, and all three land in the normalized schema. Many Lever teams state pay in prose instead of a pay field — those ranges are recovered too, so you are not left filtering on a column that looks half empty.

### How to scrape Ashby jobs

Pass `ashby:ramp`, `https://jobs.ashbyhq.com/ramp`, or `ramp.com`. Ashby is among the richest boards of the fourteen: compensation with currency and interval, an explicit remote flag, employment type, department and team — 97% salary coverage in a live run. Bonus and equity are excluded, so `salaryMin`/`salaryMax` always mean base salary and stay comparable across employers.

### How to scrape Workable jobs

Pass `workable:huggingface`, `https://apply.workable.com/huggingface`, or the company domain. Workable names the employer on every posting and is strong on structured city, state and country, a genuine remote flag, and an experience level — one of the better boards here for filtering before you pay.

### How to scrape SmartRecruiters jobs

Pass `smartrecruiters:<company>` or the company domain. Paginated automatically. SmartRecruiters publishes structured city/region/country plus an explicit remote flag and an experience level.

### How to scrape Recruitee jobs

Pass `recruitee:hygraph`, `https://hygraph.recruitee.com`, or the company domain. Recruitee is one of the few boards with a genuinely structured salary object (min, max, period, currency) — delivered as strings, converted here to numbers. It can flag a posting both remote and hybrid; remote wins, so `workplaceType` never contradicts `isRemote`.

### How to scrape Teamtailor jobs

Pass `teamtailor:southpole`, `https://southpole.teamtailor.com`, or the company domain. Teamtailor names the employer and location on every role and has the strongest Nordic coverage here. Be aware it publishes thinly — no department and no employment type — as the matrix above shows; both come back `null` rather than invented.

### How to scrape Personio jobs

Pass `personio:<company>` or the company domain. Personio publishes its own seniority and schedule fields, which most boards do not, and they come through in the same schema as everything else. Strong DACH coverage that US-centric competitors under-serve.

### How to scrape Rippling jobs

Pass `rippling:<company>` or `https://ats.rippling.com/<company>/jobs`. Rippling's board is minimal — title, department, location and URL. It publishes no dates, and `postedAt` comes back `null` rather than fabricated.

### How to scrape Breezy jobs

Pass `breezy:<company>` or the company domain. Breezy is one of the few boards with a plain-text salary field and an explicit remote flag, both of which are parsed.

### How to scrape Dover jobs

Pass `dover:<company>` or an `app.dover.com` board URL. Dover is a smaller board used mostly by early-stage startups, and it comes through in the same 34-column schema as everything else — useful when your target list mixes big employers with seed-stage ones.

### How to scrape BambooHR jobs

Pass `bamboohr:<company>` or a `<company>.bamboohr.com` URL. BambooHR is the
largest under-served platform in this category — roughly 30,000 customers against
the ~7,000 the biggest rival indexes.

One trap worth knowing, because it decides whether you can trust a result: a
subdomain that is **not** a BambooHR customer redirects to their marketing site,
which answers `200` with a normal-looking HTML page. A status check alone would
mark it a valid board. This Actor checks the response body, so a wrong slug comes
back as a free `boardNotFound` row rather than as an empty board — "this company
has no openings" and "this is not their board" are different answers.

Its list is thin: department, location and employment type, but no dates and no
salary. Nulls, not guesses.

### How to scrape Pinpoint jobs

Pass `pinpoint:<company>` or a `<company>.pinpointhq.com` URL. Pinpoint is one of
the best-structured boards of the fourteen: **94% of postings in a live run carried
a real salary range**, with currency and period as separate fields, plus department,
workplace type and employment type.

It also publishes a compensation-visibility flag. Where an employer chose not to
show pay, this Actor leaves the salary fields null rather than publishing what the
employer deliberately withheld.

### How to scrape Workday jobs

Workday is set up per employer, so it needs a real board URL rather than a bare domain — for example `https://nvidia.wd5.myworkdayjobs.com/en-US/NVIDIAExternalCareerSite`. Paste the URL exactly as it appears in your browser and the rest is handled.

Workday publishes thinly at the list level: no department, no employment type, dates only as "30+ days ago", and the literal text `"3 Locations"` in place of a city where a role spans several sites. Relative dates are converted to an approximate real date, and `"3 Locations"` is never recorded as a place — that field comes back `null` rather than wrong, so your location filter stays trustworthy. Turn on full descriptions to fill in the description, the true posted date and the employment type.

***

### Ready-made company lists

Don't have a list yet? Pick a preset and run it immediately. Every company in these was verified against its live board:

| Preset | Companies |
|---|---|
| `top-tech` | 28 — Stripe, Figma, Databricks, Discord, Cloudflare, Coinbase, Robinhood, Airtable … |
| `ai-ml` | 22 — OpenAI, Anthropic, Notion, Ramp, Linear, Supabase, PostHog, Zapier … |
| `devtools` | 22 — Vercel, Netlify, Grafana, Sentry, HashiCorp, ClickHouse, Retool … |
| `fintech` | 16 — Brex, Mercury, Wise, Monzo, Klarna, Adyen, Carta, Deel … |

***

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| **companies** | array | `["stripe.com"]` | Domains, careers URLs, board URLs, or `ats:slug` pairs — mix them freely. |
| **presets** | array | *(empty)* | Curated verified company bundles, merged with the above. |
| **onlyNewJobs** | boolean | `false` | Return only postings that are new, or whose material fields changed. |
| **postedWithinDays** | integer | *(unset)* | Keep postings published within N days. |
| **titleKeywords** | array | *(empty)* | Job title must contain one of these. |
| **excludeTitleKeywords** | array | *(empty)* | Job title must not contain any of these. |
| **locationKeywords** | array | *(empty)* | Matches any location on a multi-site posting. |
| **remoteOnly** | boolean | `false` | Remote roles only. Hybrid and unknown are excluded. |
| **seniority** | array | *(empty)* | 11 levels from `intern` to `executive`. |
| **employmentType** | array | *(empty)* | `fullTime`, `partTime`, `contract`, `internship`, `temporary`, `volunteer`. |
| **departments** | array | *(empty)* | Department contains one of these. |
| **hasSalary** | boolean | `false` | Only postings with a parsed salary range. |
| **maxJobs** | integer | *(unset)* | Hard cap for the run. Honoured exactly. |
| **maxJobsPerCompany** | integer | *(unset)* | Cap per company, newest first. |
| **includeDescription** | boolean | `false` | Add full description as HTML and text. |
| **atsPlatforms** | array | *(all)* | Restrict detection to specific platforms. |
| **maxConcurrency** | integer | `10` | Boards fetched in parallel. |
| **stateKey** | string | *(derived)* | Names this feed's change-tracking memory. Derived from your company list when empty. |

***

### Output

One row per job posting. See [`.actor/sample-output.json`](.actor/sample-output.json) for complete records from a real run.

```jsonc
// abridged from a real run
{
  "jobId": "ashby:ashby:7458d4e9-da2e-47bd-98cb-adfda43d42b2",
  "ats": "ashby",
  "companySlug": "ashby",
  "companyDomain": "ashbyhq.com",
  "title": "Engineering Manager - EU",
  "department": "Engineering",
  "team": "EMEA Engineering",
  "employmentType": "fullTime",
  "seniority": "manager",
  "location": "Remote - European Union",
  "locations": ["Remote - European Union", "Spain", "Italy", "Germany"],
  "isRemote": true,
  "workplaceType": "remote",
  "salaryMin": 110000,
  "salaryMax": 185000,
  "salaryCurrency": "EUR",
  "salaryPeriod": "year",
  "salaryRaw": "€110K - €185K",
  "postedAt": "2026-06-02T09:14:11.000Z",
  "applyUrl": "https://jobs.ashbyhq.com/ashby/7458d4e9.../application",
  "isNew": true,
  "isChanged": false,
  "firstSeenAt": "2026-08-13T08:41:04.718Z",
  "scrapedAt": "2026-08-13T08:41:04.718Z"
}
```

**Multi-location postings are one row, not many.** A role open in six cities returns a single record with six entries in `locations` — not six near-identical rows you get charged six times for and then have to deduplicate yourself.

**`jobId` is stable across runs**, so it works as a primary key for an incremental sync into your own database.

#### Dataset views

**Jobs** and **Companies with no jobs (free)**. Views project columns but cannot filter rows, so both kinds appear in both with the other's columns blank. Split on `errorReason` in your own code: it is absent on a job row and set on a free row.

***

### When a company returns no jobs

Every one of these is a **free** row — never billed — that says what happened.

| `errorReason` | Meaning |
|---|---|
| `atsNotFound` | No supported ATS found, or the board found did not belong to that company. |
| `boardNotFound` | The board no longer exists at that address — slug changed, or the company moved ATS. |
| `boardEmpty` | Board reached fine; the company currently has no open roles. |
| `noJobsMatchedFilters` | Jobs exist, but none matched your filters. |
| `requestFailed` / `timeout` / `blocked` | The board could not be read; `errorMessage` has the detail. |
| `invalidInput` | Not a usable domain, board URL, or `ats:slug` pair. |
| `chargeLimitReached` | The run hit its maximum total charge before reaching this company. |

***

### What it's used for

- **Sales and GTM** — a new job requisition is a buying trigger. Track your target account list and learn the day a company starts hiring for the role that signals budget.
- **Recruiting and talent** — monitor a set of employers for openings the aggregators haven't picked up yet, with the real apply URL.
- **Niche job boards** — back a board with roles verified open at the moment you pulled them. No reposts, no dead links, and `onlyNewJobs` keeps the daily refresh cheap.
- **Competitive and market intelligence** — headcount direction, which teams a rival is growing, which markets they are opening.
- **Investors** — hiring velocity across a portfolio, refreshed on a schedule.

***

### Honest limits

- **Coverage is the fourteen platforms listed.** A company on an ATS not covered here, or on a fully custom careers page, returns a free `atsNotFound` row. It is not silently dropped.
- **Resolving a bare domain to the right board works about 90% of the time** on a 20-company benchmark, not 100%. If a company matters and detection misses it, pass its board URL directly and it will always work.
- **Per-platform field coverage varies a lot** — see the matrix above. It is published so you can check it rather than discover it after paying.
- **`postedAt` on Workday is approximate**, derived from a relative string.
- **Change tracking is per feed, per Apify account**, and remembers a posting for 120 days.
- This Actor reads job listings only. It does not collect candidate or applicant data of any kind.

***

### Legal & responsible use

This Actor reads **only publicly published job listings** — the vacancies employers deliberately publish for candidates to find. It does not log in, bypass authentication, solve captchas, or touch applicant data.

Job postings are company information rather than personal data, which keeps this well clear of the GDPR questions that surround people-data scraping. Where a description happens to name a hiring manager, that text is reproduced exactly as the employer published it and is not enriched, cross-referenced, or resolved to a person.

You are responsible for how you use the output, including any outreach you base on it.

***

*Unofficial project. Uses only publicly available data. Not affiliated with any applicant tracking system or employer.*

# Actor input Schema

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

The companies to pull job postings from — one per line. Paste whatever you already have: a company domain (stripe.com), a careers URL (https://figma.com/careers), a board link (https://jobs.ashbyhq.com/ramp), or an exact ats:slug pair (greenhouse:figma). For a plain domain the Actor finds the company's applicant tracking system itself, so you never have to look up board slugs. Companies whose ATS cannot be found return a free row explaining why.

## `presets` (type: `array`):

Curated company bundles you can run without researching a single slug. Every company in these lists is verified against its live board. Merged with anything in Companies above and de-duplicated.

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

Return only postings that are new, or whose title, department, location, salary, employment type or workplace type changed since a previous run of this feed. Description edits do not count as a change, so a re-worded posting is never re-charged. This is what makes a daily or hourly schedule cheap: the first run returns the full board, and every run after it returns — and charges for — only what genuinely moved. Leave it off for a one-off full export.

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

Keep only postings published within this many days. Leave empty for all open roles. Postings whose board publishes no date are excluded when this is set, because they cannot be proven fresh.

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

Keep only jobs whose title contains one of these words (case-insensitive). For example: engineer, designer, account executive. Leave empty for every open role.

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

Drop jobs whose title contains any of these words. Applied after the include filter above. Useful for stripping intern, contract, or non-English duplicates out of a feed.

## `locationKeywords` (type: `array`):

Keep only jobs matching one of these locations. Every location on a multi-site posting is checked, not just the first one, so a role open in both Berlin and Lisbon matches either.

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

Keep only roles the board marks as remote. Hybrid roles are excluded, and so are roles where nothing on the posting says either way — 'remote only' should not return maybes.

## `seniority` (type: `array`):

Keep only these seniority levels. Seniority is inferred from the job title, because almost no ATS publishes it as a field. Entry level means genuinely junior and graduate roles — not a '0-2 years' bucket that returns experienced ones.

## `employmentType` (type: `array`):

Keep only these employment types, normalized across every platform's own vocabulary (Lever's 'Full-time', Ashby's 'FullTime' and Personio's 'permanent' all become fullTime).

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

Keep only jobs whose department matches one of these, for example: engineering, sales, marketing.

## `hasSalary` (type: `boolean`):

Keep only postings where a real salary range could be parsed into numbers. Boards differ a lot here — Ashby publishes structured pay, most others only mention it in prose, and some never do.

## `maxJobs` (type: `integer`):

Hard cap on billable jobs for the entire run. The Actor stops at exactly this number — never over, and it tells you in the log when a cap is the reason results were truncated. Leave empty for no cap.

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

Cap per company, applied after filtering and newest-first sorting, so a handful of very large boards cannot dominate a run. Leave empty for every matching role.

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

Add the complete job description as both HTML and plain text. Off by default because descriptions are large and most feeds do not need them. On Workday and SmartRecruiters this needs one extra request per job, so those boards get noticeably slower — the price per job does not change.

## `atsPlatforms` (type: `array`):

Only detect and fetch these applicant tracking systems. Leave empty for all of them. Narrowing this makes auto-detection faster when you already know which platform your list uses.

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

How many company boards to fetch in parallel. The default is deliberately polite to the career sites being read; raise it for very large lists.

## `stateKey` (type: `string`):

Names the memory used by 'Only new or changed jobs'. Leave empty and it is derived from your company list, so two different lists automatically keep separate memories. Set it explicitly when you run several schedules over overlapping companies and want each to track changes independently — or set the same name on two tasks to share one memory.

## Actor input object example

```json
{
  "companies": [
    "stripe.com",
    "figma.com",
    "ramp.com"
  ],
  "presets": [],
  "onlyNewJobs": false,
  "titleKeywords": [],
  "excludeTitleKeywords": [],
  "locationKeywords": [],
  "remoteOnly": false,
  "seniority": [],
  "employmentType": [],
  "departments": [],
  "hasSalary": false,
  "includeDescription": false,
  "atsPlatforms": [],
  "maxConcurrency": 10
}
```

# Actor output Schema

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

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "companies": [
        "stripe.com",
        "figma.com",
        "ramp.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("vonsensey/career-page-job-postings-scraper-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": [
        "stripe.com",
        "figma.com",
        "ramp.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("vonsensey/career-page-job-postings-scraper-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": [
    "stripe.com",
    "figma.com",
    "ramp.com"
  ]
}' |
apify call vonsensey/career-page-job-postings-scraper-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,vonsensey/career-page-job-postings-scraper-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/YdBZdIXLt3T5mNIu4/builds/TknpdKIEvyPBamDSW/openapi.json
