# Career Site Jobs Scraper — Greenhouse, Lever, Workday (`adderleydata/career-site-jobs-scraper`) Actor

Every open role at the companies you name, from their Greenhouse, Lever, Ashby, Workday, Recruitee, Breezy HR, Rippling, Workable, BambooHR, Personio, JazzHR or Pinpoint job board, in one schema. Give board links, domains or names. Incremental mode charges only for what changed. No personal data.

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

## Pricing

from $1.60 / 1,000 posting saveds

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

### What does Career Site Jobs Scraper do?

Career Site Jobs Scraper returns every open role at the companies you name, as structured data you can load straight into a spreadsheet, a database or a model. Most companies do not host their own job listings: their careers page is a front for an applicant tracking system (ATS). This Actor reads twelve of them — **Greenhouse, Lever, Ashby, Workday, Recruitee, Breezy HR, Rippling, Workable, BambooHR, Personio, JazzHR and Pinpoint** — where each one publishes a company's jobs for its careers page: a public API or feed on eleven of them, the career page itself on JazzHR.

Give it a list. Each entry can be a job-board link (`https://jobs.lever.co/acme`), a source and a board name (`lever:acme`), a company domain (`acme.com`) or just a company name (`Acme Robotics`). For every company it finds, you get each posting's title, department, location with its country, workplace type, employment type, pay range where stated, posted and updated dates and a link — and, if you ask for it, the full text. Every row names the ATS it came from, in one schema whatever the source.

What makes it different:

- **Twelve ATSs, one list, one schema.** Greenhouse, Lever, Ashby, Workday, Recruitee, Breezy HR, Rippling, Workable, BambooHR, Personio, JazzHR and Pinpoint boards sit in one dataset, each row with its `source`. The same fields Adderley Data's single-source Actors return, so nothing downstream changes when a company moves ATS.
- **Names and domains are looked up for you.** A company name or domain is tried as a board name on the nine ATSs that can be asked by name: Greenhouse, Lever, Ashby, Recruitee, Breezy HR, Rippling, BambooHR, Personio and JazzHR. A source that has no board by that name answers "not found", plainly, once: that is an answer, not an error, it is never retried and it is never charged. A company that turns up nowhere is named in the run's log and summary.
- **Workday, Workable and Pinpoint by link.** Boards on these three are never guessed from a name. Give the board link (`acme.wd5.myworkdayjobs.com/External`, `apply.workable.com/acme`, `acme.pinpointhq.com`) or pin the source (`workable:acme`), and the Actor reads it exactly as Adderley Data's Actor for that source does, pay and dates included. **Questions** says why each one needs its link.
- **Incremental mode.** Put the Actor on a schedule and each run returns only postings that are new, changed, back again or gone. Unchanged postings are skipped and **not charged**.
- **One stable schema.** Every row has every field, every time. Unknown is `null`, never a missing key. The schema is versioned (`job.v1`).
- **No personal data.** The Actor reads no recruiter fields from any source and has nowhere to put one. Contact details inside posting text are redacted by default.
- **Bounded cost.** You set a maximum number of results; the run stops there. It also stops at the spending limit you set on the run in Apify.

### What data can you extract from company career sites?

| Field | What it holds |
| --- | --- |
| `id` | Stable across runs: `source:market:sourceJobId`. Use it as your primary key. |
| `title` | Job title as listed. |
| `company.name` | The hiring company, where the listing names one. |
| `advertiser.name` | The business that placed the listing — often a recruitment agency. Never a person. |
| `location.raw` | Location text as listed. Several locations are joined with `\|`. |
| `location.suburb` | Suburb, when the listing states one. |
| `location.city` | City or area, when the listing states one. |
| `location.region` | State or region, e.g. `VIC`. |
| `location.postcode` | Postcode, when the source provides it. |
| `location.country` | ISO 3166-1 alpha-2 country code. |
| `workArrangement` | `on_site`, `hybrid`, `remote` or `unknown`. |
| `employmentTypes` | Normalised: `full_time`, `part_time`, `contract`, `casual`, `temporary`, `internship`, `volunteer`. |
| `salary.raw` | The salary text exactly as shown, or null when the listing shows none. |
| `salary.min` | Lower bound as a number, when the text contains one. |
| `salary.max` | Upper bound as a number. Equal to min for a single figure. |
| `salary.currency` | ISO 4217. Taken from the text, otherwise the market default. |
| `salary.period` | `hour`, `day`, `week`, `month` or `year`; null when the text does not say. |
| `salary.includesSuper` | true / false when the text says so ("plus super", "inc. super"); otherwise null. |
| `classifications` | The source's category and subcategory pairs. |
| `teaser` | The short summary shown on the results page. |
| `bulletPoints` | Selling points shown on the results page. |
| `postedAt` | When the listing was posted, ISO 8601 UTC. |
| `updatedAt` | The source's own last-modified time, ISO 8601 UTC. Published by ATS and API sources; null where the site does not show one. |
| `expiresAt` | Expiry, ISO 8601 UTC, where the source states one. |
| `isPromoted` | true for paid placements. A listing shown both promoted and organic is returned once. |
| `url` | Link to the listing. |
| `description` | Null unless requested. `text`, optional sanitised `html`, and `contactsRedacted`. |
| `changeType` | Incremental runs: `NEW`, `UPDATED`, `REAPPEARED`, `EXPIRED` (or `UNCHANGED` if you ask for those). Otherwise null. |
| `firstSeenAt` | Incremental runs: when this monitor first saw the listing. |
| `contentHash` | SHA-256 over the fields that define a change. Compare it to detect edits yourself. |
| `scrapedAt` | When this row was produced, ISO 8601 UTC. |
| `source` | Source key, e.g. `seek`. |
| `market` | Market key, e.g. `au`, `nz`. |
| `sourceJobId` | The source's own identifier for the listing. |
| `company.sourceCompanyId` | The source's identifier for the company, when exposed. |
| `company.url` | The company's page on the source site, when exposed. |
| `advertiser.sourceAdvertiserId` | The source's identifier for the advertiser. |
| `schemaVersion` | Always `job.v1`. Breaking changes ship as `job.v2` in a new Actor version, never silently. |

How the sources fill the schema:

- `source` names the ATS: `greenhouse`, `lever`, `ashby`, `workday`, `recruitee`, `breezy`, `rippling`, `workable`, `bamboohr`, `personio`, `jazzhr` or `pinpoint`. `id` is `source:market:sourceJobId`, stable from run to run.
- `company.sourceCompanyId` is the company's board name on that ATS — the part you would put after `lever:` to read that board again directly.
- `postedAt` is the source's own publication date. `updatedAt` is the source's last-modified time where it publishes one (Greenhouse, Ashby, Recruitee); elsewhere it is `null`.
- `salary` holds the pay the posting states, as numbers with a currency and a period, and the words it was stated in. Nothing is estimated and currencies are never converted.
- Workday, Rippling, BambooHR, JazzHR and Pinpoint boards list less than a row holds, so each saved posting's own record or page is read for the rest — its posted date, and on most of them its pay — whether or not you ask for its text. That is part of the price of a row, not an extra charge.
- Personio boards are read in English. Personio keeps a job's text per language, so a job its company wrote in another language only — German, on many Personio boards — comes with its facts and `description` set to `null`; the Personio Jobs Scraper reads other languages.
- Each source's own details — how Workday states places, how Rippling lists several pay ranges, how Breezy HR states a currency — are exactly those of Adderley Data's Actor for that source, and are described in its README.

### How much does it cost to scrape company career sites?

You pay per posting saved to your dataset — **$2.00 per 1,000 postings** on Apify's Starter plan — plus $0.005 each time a run starts. There is no monthly rental. Looking a company up costs nothing per result: a source that has no board by the name asked is never charged.

| Apify plan | Price | Per listing |
| --- | --- | --- |
| Free | $2.00 per 1,000 listings | $0.00200 |
| Starter (Bronze) | $2.00 per 1,000 listings | $0.00200 |
| Scale (Silver) | $1.80 per 1,000 listings | $0.00180 |
| Business (Gold) | $1.60 per 1,000 listings | $0.00160 |

Plus $0.005 per run start. Compute and proxy are included in these prices.

| What you run | Cost (USD, Starter plan) |
| --- | --- |
| 100 listings, one run | $0.21 |
| 1,000 listings, one run | $2.00 |
| 10,000 listings, one run | $20.00 |
| 50,000 listings, one run | $100.00 |
| A daily incremental monitor finding about 150 new or changed listings a day, for a month | $9.15 |

Use incremental mode for anything you run more than once — after the first run you pay only for what changed.

### How to scrape the careers pages of a list of companies

1. Collect your companies. The fastest entry is a board link: open the company's careers page, click any job, and copy the address it opens — `boards.greenhouse.io/…`, `jobs.lever.co/…`, `jobs.ashbyhq.com/…`, `….myworkdayjobs.com/…`, `….recruitee.com`, `….breezy.hr`, `ats.rippling.com/…`, `apply.workable.com/…`, `….bamboohr.com`, `….jobs.personio.de`, `….applytojob.com` or `….pinpointhq.com`. A domain or a plain company name works too, except for a Workday, Workable or Pinpoint board; see **Questions** for how it is looked up.
2. Open the Actor in Apify Console and go to the **Input** tab. Paste one company per line into **Companies**. Up to 500 per run, of which up to 200 names or domains.
3. Optionally filter: **Title keywords**, **Locations**, **Departments**, **Published within (days)**.
4. Set **Maximum results**. This is also your cost cap, across all companies together.
5. Press **Start**. When the run finishes, open the **Output** tab and export as JSON, CSV, Excel, XML or HTML, or read the dataset through the Apify API. The run's summary (the `RUN_SUMMARY` record) lists what each company was found as.

A posting found twice — the same company given as a link and as a name — is returned once.

### Input

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `companies` | array | — | One entry per company, in any of these forms: a job-board link on any of twelve sources — boards.greenhouse.io/acme, jobs.lever.co/acme, jobs.ashbyhq.com/acme, acme.wd5.myworkdayjobs.com/External, acme.recruitee.com, acme.breezy.hr, ats.rippling.com/acme, apply.workable.com/acme, acme.bamboohr.com/careers, acme.jobs.personio.de, acme.applytojob.com/apply or acme.pinpointhq.com (read exactly as given); a source and a board name such as "lever:acme" (read on that source only); a company domain such as acme.com, or a careers-page link on it (its name is taken from the domain); or a company name such as "Acme Robotics". A domain or a name is looked up as a board name on Greenhouse, Lever, Ashby, Recruitee, Breezy HR, Rippling, BambooHR, Personio and JazzHR. Workday, Workable and Pinpoint boards always need their link or a pin such as "workable:acme". Up to 500 entries per run, of which up to 200 domains or names. |
| `keywords` | array | — | Keep postings whose title contains every word of any keyword, in any order — "engineer data" matches "Senior Data Engineer". On Workday boards each keyword is a search on the board itself. Leave empty for all titles. |
| `locations` | array | — | Keep postings whose location contains this text, e.g. "Melbourne", "Canada", "Remote". Leave empty for all locations. |
| `departments` | array | — | Keep postings in a department whose name contains this text, e.g. "Engineering". Leave empty for all departments. |
| `postedWithinDays` | integer | — | Keep postings first published in the last N days. Rippling, BambooHR, JazzHR and Pinpoint boards list no dates, so this filter is not applied to them; Workday boards state a posting's age only up to 30 days, so a longer window reads every Workday posting. Leave empty for any time. |
| `maxResults` | integer | `100` | The run stops once this many postings are saved, across all companies. You are charged per posting saved, so this is also your cost cap. |
| `includeDescription` | boolean | `false` | On: the full posting text comes back with each row. On Greenhouse, Lever, Ashby, Recruitee, Workable, Personio and Pinpoint it arrives with the board itself, and on Workday, Rippling, BambooHR and JazzHR with the posting's own record, which is read anyway: no extra request. On Breezy HR it costs one request per posting saved. Personio text is read in English. Off: listing fields only. |
| `descriptionFormat` | `text`, `text_and_html` | `"text"` | Plain text, or plain text plus sanitised HTML. |
| `redactContacts` | boolean | `true` | On by default: email addresses, phone numbers and personal profile links inside description text are replaced with \[redacted]. This Actor never outputs recruiter names or contact fields. |
| `incremental` | boolean | `false` | Remember what earlier runs saw and save only postings that are new, changed or gone. Unchanged postings are skipped and not charged. Put the Actor on a schedule with this on. |
| `stateKey` | string | — | Optional name for this monitor, e.g. "competitor-hiring". Runs with the same key share memory. Left empty, a key is derived from the companies and filters themselves. |
| `emitExpired` | boolean | `true` | Incremental mode only. When a complete run no longer finds a posting it saw before, save one row with changeType EXPIRED. |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Saves (and charges for) every posting, labelled UNCHANGED where nothing moved. |
| `proxyConfiguration` | object | `{"useApifyProxy":true}` | Apify Proxy, automatic group, is the default and is what this Actor is tested with. |
| `maxConcurrency` | integer | `4` | Parallel requests. The default is deliberately modest. |
| `maxRequestsPerMinute` | integer | `90` | An upper bound on request rate across the whole run, all sources together. |

A typical input:

```json
{
  "companies": [
    "Ashby",
    "Rippling",
    "https://boards.greenhouse.io/greenhouse"
  ],
  "maxResults": 100
}
```

### Output

One row per posting. This is a synthetic example in the exact shape the Actor returns — a Greenhouse board found from the name "Example Freight":

```json
{
  "schemaVersion": "job.v1",
  "id": "greenhouse:global:7000101",
  "source": "greenhouse",
  "market": "global",
  "sourceJobId": "7000101",
  "url": "https://job-boards.greenhouse.io/example-freight/jobs/7000101?gh_jid=7000101",
  "title": "Data Analyst",
  "company": {
    "name": "Example Freight Co",
    "sourceCompanyId": "example-freight",
    "url": "https://job-boards.greenhouse.io/example-freight"
  },
  "advertiser": {
    "name": "Example Freight Co",
    "sourceAdvertiserId": "example-freight"
  },
  "location": {
    "raw": "Melbourne, Victoria, Australia",
    "suburb": null,
    "city": "Melbourne",
    "region": "Victoria",
    "postcode": null,
    "country": "AU"
  },
  "workArrangement": "unknown",
  "employmentTypes": [],
  "salary": {
    "raw": "The salary range for this role is A$95,000 – A$110,000 per year plus super.",
    "min": 95000,
    "max": 110000,
    "currency": "AUD",
    "period": "year",
    "includesSuper": false
  },
  "classifications": [
    {
      "category": "Data",
      "subcategory": null
    },
    {
      "category": "Analytics",
      "subcategory": null
    }
  ],
  "teaser": null,
  "bulletPoints": [],
  "postedAt": "2026-09-20T22:14:05.000Z",
  "updatedAt": "2026-09-21T03:02:11.000Z",
  "expiresAt": null,
  "isPromoted": false,
  "description": null,
  "changeType": "NEW",
  "firstSeenAt": "2026-09-21T19:30:12.000Z",
  "contentHash": "1aa33a7fa1171769f261473be1b6a98c450af56dde37f9c785a47fb084b189ce",
  "scrapedAt": "2026-09-21T19:30:12.000Z"
}
```

The Output tab has two table views: **Overview** (the fields most people want, flattened) and **Changes** (for incremental runs).

### Incremental mode: monitor hiring across a list of companies

Turn on **Incremental mode** and run the same input on a schedule — hourly, daily, weekly. The Actor keeps a small record of what it has seen and every row tells you what happened:

| `changeType` | Meaning |
| --- | --- |
| `NEW` | First time this monitor has seen the posting |
| `UPDATED` | Seen before, and something the source reports about it has changed |
| `REAPPEARED` | Was reported as expired and is back |
| `EXPIRED` | Seen before and no longer on the company's board. One row, once |
| `UNCHANGED` | Only if you turn on **Also save unchanged postings** |

How it behaves, so there are no surprises:

- The first run returns everything as `NEW`. From the second run you pay only for the difference.
- What counts as a change is the source's own: on Greenhouse, Ashby and Recruitee an edit anywhere in a posting moves its last-modified time, and that is reported; on Workable, Personio and Pinpoint an edit to a posting's text or to any fact its board lists is reported; on Workday, Rippling, BambooHR and JazzHR, change is judged from what the board's list shows (title, place, department), so an edit only to a posting's text or pay is not reported.
- `EXPIRED` is only ever reported by a complete run. If a run hits your result cap or your spending limit, or a board cannot be read, nothing is declared expired. A source answering "no board by that name" is an answer, so it does not make a run incomplete — and a board that was found before and has now gone reports its postings as expired, which is what happened.
- For a monitor, prefer board links over names: a link is read exactly as given every run, while a name is looked up again each time.
- Runs share memory when they share a **State key**. Leave it empty and the key is derived from the companies and filters themselves, so the same input always continues the same monitor.
- A posting not seen for 45 days is forgotten.

### Descriptions and contact details

Full descriptions are off by default. Turn on **Include full descriptions** and each row carries `description.text` (and sanitised `description.html` if you choose that format). On Greenhouse, Lever, Ashby, Recruitee, Workable, Personio and Pinpoint the text arrives with the board itself and costs no extra request; on Workday, Rippling, BambooHR and JazzHR it comes with the posting's own record, which is read for every saved posting anyway; on Breezy HR it costs one request per posting saved.

Postings sometimes contain a recruiter's email address or phone number. With **Redact contact details** on — the default — those are replaced with `[redacted]` and `description.contactsRedacted` is `true`; so is a personal profile address (linkedin.com/in/…). The optional HTML keeps each link's words and drops its address, and drops images. The Actor never returns recruiter names or contact details as fields, under any setting: it reads no such fields from any source, and the schema has nowhere to put one. If your use case is contacting individuals, this is the wrong tool.

### What people use it for

- **Competitor and market hiring signals.** Which companies are opening which roles, in which departments and countries — a daily monitor across a list of companies is one scheduled run, whatever ATS each one uses.
- **Job boards and alert products.** A clean feed of new postings from a curated list of employers, deduplicated and labelled by change.
- **Pay-transparency datasets.** Stated ranges across companies and roles, as numbers with their currency and period, and the words they were stated in.
- **Sales and partnership research at company level.** Growth signals from hiring, without collecting anything about the individuals involved.
- **Research and teaching.** A clean, repeatable dataset with a documented schema.

### Using the API

Run it from code with the Apify client, using your own API token:

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('adderleydata/career-site-jobs-scraper').call({"companies":["Ashby","Rippling","https://boards.greenhouse.io/greenhouse"],"maxResults":100});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.length, items[0]?.source, items[0]?.title);
```

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("adderleydata/career-site-jobs-scraper").call(run_input={"companies":["Ashby","Rippling","https://boards.greenhouse.io/greenhouse"],"maxResults":100})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["source"], item["company"]["name"], item["title"], item["location"]["country"])
```

Schedules, webhooks and the Make, Zapier, n8n and Google Sheets integrations all work the way they do for any Apify Actor. The Actor runs with limited permissions and is priced per event, so AI agents can call it through Apify's MCP server as well.

### Is it legal to scrape company career sites?

The Actor reads the public job boards that Greenhouse, Lever, Ashby, Workday, Recruitee, Breezy HR, Rippling, Workable, BambooHR, Personio, JazzHR and Pinpoint serve for their customers' own careers pages — with no login, no key and no rate tricks — at a modest default rate, and returns facts about job postings. It does not log in, does not solve CAPTCHAs, and does not collect personal information. It does not fetch the companies' own websites: a domain is used only to make a board name.

What you do with the data is your responsibility. Each company's postings are its own copyright — analyse them, do not republish them. If your project touches personal information, privacy law applies to you wherever you are. This is general information, not legal advice.

### Questions

**How does it find a company from its name?** It makes a board name from what you give it — `Acme Robotics Pty Ltd` becomes `acmerobotics` and `acme-robotics` (legal words like Pty, Ltd, Inc and GmbH are dropped); `careers.acme.com.au` becomes `acme` — and asks Greenhouse, Lever, Ashby, Recruitee, Breezy HR, Rippling, BambooHR, Personio and JazzHR whether they have a board by that name. Every source that has one is read. That is up to eighteen small requests per name, none of them charged. A Recruitee board that holds only Recruitee's own sample postings is treated as not found when a name is looked up: Recruitee's trial accounts under large brands' names hold nothing else.

**Can a name match the wrong company?** Yes. A board name is only a name, and a common word can belong to different companies on different sources: in a check on 25 September 2026, "jobs" matched boards on Ashby, Recruitee and Breezy HR. Every row carries its `source` and `company.name`, and the run summary lists every board each name matched. When you need certainty, give the board link, or pin the source: `lever:acme` reads Lever's `acme` board and nothing else.

**A company came back as "No job board found".** None of the nine sources asked has a board under the names tried. The company may use a different board name (open any job on its careers page and copy the link), a Workday, Workable or Pinpoint board (which always need their link), or an ATS this Actor does not read. The run log and summary name each such company; the other companies in the run are unaffected.

**Why do Workday, Workable and Pinpoint need a link?** Each for its own reason:

- **Workday:** a board is a tenant, a numbered data centre and a site name — `acme.wd5.myworkdayjobs.com/External` — and nothing public maps a company's name to those three.
- **Workable:** its servers refuse an address that sends more than a handful of requests in quick succession, and looking up a list of names there would pass that within the first few companies. Workable boards given by link are read one every six seconds at most, whatever the run's rate. A board Workable refuses with a 429 (too many requests) is waited for — as long as Workable asks, or 30 seconds — and asked again from the same address, twice at most; the Actor never switches address to get round the limit.
- **Pinpoint:** some Pinpoint sites named after well-known brands are demonstrations that list Pinpoint's own made-up sample jobs, so a name looked up there could return, and charge you for, postings no company has.

A link or a pin (`workable:acme`, `pinpoint:acme`) reads the board directly, as it does on every source.

**Does it read the company's own careers page?** No. A careers page's job list is drawn by the ATS, so the Actor reads the ATS directly; a domain is only used to make a board name. A board found only on a careers page would have to be found again on every run, and a page redesign would read as every posting expiring.

**Is `postedWithinDays` applied everywhere?** On every source but Rippling, BambooHR, JazzHR and Pinpoint, whose boards list no dates. Their rows still carry `postedAt` where the posting's own record states it, so filter on it in your dataset. Workday states a posting's age only up to 30 days, so a window longer than 30 days reads every Workday posting.

**Why is a board I gave "not found"?** The source says it has no board by that name — a typo, a board the company has closed (BambooHR and JazzHR say so for an expired or inactive account), or a Lever or Rippling name with different capitals (those two are case-sensitive). It is asked once, not retried, and named in the log.

**Does it need a login or an API key?** No. Every job board it reads is public.

**Can I get recruiter emails or phone numbers?** No, by design.

**How current is the data?** It is read from each source while your run is in progress. `postedAt` is the source's publication date; `scrapedAt` records when the row was produced.

**The ATS I need is not here.** Open an issue on the **Issues** tab and name it. Sources are added without breaking existing fields.

### Support

Use the **Issues** tab on this page. We read it every day. Include the run ID and what you expected to see.

### Other Adderley Data Actors

Every Actor in a vertical returns the same fields, so adding a source needs no new code on your side.

- [Ashby Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/ashby-jobs-scraper) — same `job.v1` fields
- [BambooHR Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/bamboohr-jobs-scraper) — same `job.v1` fields
- [Breezy HR Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/breezy-jobs-scraper) — same `job.v1` fields
- [Greenhouse Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/greenhouse-jobs-scraper) — same `job.v1` fields
- [Lever Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/lever-jobs-scraper) — same `job.v1` fields
- [Personio Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/personio-jobs-scraper) — same `job.v1` fields
- [Pinpoint Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/pinpoint-jobs-scraper) — same `job.v1` fields
- [Recruitee Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/recruitee-jobs-scraper) — same `job.v1` fields
- [Rippling Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/rippling-jobs-scraper) — same `job.v1` fields
- [Workday Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/workday-jobs-scraper) — same `job.v1` fields

### About

Made by Adderley Data, Melbourne — https://adderleydata.com. Not affiliated with, endorsed by or sponsored by Greenhouse, Lever, Ashby, Workday, Recruitee, Breezy HR, Rippling, Workable, BambooHR, Personio, JazzHR, Pinpoint or any company whose job board it reads. Their names are trade marks of their owners and are used here only to describe what this Actor reads.

# Changelog

This Actor's version history is a separate document: https://apify.com/adderleydata/career-site-jobs-scraper/changelog.md

# Actor input Schema

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

One entry per company, in any of these forms: a job-board link on any of twelve sources — boards.greenhouse.io/acme, jobs.lever.co/acme, jobs.ashbyhq.com/acme, acme.wd5.myworkdayjobs.com/External, acme.recruitee.com, acme.breezy.hr, ats.rippling.com/acme, apply.workable.com/acme, acme.bamboohr.com/careers, acme.jobs.personio.de, acme.applytojob.com/apply or acme.pinpointhq.com (read exactly as given); a source and a board name such as "lever:acme" (read on that source only); a company domain such as acme.com, or a careers-page link on it (its name is taken from the domain); or a company name such as "Acme Robotics". A domain or a name is looked up as a board name on Greenhouse, Lever, Ashby, Recruitee, Breezy HR, Rippling, BambooHR, Personio and JazzHR. Workday, Workable and Pinpoint boards always need their link or a pin such as "workable:acme". Up to 500 entries per run, of which up to 200 domains or names.

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

Keep postings whose title contains every word of any keyword, in any order — "engineer data" matches "Senior Data Engineer". On Workday boards each keyword is a search on the board itself. Leave empty for all titles.

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

Keep postings whose location contains this text, e.g. "Melbourne", "Canada", "Remote". Leave empty for all locations.

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

Keep postings in a department whose name contains this text, e.g. "Engineering". Leave empty for all departments.

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

Keep postings first published in the last N days. Rippling, BambooHR, JazzHR and Pinpoint boards list no dates, so this filter is not applied to them; Workday boards state a posting's age only up to 30 days, so a longer window reads every Workday posting. Leave empty for any time.

## `maxResults` (type: `integer`):

The run stops once this many postings are saved, across all companies. You are charged per posting saved, so this is also your cost cap.

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

On: the full posting text comes back with each row. On Greenhouse, Lever, Ashby, Recruitee, Workable, Personio and Pinpoint it arrives with the board itself, and on Workday, Rippling, BambooHR and JazzHR with the posting's own record, which is read anyway: no extra request. On Breezy HR it costs one request per posting saved. Personio text is read in English. Off: listing fields only.

## `descriptionFormat` (type: `string`):

Plain text, or plain text plus sanitised HTML.

## `redactContacts` (type: `boolean`):

On by default: email addresses, phone numbers and personal profile links inside description text are replaced with \[redacted]. This Actor never outputs recruiter names or contact fields.

## `incremental` (type: `boolean`):

Remember what earlier runs saw and save only postings that are new, changed or gone. Unchanged postings are skipped and not charged. Put the Actor on a schedule with this on.

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

Optional name for this monitor, e.g. "competitor-hiring". Runs with the same key share memory. Left empty, a key is derived from the companies and filters themselves.

## `emitExpired` (type: `boolean`):

Incremental mode only. When a complete run no longer finds a posting it saw before, save one row with changeType EXPIRED.

## `emitUnchanged` (type: `boolean`):

Incremental mode only. Saves (and charges for) every posting, labelled UNCHANGED where nothing moved.

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

Apify Proxy, automatic group, is the default and is what this Actor is tested with.

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

Parallel requests. The default is deliberately modest.

## `maxRequestsPerMinute` (type: `integer`):

An upper bound on request rate across the whole run, all sources together.

## Actor input object example

```json
{
  "companies": [
    "Ashby",
    "https://boards.greenhouse.io/greenhouse"
  ],
  "maxResults": 20,
  "includeDescription": false,
  "descriptionFormat": "text",
  "redactContacts": true,
  "incremental": false,
  "emitExpired": true,
  "emitUnchanged": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "maxConcurrency": 4,
  "maxRequestsPerMinute": 90
}
```

# Actor output Schema

## `postings` (type: `string`):

One row per posting in the job.v1 schema; every key is always present and unknown is null. In incremental runs each row carries a changeType of NEW, UPDATED, REAPPEARED or EXPIRED.

## `runSummary` (type: `string`):

Requests made, first-attempt success, rows saved and skipped, rows that failed validation, and contact details redacted.

# 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": [
        "Ashby",
        "https://boards.greenhouse.io/greenhouse"
    ],
    "maxResults": 20,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("adderleydata/career-site-jobs-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "companies": [
        "Ashby",
        "https://boards.greenhouse.io/greenhouse",
    ],
    "maxResults": 20,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("adderleydata/career-site-jobs-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "companies": [
    "Ashby",
    "https://boards.greenhouse.io/greenhouse"
  ],
  "maxResults": 20,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call adderleydata/career-site-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,adderleydata/career-site-jobs-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/wbboJcV8Cc4siHIYL/builds/5FodzaLaUDgyNiat3/openapi.json
