# Greenhouse, Lever, Ashby & Workday Jobs: Careers Page Scraper (`yourevenings/careers-jobs-api`) Actor

Greenhouse, Lever, Ashby & Workday jobs API: filter by title, location, remote and posting date before you pay. Also Workable, SmartRecruiters and Recruitee.

- **URL**: https://apify.com/yourevenings/careers-jobs-api.md
- **Developed by:** [Your Evenings](https://apify.com/yourevenings) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.60 / 1,000 job returneds

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

### Why this one

- **Filters that actually apply, before you're charged.** Filter by title keywords (and excludes), location, remote, department and posted date. Jobs your filters remove are never returned or charged, and each company's row shows how many were filtered out. Matching ignores accents, handles German compound titles ("entwickler" finds "Softwareentwickler") and knows local place names and remote words (München = Munich, Homeoffice = remote).
- **Workday career sites too, including boards with more than 2,000 jobs.** Give a company name, a domain or a `myworkdayjobs.com` link. It reads the career site's own public job search and works around Workday's 2,000-results-per-search limit; if any jobs can't be read, the company's note says how many and why.
- **Live from each job board, not a stale daily copy.** Every run reads each company's board directly, so you get the jobs it lists at the moment of the run.
- **Every company you enter gets a status, and you can see the board's full size.** The `COMPANY-STATUS` CSV has one line per input, including repeats and companies not reached, with its status, the board's open-job count, jobs returned, jobs filtered out and a note. Every job row carries `companyTotalJobs`. If a board token you gave has moved to another supported job board, the jobs are read from the new board and the status says `board_moved`. Companies we can't find are free.
- **Your limits are honoured, with no hidden extra runs or double charges.** `maxJobsPerCompany` caps rows per company in normal mode, charging stops at your maximum cost per run, one run never starts other runs, and two inputs that point to the same board are read and charged once (the second gets a free `duplicate` row).
- **Alerts for new and removed jobs.** Turn on `monitorMode` and schedule it daily: you get only jobs opened or closed since the last run, with `firstSeenAt` and `lastSeenAt`.
- **No AI-guessed fields.** Salary and workplace type are copied from the board's own fields, and salary is filled only when the employer posts it.

## Greenhouse, Lever, Ashby & Workday Jobs API: Careers Page Scraper

**Works with:** Greenhouse · Lever · Ashby · Workday · Workable · SmartRecruiters · Recruitee · Personio · Teamtailor

**Runs on** <img src="https://yourevenings.com/logos/apify-logo-dark.svg" alt="Apify" height="20"> Apify. Use it from Make, Zapier, <img src="https://yourevenings.com/logos/n8n-logo-black.svg" alt="n8n" height="16"> n8n or the Apify API.

<sub>Product names and logos belong to their owners. Your Evenings Ltd is not affiliated with or endorsed by them.</sub>

**Greenhouse, Lever, Ashby & Workday Jobs API** turns a list of company domains into a clean, up-to-date list of their open jobs. Give it `stripe.com`, a careers page URL or a job-board link. It finds the company's applicant tracking system (ATS) and reads every open job from the ATS's **public job-board API** (for Workday, the career site's own public job search), then returns one normalised row per job: title, department, location, remote flag, employment type, salary (when the employer posts it), job URL, apply URL and dates.

One Actor reads the public job boards of exactly these nine applicant tracking systems:

| ATS | Typical board URL | Notes |
|---|---|---|
| Greenhouse | `boards.greenhouse.io/{token}`, `job-boards.greenhouse.io/{token}` | Posted pay ranges included |
| Lever | `jobs.lever.co/{token}` (and the EU instance `jobs.eu.lever.co`) | Posted salary range included |
| Ashby | `jobs.ashbyhq.com/{token}` | Posted compensation included |
| Workable | `apply.workable.com/{token}` | |
| SmartRecruiters | `jobs.smartrecruiters.com/{token}` | |
| Recruitee | `{token}.recruitee.com` | Posted salary included |
| Workday | `{tenant}.wd{N}.myworkdayjobs.com/{site}`, `wd{N}.myworkdaysite.com/recruiting/{tenant}/{site}` | Department from the job family. Location shows "3 Locations" for multi-location jobs unless a location or remote filter is set (then each job's page is read). Posted date for the last 30 days. No salary fields |
| Personio | `{token}.jobs.personio.de` (or `.com`) | **Only when the employer has switched on Personio's public XML job feed** (see below). Posted salary included |
| Teamtailor | `{token}.teamtailor.com`, or the employer's own career-site domain (e.g. `jobs.tibber.com`) | Read from the career site's public RSS job feed (see below). **No salary, employment type or update date in the feed** |

**Not covered yet:** BambooHR, iCIMS, SuccessFactors, Taleo, Jobvite and companies' own in-house careers sites. Companies on those systems are returned as `not_found` (free of charge).

Use it as a **Greenhouse jobs API**, a **Lever jobs scraper**, an **Ashby jobs API** or a **Workday jobs scraper** without building separate integrations, or as a **careers page jobs** feed for a list of target accounts.

### What you can use it for

- **Hiring signals for sales and recruiting teams.** Turn on monitor mode and schedule it daily: you get only the jobs that were opened or closed since the last run.
- **Job boards and aggregators.** Pull fresh jobs straight from employers' own boards in one schema.
- **Market and salary research.** Compare roles, locations, remote share and posted pay ranges across companies.
- **Talent intelligence.** See which teams a company is growing, and where.

### Example tasks (ready-made inputs)

Open one, click **Try for free**, then swap in your own companies:

- [Track open jobs at top AI startups](https://apify.com/yourevenings/careers-jobs-api/examples/ai-startup-jobs)
- [Export Greenhouse job boards to CSV or JSON](https://apify.com/yourevenings/careers-jobs-api/examples/greenhouse-jobs-export)
- [Collect posted salary ranges from tech job boards](https://apify.com/yourevenings/careers-jobs-api/examples/posted-salary-ranges)
- [Daily hiring alerts for your target accounts](https://apify.com/yourevenings/careers-jobs-api/examples/hiring-signals-monitor)
- [Find open jobs at UK and European fintechs](https://apify.com/yourevenings/careers-jobs-api/examples/uk-europe-fintech-jobs)
- [Export Workday jobs to CSV or JSON](https://apify.com/yourevenings/careers-jobs-api/examples/workday-jobs-export)

### How it finds each company's jobs

For every line of input the Actor:

1. Uses the board directly if you give an explicit token (`greenhouse:stripe`) or a job-board URL (`https://jobs.lever.co/spotify`). If that board no longer exists or lists no jobs, the same token is tried on the other supported job boards (see [Moved boards](#moved-boards)).
2. Otherwise derives likely board tokens from the domain (for example `figma.com` → `figma`) and checks them against each ATS's public API. Boards whose company name or job links clearly belong to a different organisation are skipped.
3. If that fails, reads up to 4 pages of the company's website (your careers URL, `/careers`, `/jobs` and the homepage) and looks for links to a supported job board, including Workday career sites.
4. If that fails too, checks whether Workday has a tenant with that name: each Workday host's public `robots.txt` says whether the tenant exists and lists its career sites. The main external site (the one with the most jobs, skipping intern, campus and internal sites) is used.
5. If nothing is found, returns one row with `status: "not_found"`. Not-found companies are free.

Each row says how its board was found (`detectionMethod`) and how sure the Actor is that the board is the right company (`detectionConfidence`):

- **high**: you gave the token or board URL; the company's website links to the board; the board's job links are on the company's own domain; or the board's company name matches the domain exactly.
- **medium**: a token guessed from the domain answered on Lever, Ashby or Workday. These boards publish no company name to cross-check, so verify them once.
- **low**: the board's company name only partly matches the domain. Check these before relying on them. For the most reliable runs, pass the board token or board URL once you know it; the `boardToken` field in the output gives you the value to reuse.

### Input

| Field | What it does | Default |
|---|---|---|
| `companies` | Domains, careers URLs, job-board URLs or `ats:token` values, one per line | 5 example companies |
| `includeDescription` | Adds a plain-text `descriptionText` field (emails and phone numbers removed) | off |
| `maxJobsPerCompany` | Caps rows per company (0 = all jobs), applied after the filters below | 0 |
| `titleKeywords` | Keep jobs whose title contains any of these words or phrases | none |
| `excludeTitleKeywords` | Drop jobs whose title contains any of these words or phrases | none |
| `locations` | Keep jobs whose location contains any of these places | none |
| `remoteOnly` | Keep only remote jobs | off |
| `departments` | Keep jobs whose department or team contains any of these texts | none |
| `postedWithinDays` | Keep jobs posted within the last N days (0 = any date) | 0 |
| `keepJobsWithoutPostedDate` | With `postedWithinDays` set, keep jobs whose board gives no posted date | on |
| `monitorMode` | Return only new and removed jobs since the previous run | off |
| `emitBaselineOnFirstRun` | In monitor mode, return all current jobs on the first run | on |
| `stateStoreName` | Named key-value store that remembers seen jobs | `careers-jobs-api-state` |
| `persistState` | Keep `firstSeenAt` accurate across normal runs | on |
| `scanCareersPage` | Look for job-board links on the company website if guessing fails | on |
| `maxConcurrency` | Max parallel HTTP requests | 5 |
| `requestTimeoutSecs` | Per-request timeout; failed requests are retried 3 times with backoff | 15 |

Example input:

```json
{
  "companies": [
    "figma.com",
    "https://www.notion.so/careers",
    "https://jobs.lever.co/spotify",
    "greenhouse:stripe",
    "ashby:ramp"
  ],
  "includeDescription": false,
  "monitorMode": false
}
```

Accepted explicit prefixes: `greenhouse:`, `lever:`, `lever-eu:`, `ashby:`, `workable:`, `smartrecruiters:`, `recruitee:`, `personio:`, `workday:`, `teamtailor:`.

### Job filters (applied before you're charged)

The Actor reads each board in full, applies your filters, and only then pushes and charges job rows. Jobs a filter removes are never returned or charged. All filters are optional and combine with AND; values inside one filter combine with OR.

```json
{
  "companies": ["stripe.com", "figma.com"],
  "titleKeywords": ["engineer", "engineering"],
  "excludeTitleKeywords": ["intern"],
  "locations": ["London", "Remote"],
  "postedWithinDays": 7
}
```

How each filter matches:

- **`titleKeywords` / `excludeTitleKeywords`**: whole words or phrases in the job title, ignoring case and accents (`developpeur` matches "Développeur"). `engineer` matches "Software Engineer" but not "Engineering Manager", so list both forms if you want both. A single word of 5 or more letters also matches at the end of a compound word and with the German endings -in/-innen: `entwickler` matches "Softwareentwickler (m/w/d)" and "Entwicklerin", and `berater` matches "Steuerberater". It doesn't match at the start of a word, so `java` doesn't match "JavaScript". There's no translation: `developer` doesn't match "Entwickler".
- **`locations`**: whole words in the board's location text, ignoring case and accents (`Zurich` matches "Zürich"). About 50 groups of place names are matched together: local and English names (München/Munich, Köln/Cologne, Wien/Vienna, Warszawa/Warsaw, Lisboa/Lisbon, Deutschland/Germany, Schweiz/Suisse/Switzerland and others), short forms (NYC/New York, UK/United Kingdom/England/Scotland/Wales/Northern Ireland, USA/US/United States, UAE, SF/San Francisco) and old names (Bangalore/Bengaluru, Bombay/Mumbai). A city isn't matched to its country, so `United Kingdom` does not match a job listed only as "London". `Remote` also matches jobs the board flags as remote.
- **`remoteOnly`**: keeps jobs with `isRemote: true` or `workplaceType: "remote"`. Hybrid jobs are not included. Remote words in other languages count when they're in the location or the board's workplace field: Homeoffice, mobiles Arbeiten, ortsunabhängig, télétravail, teletrabajo, remoto, teletrabalho, thuiswerk, telelavoro, smart working, praca zdalna, på distans, etätyö and similar. Text that also says hybrid stays hybrid.
- **`departments`**: case- and accent-insensitive partial match on the board's department or team name (`eng` matches "Engineering"). Department names are whatever each employer uses.
- **`postedWithinDays`**: compares the board's `postedAt` with the run time, so a scheduled run always looks at the latest days.

**Missing fields.** If a filter needs a field the board doesn't give for a job: with `locations` set, jobs with no location are dropped; with `departments` set, jobs with no department or team are dropped; with `postedWithinDays` set, jobs with no posted date are **kept** and flagged `postedAtUnknown: true`, unless you turn off `keepJobsWithoutPostedDate`.

**What you see.** Every job row has `companyTotalJobs` (open jobs on the board before filters and limits). When any filter is set, each company also gets one free company row with `companyTotalJobs`, `filteredOut` and `jobsReturned`. If nothing matches, the company row has `status: "no_matching_jobs"` and a note with the board's job count. `SUMMARY` shows the filters used and the total filtered out.

In monitor mode the filters apply to new and removed jobs: only matching changes are returned and charged. Changes your filters exclude are recorded, so they aren't reported later either.

### Output

One row per job. Example (Greenhouse):

```json
{
  "recordType": "job",
  "status": "ok",
  "changeType": null,
  "company": "Figma",
  "companyDomain": "figma.com",
  "ats": "greenhouse",
  "boardToken": "figma",
  "detectionMethod": "token-guess",
  "detectionConfidence": "high",
  "jobId": "5426468004",
  "title": "Account Executive, Enterprise",
  "department": "Sales",
  "team": null,
  "location": "San Francisco, CA • New York, NY • United States",
  "isRemote": false,
  "workplaceType": null,
  "employmentType": null,
  "salaryMin": 165000,
  "salaryMax": 190000,
  "salaryCurrency": "USD",
  "salaryPeriod": "year",
  "salaryText": "Annual Base Salary Range: USD 165,000 - USD 190,000",
  "url": "https://boards.greenhouse.io/figma/jobs/5426468004?gh_jid=5426468004",
  "applyUrl": "https://boards.greenhouse.io/figma/jobs/5426468004?gh_jid=5426468004",
  "postedAt": "2025-01-28T23:57:29.000Z",
  "postedAtUnknown": false,
  "postedAtBasis": "board-date",
  "companyTotalJobs": 139,
  "updatedAt": "2026-07-22T09:37:08.000Z",
  "firstSeenAt": "2026-10-21T17:04:19.642Z",
  "lastSeenAt": "2026-10-21T17:04:19.642Z",
  "input": "figma.com",
  "note": null
}
```

Field notes:

- **Salary fields** are filled only when the employer publishes pay on its board. Nothing is estimated. Where an employer posts several ranges (for example by pay zone), `salaryMin` and `salaryMax` span them and `salaryText` lists each one.
- **`isRemote`** is `true` when the board marks the job remote, a listed location says Remote, or the location uses a local remote word such as Homeoffice or télétravail (not when it also says hybrid). **`workplaceType`** is `remote`, `hybrid` or `onsite` when the board says so.
- **Dates** are ISO 8601 in UTC. Not every ATS publishes an update date, so `updatedAt` can be null.
- **`postedAtUnknown`** is `true` when the board gives no posted date for the job.
- **`postedAt`** is the board's own posting date (for Greenhouse `first_published`, never `updated_at`, which Greenhouse changes across whole boards at once). **`postedAtBasis`** says where it came from: `board-date`, `personio-created-at` (the job's creation date in Personio), `teamtailor-published` (the publication date in the Teamtailor feed), or `approximate-from-listing-text` for Workday jobs whose listing only says "Posted 3 Days Ago" (converted to a UTC date, accurate to about a day; `postedWithinDays` allows one extra day for these). Workday jobs listed as "Posted 30+ Days Ago" have no `postedAt` unless their job page is read.
- **`companyTotalJobs`** is the number of open jobs the company's board lists at run time, before your filters and `maxJobsPerCompany`. It is on every job row and on company rows for boards that were read (null for `not_found`, `duplicate` and `error`). Compare it with the rows you got to check nothing was left out. For SmartRecruiters it is the board's own `totalFound` count; the Actor reads at most 10,000 jobs from one SmartRecruiters board.
- **Company rows.** Inputs that don't produce jobs return one row with `recordType: "company"` and a `status` of `not_found`, `no_open_jobs`, `no_matching_jobs` (filters set, nothing matched), `no_change`, `partial`, `empty_unconfirmed` or `fetch_failed` (monitor mode, see [Monitor mode](#monitor-mode-only-new-and-removed-jobs)), `baseline_saved`, `duplicate`, `feed_disabled` (Personio career page found but its public job feed is switched off) or `error`, plus a `note` explaining why. A board token you gave that was found on a different job board gets a `board_moved` row with `fromAts`, `toAts` and `fromBoardToken` (see [Moved boards](#moved-boards)). When job filters are set, companies with matching jobs also get a company row (status `ok`, `changes` or `partial_budget`) with `companyTotalJobs`, `filteredOut` and `jobsReturned`. Company rows are never charged.
- **No personal contact data.** The Actor never outputs recruiter or hiring-manager names or contact details, and removes email addresses and phone numbers from description text.

The Output tab has four views: **Jobs**, **Salaries**, **Monitor changes** and **Detection**. A run summary (status per company, detected board, timings) is saved as `SUMMARY` in the run's key-value store.

#### Per-company status: `COMPANY-STATUS` CSV

Every run also saves `COMPANY-STATUS`, a CSV in the run's key-value store (linked from the Output tab and from the run's final status message). It has one line per company you entered, in your input order:

| Column | Meaning |
|---|---|
| `input` | The line you entered |
| `company`, `ats`, `boardToken` | The board it resolved to (empty if not found) |
| `status` | `ok`, `board_moved`, `partial_budget`, `no_open_jobs`, `no_matching_jobs`, `no_change`, `changes`, `partial`, `empty_unconfirmed`, `fetch_failed`, `baseline_saved`, `not_found`, `feed_disabled`, `duplicate`, `error` or `skipped_budget` |
| `totalJobs` | Open jobs on the board before filters and limits (empty when the board wasn't read) |
| `returned` | Job rows returned for this input |
| `filteredOut` | Jobs (or monitor changes) your filters excluded |
| `note` | Plain-English explanation |
| `fromAts`, `toAts` | For `board_moved` lines: the job board you gave and the one the jobs were read from (empty otherwise) |

Exact repeats in your input are read once and listed as `duplicate`. Companies the run didn't reach because it hit your maximum cost are listed as `skipped_budget`. Neither is charged.

### Workday career sites

Give a company name (`Mastercard`), a domain (`crowdstrike.com`), a career-site link (`https://nvidia.wd5.myworkdayjobs.com/en-US/NVIDIAExternalCareerSite`) or an explicit token (`workday:adobe.wd5/external_experienced`, or just `workday:intel` to find the host and site). The `boardToken` in the output (`tenant.wdN/site`) is the exact value to reuse.

Jobs are read from the public job search the career site itself uses (`/wday/cxs/{tenant}/{site}/jobs`), with no login and no candidate data. Requests go one at a time per Workday site with a short gap, back off when the site answers HTTP 429, and respect the site's `robots.txt` (sites it disallows are skipped and returned as `not_found`).

**Boards with more than 2,000 jobs.** One Workday search returns at most 2,000 results, 20 at a time; later pages repeat the first page. The Actor splits the search by job family (and, when needed, by other filters the site offers, such as time type or location) so each part stays under 2,000, then merges the results by job ID. The board's true total comes from Workday's own filter counts, and `companyTotalJobs` reports it. In our tests (22 Oct 2026) all of NVIDIA's roughly 2,700 open jobs were read (about 150 requests, 2.5 minutes). If a board changes while it's being read, a second pass picks up jobs that moved between pages. If some jobs still can't be read, or the run is about to time out, the company row and `COMPANY-STATUS` note say how many of the total were read and why.

**What Workday rows contain.** `department` comes from the job family. For a job listed in several places, `location` shows the count (for example "3 Locations") unless you set a `locations` filter or `remoteOnly`, in which case that job's own page is read to get every location. `postedAt` is filled for jobs posted in the last 30 days (Workday's listing shows "Posted 30+ Days Ago" for older ones). With `postedWithinDays` above 30, older jobs' pages are read for the exact date. `includeDescription` reads each job's page for the full text, which is slower on big boards. Workday shows no salary fields in its search, so salary is empty.

### Personio career pages

Give a company name (`thermondo`), a domain (`thermondo.de`), a career-page link (`https://mway.jobs.personio.de`) or an explicit token (`personio:mway`).

Jobs are read from the employer's **public Personio XML job feed** (`https://{token}.jobs.personio.de/xml`, or `.jobs.personio.com`), the feed Personio offers for job boards and websites. No login, no candidate data, and no scraping of the career page's HTML. **Employers can switch this feed off in Personio. When it is off, the jobs can't be read**:

- If you gave a Personio token or link, or the company's website links to its Personio page, you get one free row with `status: "feed_disabled"` and a note saying the career page exists but its public feed is switched off.
- If the Personio page was only a guess from a name or domain, you get a free `not_found` row whose note mentions the Personio page with the feed switched off.
- Unknown Personio names return a free `not_found` row.

Requests to Personio go one at a time with a short gap, and each career site's `robots.txt` is respected.

**What Personio rows contain.** `department` and `team` (Personio's recruiting category) as published; `location` is the main office plus any additional offices, separated by "; "; `employmentType` combines Personio's type and schedule (for example Full-time, Part-time, Internship, Working student, Trainee, Contract); salary only when the employer adds it to the feed; `url` and `applyUrl` are the job's public Personio page. `postedAt` is the job's creation date in Personio (`postedAtBasis: "personio-created-at"`), which can be earlier than the date it was first published. `includeDescription` adds the description sections from the feed.

### Teamtailor career sites

Give a company name (`Oatly`), a domain (`tibber.com`), a career-site link (`https://polestar.teamtailor.com/jobs`) or an explicit token: `teamtailor:polestar`, or the career site's own host for sites on the employer's domain (`teamtailor:jobs.tibber.com`).

Jobs are read from the career site's **public RSS job feed** (`{career site}/jobs.rss`), which Teamtailor documents for sharing a career site's public jobs with other systems. No login, no API key and no scraping of the career pages. The feed is paged 100 jobs at a time, so career sites with up to 5,000 open jobs are read in full. Requests go one at a time per career site with a short gap, and each site's `robots.txt` is respected. A career site on the employer's own domain is found when the company's careers page is (or redirects to) a Teamtailor-built site.

**What Teamtailor rows contain.** `title`; `department`; `team` (Teamtailor's division, often empty); `location` as city and country for each listed location, separated by "; "; `workplaceType` from Teamtailor's remote status (fully remote = `remote`, hybrid = `hybrid`, none = `onsite`; "temporarily remote" is left empty); `url` and `applyUrl` are the job's page on the career site; `postedAt` is the publication date in the feed (`postedAtBasis: "teamtailor-published"`); `includeDescription` adds the job description. `company` is the career site's name as the feed gives it (for example "Oatly AB").

**What Teamtailor's feed doesn't have:** salary, employment type (full-time, part-time and so on), an update date (`updatedAt` is empty) and a separate apply link. Those fields are always empty for Teamtailor jobs. Office street addresses in the feed are not copied.

### Moved boards

Companies sometimes switch job boards (for example from Lever to Ashby) and keep the same name in the URL. When you give an explicit token (`lever:notion`) or a board URL (`https://jobs.lever.co/notion`) and that board answers "not found" or lists no open jobs, the Actor tries the same token on the other supported public job-board feeds: Greenhouse, Lever (US and EU), Ashby, Workable, SmartRecruiters, Recruitee, Personio and Teamtailor.

- **Found with open jobs:** the jobs are read from the new board, with your filters applied before charging and charged as usual. The company gets one free row with `status: "board_moved"`, `fromAts` (the board you gave), `toAts` (where the jobs came from), `fromBoardToken` and a note. Job rows show the new board in `ats` and `detectionMethod: "moved-board"`. Use the new `ats` and `boardToken` in future runs.
- **Not found anywhere:** you get the same free `not_found` row as before, and the note lists the job boards that were checked.
- **The old board still exists but is empty, and nothing is found elsewhere:** you get `no_open_jobs` as before.
- A board under the same token that belongs to a different company (its company name or job links don't match) is not used. On Workable, SmartRecruiters, Recruitee, Personio and Teamtailor, where short names are reused more often, a board is used only when its company name matches the token.
- Workday is not searched, because a token alone doesn't say which Workday host a company would be on. A Workday token that's gone (`workday:tenant.wd5/site`) is tried on the other boards using the tenant name.
- This applies only to tokens and board URLs you give. Domains and company names are already looked up on every supported job board.
- The lookup makes a request or two to each job board's public API, at the normal rate, and isn't charged.
- In monitor mode, the first run after a move saves the new board as the baseline (job IDs differ between job boards, so the old board's history can't be carried over).

### Monitor mode: only new and removed jobs

Turn on `monitorMode` and schedule the Actor (daily or weekly) with the same input:

- The **first run** for a company saves its current jobs as a baseline and, by default, returns them all with `changeType: "new"`.
- **Later runs** return only jobs that appeared (`changeType: "new"`) or disappeared (`changeType: "removed"`) since the previous run. Removed rows keep the job's title, location, URL and first/last-seen dates.
- A company with **no changes** returns one `no_change` row, charged at the low monitor-check price.

**When a board can't be read in full.** The saved job list is only replaced after a complete read of the board:

- **Read failed** (for example the job board returned an error): the company gets a free `fetch_failed` row, the saved list is left as it was, and no jobs are reported as removed. The board is checked again on the next run.
- **Only part of the board was read** (a Workday or Teamtailor board cut short by the run's time limit, a Workday board where some jobs can't be reached through its public search, or a SmartRecruiters board that stops paging early): new jobs seen in the part that was read are returned as usual, but no jobs are reported as removed, and saved jobs that weren't seen are kept. The company row has status `partial` and a note; if there are no new jobs, that row is free. On boards that are always read only in part, closed jobs are therefore not reported as removed.
- **A board suddenly lists no jobs** after the last run saw some: this is treated as unconfirmed. The company gets a free `empty_unconfirmed` row, nothing is reported as removed and the saved list is kept. If the next run also reads 0 jobs, the saved jobs are then returned as `removed`.

Job rows are only charged when they are delivered, so none of these cases charges for rows that weren't returned.

The job lists are kept in a named key-value store in your account (`stateStoreName`). Use a different store name for each separate watch list. If a run stops early because it reached your maximum charge, the changes it didn't deliver are reported on the next run, not lost.

### Pricing

This Actor uses pay-per-event pricing. You pay for results, not for compute:

| Event | When it is charged | Price |
|---|---|---|
| Actor start | Once per run (per GB of memory; the default 256 MB run counts as one start) | $0.00005 |
| Job | Each job row returned (in monitor mode, each new or removed job). Jobs removed by your filters are not charged | $0.002 |
| Company resolved | Each company whose board is found and read (normal mode, and the first monitor run), including when no jobs match your filters | $0.002 |
| Monitor check, no change | Monitor mode: a company with no new or removed jobs | $0.0005 |

Companies that can't be found are never charged. Higher Apify plans get tier discounts (Silver −10%, Gold −20%). Prices on the Actor's Pricing tab are authoritative.

Examples at the base price:

- One run over 20 companies with 100 open jobs each: $0.00005 + 20 × $0.002 + 2,000 × $0.002 = about $4.04.
- A daily monitor run over the same 20 companies, where 3 companies changed by 2 jobs each: $0.00005 + 17 × $0.0005 + 6 × $0.002 = about $0.021.
- The first example with filters that keep 10 jobs per company: $0.00005 + 20 × $0.002 + 200 × $0.002 = about $0.44.

The Actor respects the **maximum cost per run** you set: it stops adding rows when the limit is reached and says so in the run's status message.

### Use it from code, spreadsheets or an AI agent

- **Schedule it**: Console > Schedules, daily, with `monitorMode` on, to get a new-jobs feed.

- **Google Sheets, Make, Zapier, n8n, Slack**: use the Actor's Integrations tab to send each run's dataset where you need it.

- **API (Python)**:

  ```python
  from apify_client import ApifyClient
  client = ApifyClient("<YOUR_APIFY_TOKEN>")
  run = client.actor("yourevenings/careers-jobs-api").call(run_input={"companies": ["stripe.com", "ashby:ramp"]})
  for item in client.dataset(run["defaultDatasetId"]).iterate_items():
      print(item["company"], item.get("title"), item.get("location"), item.get("salaryMin"))
  ```

- **AI agents**: the Actor can be run through the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp), so Claude, ChatGPT or Cursor agents can call it with their own input.

#### AI agents can pay for runs themselves

AI agents can run this Actor and pay with stablecoins such as USDC through the x402 or MPP protocols, with no Apify account needed. The agent buys a prepaid, spend-capped Apify API token from Apify's Agent General Interface (AGI) and then runs the Actor with it as normal. Prepaid tokens only run Actors that use limited permissions, and this Actor does. Agentic payments are an experimental Apify feature, and the terms (minimum amount, expiry, usage limits) are set by Apify. See Apify's docs: [Let your AI agent pay for Actor runs](https://docs.apify.com/agents/payments), [x402](https://docs.apify.com/integrations/x402) and [MPP](https://docs.apify.com/integrations/mpp).

### Tips

- Run once with domains, then switch to the `ats:token` values from the `boardToken` column. Lookups are then exact and faster.
- Large boards can have 500–1,000+ jobs. Use the job filters to get (and pay for) only the jobs you need, and `maxJobsPerCompany` while you test.
- Turn on `includeDescription` only if you need the full text. It makes the dataset much larger.

### FAQ

**Is this an official Greenhouse, Lever, Ashby or Workday product?**
No. It's an independent tool. It isn't affiliated with or endorsed by Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee or Workday. The names are used only to describe which public job boards it reads.

**Where does the data come from?**
From the public job-board endpoints the ATS vendors publish so employers' openings can be shown on careers sites and job boards, and for Workday from the public job search each career site uses. No login, no browser automation and no proxies are used. Requests are rate-limited to be polite.

**Why was my company not found?**
It may use an ATS that isn't supported yet (for example SuccessFactors, iCIMS or an in-house careers site), its Workday career site may use a different tenant name from its domain, or its board token may not match its domain. If you know the board URL, pass it directly (for example `https://jobs.ashbyhq.com/yourcompany` or `https://yourcompany.wd5.myworkdayjobs.com/External`).

**Could a guessed board belong to a different company with the same name?**
It can happen with very generic names. The Actor checks company names and job-link domains to reduce this, and every row shows `detectionMethod` and `boardToken` so you can verify. Explicit tokens and board URLs avoid the issue entirely.

**How do I know I got every job?**
Each job row has `companyTotalJobs`, the board's open-job count before your filters and limits, and `COMPANY-STATUS` shows per company how many jobs the board had, how many were returned and how many your filters removed. In normal mode, if `returned` plus `filteredOut` is lower than `totalJobs`, the note says why: `maxJobsPerCompany`, your maximum cost per run, or (on Workday) jobs its public search couldn't reach in time. (In monitor mode `returned` counts new and removed jobs, so it isn't compared with the board total.)

**How fresh is the data?**
Every run reads the boards live, so jobs are current as of the run.

**Is it legal to use this data?**
The Actor reads only job postings that employers publish on public job boards, and it drops personal contact details. You're responsible for using the data in line with the laws that apply to you (for example GDPR) and the terms of the job boards and sites you use it with.

**Is there a free Greenhouse jobs API?**
Greenhouse, Lever and Ashby publish public job-board endpoints, but each has its own format and you have to know every company's board token. This Actor tries to find the token from the domain (or takes the token or board URL if you know it) and returns one schema for all nine systems. You pay per job returned and per company resolved; companies that can't be found are free.

**Am I charged for jobs my filters remove?**
No. Filters run before any job row is pushed, so filtered-out jobs are never returned or charged. Each company whose board is found is still charged the company-resolved event once (also when no jobs match), plus the per-run start fee. Companies that can't be found are free.

**Which ATSs are supported?**
Exactly nine: Greenhouse, Lever (including its EU instance), Ashby, Workable, SmartRecruiters, Recruitee, Workday, Personio (only for employers whose public Personio job feed is switched on) and Teamtailor (from the career site's public RSS feed, which has no salary or employment type). BambooHR, iCIMS, SuccessFactors and other systems are not covered yet. They are being considered for future versions, but there is no date. Open an issue on the Actor's Issues tab to request others.

### Feedback

Found a wrong match, a missing field or a company that should work? Open an issue on the Issues tab with the input line and run ID, and it'll be looked at quickly.

# Changelog

This Actor's version history is a separate document: https://apify.com/yourevenings/careers-jobs-api/changelog.md

# Actor input Schema

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

One company per line. Accepted forms: a domain such as <code>figma.com</code>; a careers page URL such as <code>https://www.notion.so/careers</code>; a job-board URL on boards.greenhouse.io, job-boards.greenhouse.io, jobs.lever.co, jobs.eu.lever.co, jobs.ashbyhq.com, apply.workable.com, jobs.smartrecruiters.com, *.recruitee.com or a Workday career site (*.myworkdayjobs.com, *.myworkdaysite.com) or a Personio career page (*.jobs.personio.de, *.jobs.personio.com) or a Teamtailor career site (*.teamtailor.com); or an explicit token in the form <code>ats:token</code>, where ats is greenhouse, lever, lever-eu, ashby, workable, smartrecruiters, recruitee, workday, personio or teamtailor (for example <code>greenhouse:stripe</code> or <code>workday:nvidia.wd5/NVIDIAExternalCareerSite</code>). Explicit tokens and board URLs are the fastest and most reliable. Companies whose board can't be found are returned as a row with status <code>not_found</code> and are not charged. Personio boards can only be read when the employer has switched on Personio's public XML job feed; otherwise the row has status <code>feed_disabled</code> (or <code>not_found</code> for a guessed board) and is not charged.

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

Add a plain-text <code>descriptionText</code> field to every job. Email addresses and phone numbers are removed from the text. Off by default because descriptions make the dataset much larger and runs slower.

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

Limit the number of job rows returned for each company. 0 means no limit (all open jobs). Ignored in monitor mode, where every change is reported.

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

Keep only jobs whose title contains at least one of these words or phrases. Ignores case and accents; whole words: <code>engineer</code> matches "Software Engineer" but not "Engineering Manager", so add <code>engineering</code> too if you want both. A single word of 5+ letters also matches at the end of a compound word and with German -in/-innen endings: <code>entwickler</code> matches "Softwareentwickler" and "Entwicklerin". Examples: <code>engineer</code>, <code>product manager</code>, <code>data scientist</code>. Jobs removed by any filter are never returned or charged.

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

Drop jobs whose title contains any of these words or phrases (same matching as title keywords: ignores case and accents, whole words, compound endings for words of 5+ letters). Examples: <code>intern</code>, <code>manager</code>, <code>senior</code>.

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

Keep only jobs whose location contains one of these places (whole words, ignoring case and accents). Examples: <code>London</code>, <code>United Kingdom</code>, <code>München</code>, <code>Remote</code>. About 50 groups of place names are matched together, e.g. Munich/München, Cologne/Köln, Zurich/Zürich, Vienna/Wien, NYC/New York, UK/United Kingdom/England, USA/US, Germany/Deutschland. Cities aren't matched to countries. <code>Remote</code> also matches jobs the board flags as remote (including local words like Homeoffice). Jobs with no location on the board are dropped when this filter is set.

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

Keep only jobs with <code>isRemote: true</code> or <code>workplaceType: "remote"</code>. Hybrid jobs are not included. <code>isRemote</code> comes from the board's remote flag or, when the board has none, the word "remote" in the job's location or title, or a local remote word in the location (Homeoffice, télétravail, teletrabajo, remoto, thuiswerk and similar).

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

Keep only jobs whose department or team contains one of these texts (ignores case and accents, partial match: <code>eng</code> matches "Engineering"). Examples: <code>Engineering</code>, <code>Sales</code>, <code>Data</code>. Department names are whatever each employer uses on its board. Jobs with no department or team are dropped when this filter is set.

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

Keep only jobs whose <code>postedAt</code> is within this many days of the run (0 or empty = any date). Counts back from each run, so a scheduled run always looks at the latest days. Example: <code>1</code> for the last 24 hours, <code>7</code> for the last week. Workday listings show only relative dates ("Posted 3 Days Ago"); those are converted to a date accurate to about a day and get one extra day of leeway.

## `keepJobsWithoutPostedDate` (type: `boolean`):

Some boards don't publish a posted date for every job. With <b>Posted within the last N days</b> set, those jobs are kept by default and flagged <code>postedAtUnknown: true</code>. Turn this off to drop them instead.

## `monitorMode` (type: `boolean`):

When on, the Actor compares each company's board with the previous run and returns only jobs that are new (<code>changeType: "new"</code>) or have been removed (<code>changeType: "removed"</code>). Companies with no changes return one summary row with status <code>no_change</code>. Schedule the Actor daily or weekly with the same input to get a hiring-signal feed. The previous job lists are kept in the named key-value store set below.

## `emitBaselineOnFirstRun` (type: `boolean`):

In monitor mode, the first run for a company has nothing to compare with. When on, all current jobs are returned as <code>new</code> (and charged as jobs). When off, the first run only saves the baseline and returns one <code>baseline_saved</code> row per company.

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

Name of the key-value store in your account that remembers previously seen jobs (for monitor mode and the firstSeenAt field) and which board each company uses. Use a different name for each separate monitoring list.

## `persistState` (type: `boolean`):

In normal (non-monitor) mode, save the jobs seen in this run so <code>firstSeenAt</code> stays accurate across runs. Monitor mode always saves state.

## `scanCareersPage` (type: `boolean`):

If guessing the board from the domain fails, fetch up to 4 pages of the company website (the careers URL you gave, /careers, /jobs and the homepage) and look for links to a supported job board.

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

Maximum number of HTTP requests in flight at once, across all companies. The default is deliberately polite to the job-board APIs.

## `requestTimeoutSecs` (type: `integer`):

Timeout for each request to a job-board API. Failed or timed-out requests are retried up to 3 times with backoff.

## Actor input object example

```json
{
  "companies": [
    "figma.com",
    "discord.com",
    "https://jobs.lever.co/spotify",
    "linear.app",
    "ashby:ramp"
  ],
  "includeDescription": false,
  "maxJobsPerCompany": 0,
  "remoteOnly": false,
  "keepJobsWithoutPostedDate": true,
  "monitorMode": false,
  "emitBaselineOnFirstRun": true,
  "stateStoreName": "careers-jobs-api-state",
  "persistState": true,
  "scanCareersPage": true,
  "maxConcurrency": 5,
  "requestTimeoutSecs": 15
}
```

# Actor output Schema

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

All dataset rows: one row per open job (recordType job) plus per-company status rows (recordType company). Opens on the Jobs view.

## `salaries` (type: `string`):

Jobs with their posted salary fields.

## `changes` (type: `string`):

Monitor mode: new and removed jobs since the previous run.

## `detection` (type: `string`):

Which ATS board was found for each company and how.

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

SUMMARY record: status, detected board, job count and timing per company.

## `companyStatus` (type: `string`):

COMPANY-STATUS: one line per company you entered (input, company, ats, boardToken, status, totalJobs, returned, filteredOut, note), including merged repeats and companies not reached.

# 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": [
        "figma.com",
        "discord.com",
        "https://jobs.lever.co/spotify",
        "linear.app",
        "ashby:ramp"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("yourevenings/careers-jobs-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": [
        "figma.com",
        "discord.com",
        "https://jobs.lever.co/spotify",
        "linear.app",
        "ashby:ramp",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("yourevenings/careers-jobs-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": [
    "figma.com",
    "discord.com",
    "https://jobs.lever.co/spotify",
    "linear.app",
    "ashby:ramp"
  ]
}' |
apify call yourevenings/careers-jobs-api --silent --output-dataset

```

## MCP server setup

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