# Job Search API: 2M Job Postings, 45k+ Company Career Sites (`jamie_developer/job-search`) Actor

Search about 2 million job postings from 49,000 company career sites on 19 applicant tracking systems (Greenhouse, Workday, Lever, Ashby, SmartRecruiters, Paylocity, Personio and more), updated daily. Filter by title, country, remote, salary, date and company. Only-new mode for job alerts.

- **URL**: https://apify.com/jamie_developer/job-search.md
- **Developed by:** [JamieDoe](https://apify.com/jamie_developer) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 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

## Job Search API: 2M Jobs, 45k+ Company Career Sites, Daily

**Search about 2 million open jobs from about 49,000 company career sites in one call. One clean format across 19 applicant tracking systems, from Greenhouse, Workday, Lever and Ashby to BambooHR, Paylocity, Personio and Teamtailor, refreshed daily.**

> **Why this one**
>
> - **No scraping at run time.** Your run searches a ready-made index that is rebuilt every day, so a search across all 2 million jobs takes a minute at most, often seconds.
> - **One schema for every ATS**, with a normalized `countryCode`, remote/hybrid flag, salary range and posting date.
> - **Job alerts built in.** *Only jobs not returned before* gives you just the new jobs on every scheduled run, and you aren't charged for the rest.
> - **Beyond the usual three ATSs.** Workday's big employers, US mid-market platforms (BambooHR, Paylocity, Rippling, Breezy, Jobvite) and European ones (Personio, Recruitee, Teamtailor, softgarden, JOIN, d.vinci) sit next to Greenhouse, Lever and Ashby.

### What it does

Companies publish their open jobs through their applicant tracking system (ATS). Each ATS offers a public job-board feed, the same one that powers the company's careers page. A daily process reads those feeds for about 49,000 companies and stores their open jobs in one index (2,016,483 jobs on 31 October 2026: Workday 907k, SmartRecruiters 297k, Greenhouse 210k, Paylocity 80k, Lever 74k, Ashby 66k, Workable 57k, softgarden 50k, BambooHR 49k, Teamtailor 49k, Recruitee 41k, Breezy 35k, Personio 33k, JOIN 19k, Rippling 17k, Jobvite 11k, Comeet 8k, Pinpoint 8k, d.vinci 5k). This Actor searches that index with your filters and saves the matching jobs as a dataset you can download as JSON, CSV or Excel, or read through the API.

Need the full job ad text? Turn on **Include job description** and the Actor fetches it live from each job's own board during your run, for every platform in the index. Most platforms need one request per company; a few whose feeds carry no descriptions (such as Workday and SmartRecruiters) need one per job, so those are limited to 500 jobs per company and 5,000 per run. Jobs over the limit, or on a job board that can't be read at that moment, are still returned, with `description` set to null and `descriptionAvailable` set to false; the status message and the SUMMARY record say how many.

### Filters

| Filter | What it does |
|---|---|
| **Job title contains any of** / **none of** | Keywords like `engineer`, `data scientist`, `account executive`. Matches the start of words, so `engineer` also finds *Engineering*; 1-2 letter keywords (`AI`, `QA`, `Go`) must match a whole word. |
| **Department or team contains** | e.g. `engineering`, `sales` |
| **Employment type** | Full-time, part-time, contract, internship, temporary. Each ATS words this differently (Vollzeit, FULL_TIME, full-or-part-time); they are grouped for you. |
| **Location contains any of** | Free text against the job's locations, e.g. `Berlin`, `Bay Area`, `Bavaria` |
| **Countries** | `Germany`, `United States`, `GB`, `CH`... From the ATS's country field or, when missing, from the location (`Austin, TX` is US, `München` is DE). Tick **Also keep jobs that name no country** to add jobs listed only as "Remote" or "EMEA". |
| **Workplace type** / **Remote jobs only** | Remote, hybrid, on-site |
| **Posted within the last N days** | Uses the ATS posting date, or the day the job first appeared in the index |
| **Jobs with salary only**, **Minimum salary**, **Salary currency** | Keep jobs that publish pay; minimum is per year, with monthly and hourly pay converted |
| **Companies** | Company names (`Stripe`, `Hugging Face`), job board names (`huggingface`) or job board URLs |
| **Applicant tracking systems** | e.g. `greenhouse`, `personio`. The SUMMARY of every run lists the platforms in the current index with their job counts. |
| **Sort by**, **Max jobs** | Newest first or company A-Z; cap the number of jobs (and so the cost) |

#### Example input

```json
{
  "titleKeywords": ["engineer", "developer"],
  "excludeTitleKeywords": ["senior", "staff"],
  "countries": ["Germany", "Switzerland", "Austria"],
  "workplaceType": ["remote", "hybrid"],
  "postedWithinDays": 7,
  "onlyNewSinceLastRun": true,
  "maxJobs": 200
}
```

### Output

| Field | Example | Notes |
|---|---|---|
| `title` | ClickHouse Operations Engineer | |
| `company`, `companySlug` | posthog | `company` is the employer name, cleaned of board labels such as "Careers" or "Jobs at" (for Workday, read from the employer's own job ads); empty if a board names no employer. `companySlug` is the company's job board name on its ATS |
| `ats`, `jobId` | ashby, 43dbf072-… | `ats` + `companySlug` + `jobId` identify a job |
| `location`, `locations` | Remote | all locations of the job |
| `country`, `countryCode` | USA, US | `country` as the ATS gives it; `countryCode` is always ISO 3166 (e.g. DE, GB, US), empty if unknown |
| `workplaceType` | remote / hybrid / onsite | from the ATS, or from "remote" / "hybrid" in the location or title |
| `department`, `team`, `employmentType` | Engineering, ClickHouse, Full-time | as published |
| `postedAt`, `updatedAt` | 2026-10-29T15:32:29Z | from the ATS (ISO 8601, UTC) |
| `firstSeenAt`, `lastSeenAt` | 2026-10-29T15:32:29Z | when the index first and last saw the job open |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod`, `salaryText` | 150000, 190000, USD, year, "$150K – $190K" | from the ATS's salary fields or the pay range in the ad |
| `url`, `applyUrl` | links to the job ad and the application form | |
| `sourceUrl` | the company job board | |
| `description`, `descriptionAvailable` | full job ad as plain text; true/false | only with *Include job description*; null and false when the job board couldn't be read or the job was over the per-job limit |
| `scrapedAt` | 2026-10-31T01:42:07Z | when your run returned the job |

Empty values are `null`, so every row has the same columns in CSV and Excel.

#### Example output

```json
{
  "title": "ClickHouse Operations Engineer",
  "company": "posthog",
  "ats": "ashby",
  "companySlug": "posthog",
  "jobId": "43dbf072-0fc1-48c9-8c1e-7416db7d4a14",
  "location": "Remote",
  "locations": ["Remote"],
  "country": "USA",
  "countryCode": "US",
  "workplaceType": "remote",
  "department": "Engineering",
  "team": "ClickHouse",
  "employmentType": "Full-time",
  "postedAt": "2026-10-29T15:32:29Z",
  "updatedAt": null,
  "firstSeenAt": "2026-10-29T15:32:29Z",
  "lastSeenAt": "2026-10-31T01:32:30Z",
  "salaryMin": null,
  "salaryMax": null,
  "salaryCurrency": null,
  "salaryPeriod": null,
  "salaryText": null,
  "url": "https://jobs.ashbyhq.com/posthog/43dbf072-0fc1-48c9-8c1e-7416db7d4a14",
  "applyUrl": "https://jobs.ashbyhq.com/posthog/43dbf072-0fc1-48c9-8c1e-7416db7d4a14/application",
  "sourceUrl": "ashby:posthog",
  "description": "ABOUT POSTHOG\n\nPostHog is your product's context layer. …",
  "scrapedAt": "2026-10-31T01:42:07Z"
}
```

### Job alerts and integrations

1. Set your filters and turn on **Only jobs not returned before**.
2. Create a schedule in Apify, e.g. every morning.
3. The first run returns the jobs that match now. Every later run returns only jobs that earlier runs of the same search haven't returned, and you aren't charged for the ones it skips.
4. Connect an integration to send each run's jobs to Slack, email, Google Sheets, Make, Zapier, n8n or a webhook.

The memory of returned jobs is kept per set of filters, in a key-value store named `job-search-state` in your own account. Give the search a **Search name** if you want to keep the memory while you edit the filters.

### Use cases

- **Job boards and aggregators:** fill a niche board (remote jobs, jobs in Switzerland, climate tech, jobs with salary) with fresh jobs every day, in one consistent format.
- **Job alerts:** a daily feed of new jobs for a set of titles, countries or companies, sent to Slack or email.
- **Recruiting and talent intelligence:** see which companies hire for which roles and where, and how fast.
- **Sales signals:** a company hiring a "Salesforce administrator", "data engineer" or "head of security" is a buying signal for tools and services in that area. Filter by title or department and feed the companies into your CRM.
- **Market and salary research:** compare titles, countries, remote share and published pay ranges across employers and ATSs.

### Pricing

You pay per job saved to the dataset. The exact price is on the Pricing tab. Jobs removed by your filters, jobs skipped as already returned, and jobs dropped because they closed are free. **Max jobs** and the maximum cost per run in the run options both cap what a run can cost; the Actor stops cleanly at either limit.

### How the filters work

- **Keywords** are case-insensitive and match the start of a word: `data` finds *Data Engineer* and *Database Administrator*, but `ai` doesn't match inside *Retail*. Add exclusions for the rest, e.g. title `java` with exclude `javascript`.
- **Countries** use the ATS's country field when it has one, otherwise the location text, including US states (`Austin, TX`) and major cities (`London`, `München`, `Bengaluru`). A job listed only as "Remote" or "EMEA" has no country and is dropped when you filter by country, unless you tick **Also keep jobs that name no country**.
- **On-site** includes jobs with no stated workplace type, because most ATSs only flag remote and hybrid jobs.
- **Minimum salary** compares the bottom of the published range, converted to a year (monthly x12, hourly x2080). Set **Salary currency** too, otherwise amounts in different currencies are compared as-is.
- **Posted within** uses the ATS's posting date. Some companies repost evergreen roles or keep old dates. A few platforms publish no posting date at all (BambooHR, Rippling, Jobvite, Comeet, Pinpoint, and some softgarden and Workday jobs); those jobs use the day they first appeared in the index, so they all count as recent for the first days after their platform or company was added (many on 31 October 2026).
- A **SUMMARY** record in the run's key-value store shows the index date and size, the job count per platform, how many jobs matched, any warnings and, with descriptions on, which job boards couldn't be read.

### FAQ

**Is this legal?** The index is built only from the public job-board feeds that ATS vendors provide so companies can publish their openings. No logins, nothing bypassed, and no candidate or personal data: just job postings. The job ads belong to the employers, so check your use case (for example, republishing full descriptions) against the employer's terms. This Actor is not affiliated with any applicant tracking system vendor (such as Greenhouse, Workday, Lever, Ashby or SmartRecruiters) or with any employer whose jobs appear in the results.

**How fresh are the jobs?** The index is rebuilt every day. Most job boards are read on every rebuild; Workday's large employer sites are read in rotation, each at least every 2–3 days. If a board can't be read in a rebuild, its jobs from the last good read stay in the index for up to 3 days. The run's status message and the SUMMARY record show when it was last updated, and `lastSeenAt` on each job says when it was last seen open. With **Include job description** on, each job is also checked against the live job board, and jobs that closed since the last update are dropped.

**Why did I get fewer jobs than Max jobs?** Fewer jobs matched your filters, earlier runs already returned them (with *Only jobs not returned before*), or the run reached its maximum cost. The status message says which.

**A company I need is missing.** Check its name or job board name in **Companies**. If it still doesn't show up, open an issue with its careers page so it can be added to the index.

**Which platforms are covered?** The 19 listed under *What it does*, as of the date shown there. Some platforms are only partly covered so far (for example Workday, where the index holds about 2,600 of the roughly 3,400 employer sites it knows); coverage grows over time, and the SUMMARY record of every run lists the platforms in the current index and how many jobs each has. Job sites such as LinkedIn or Indeed are not included, only company career sites.

**Why is `countryCode` empty for some jobs?** The ATS gave no country and the location (e.g. "Remote", "EMEA", a small town) doesn't name one.

# Actor input Schema

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

Keep jobs whose title contains at least one of these keywords, e.g. engineer, data scientist, account executive. Case-insensitive. A keyword matches at the start of a word, so engineer also finds Engineering and Engineers; keywords of 1-2 letters (AI, QA, Go) must match a whole word. Leave empty for all titles.

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

Drop jobs whose title contains any of these keywords, e.g. senior, intern, manager. Same matching as above.

## `departmentKeywords` (type: `array`):

Keep jobs whose department or team contains one of these keywords, e.g. engineering, sales, marketing. Jobs without a department are dropped when this is set.

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

Keep jobs of these types. Each ATS words it differently (Full-time, Vollzeit, FULL_TIME...), so they are grouped. Internship also matches titles with intern, Werkstudent or Praktikum. Jobs without a stated type are dropped when this is set. Leave empty for all.

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

Keep jobs whose location text contains one of these words, e.g. Berlin, London, New York, Bavaria. Matched against the job's location, all its locations and its country field.

## `countries` (type: `array`):

Keep jobs in these countries. Use English names (Germany, United States, United Kingdom) or ISO codes (DE, US, GB). The country comes from the ATS when it gives one, otherwise from the location text (e.g. 'Austin, TX' is US, 'München' is DE). Jobs whose country can't be determined, such as 'Remote' with no country, are dropped when this is set.

## `includeUnknownCountry` (type: `boolean`):

With a country filter set, also keep jobs whose location names no country, such as 'Remote', 'Worldwide' or 'EMEA'. Useful with Remote jobs only.

## `workplaceType` (type: `array`):

Keep remote, hybrid and/or on-site jobs. Most ATSs only flag remote and hybrid jobs, so On-site also includes jobs with no remote or hybrid indication. Leave empty for all.

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

Shortcut: keep only jobs marked remote by the ATS or with remote in the location or title.

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

Keep jobs posted in the last N days, using the ATS's posting date or, when the ATS gives none, the day the job first appeared in the index. 0 = any date.

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

Keep only jobs that state a pay range, either in the ATS's salary fields or written in the job ad.

## `salaryMinAtLeast` (type: `integer`):

Keep jobs whose pay range starts at or above this yearly amount (the single figure if only one is given). Monthly, weekly, daily and hourly pay is converted to a year (x12, x52, x260, x2080). Use it with Salary currency, otherwise amounts in different currencies are compared as-is. 0 = no minimum.

## `salaryCurrency` (type: `string`):

Keep only jobs that pay in this currency (3-letter code). Implies 'Jobs with salary only'.

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

Keep jobs from these companies only. Use the company name (Stripe, Hugging Face), its job board name (huggingface) or job board URL (https://boards.greenhouse.io/stripe). Names are matched exactly, ignoring case, spaces and suffixes like Inc or GmbH.

## `ats` (type: `array`):

Keep jobs from these job board platforms only, one per line. The index covers greenhouse, smartrecruiters, workday, lever, ashby, workable, teamtailor, paylocity, bamboohr, recruitee, softgarden, breezy, personio, rippling, jobvite, join, comeet, pinpoint and dvinci. Every run's SUMMARY record lists the platforms and job counts in the current index. Leave empty for all.

## `onlyNewSinceLastRun` (type: `boolean`):

For scheduled runs and job alerts: return only jobs that earlier runs of the same search haven't returned. The first run returns the current matches; later runs return only new ones. Skipped jobs are not charged. The memory is kept per set of filters in a key-value store named job-search-state in your account.

## `stateName` (type: `string`):

Name for the 'already returned' memory. Leave empty to derive it from the filters; set it to keep the memory when you edit the filters.

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

Add the full job description as plain text, fetched live from each job's own board at run time: one request per company for most platforms, one per job for platforms whose feeds carry no descriptions (such as Workday or SmartRecruiters), up to 500 jobs per company and 5,000 per run; jobs over that come back without a description. Makes runs slower. Jobs that closed since the last index update are dropped and not charged.

## `sortBy` (type: `string`):

Newest first (by posting date, else first seen), or company name A-Z (newest first within a company).

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

Stop after this many jobs (up to 200,000). You pay per job saved, so this caps the cost of a run.

## Actor input object example

```json
{
  "titleKeywords": [
    "engineer",
    "developer"
  ],
  "excludeTitleKeywords": [
    "senior",
    "staff",
    "principal"
  ],
  "departmentKeywords": [
    "engineering",
    "data"
  ],
  "employmentType": [
    "full-time"
  ],
  "locationKeywords": [
    "Berlin",
    "Munich"
  ],
  "countries": [
    "Germany",
    "Switzerland",
    "AT"
  ],
  "includeUnknownCountry": false,
  "workplaceType": [
    "remote",
    "hybrid"
  ],
  "remoteOnly": true,
  "postedWithinDays": 7,
  "hasSalary": false,
  "salaryMinAtLeast": 120000,
  "salaryCurrency": "USD",
  "companies": [
    "Stripe",
    "Notion",
    "https://jobs.ashbyhq.com/openai"
  ],
  "ats": [
    "personio",
    "recruitee"
  ],
  "onlyNewSinceLastRun": false,
  "includeDescription": false,
  "sortBy": "newest",
  "maxJobs": 25
}
```

# Actor output Schema

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

No description

## `summary` (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 = {
    "titleKeywords": [
        "engineer"
    ],
    "remoteOnly": true,
    "postedWithinDays": 7,
    "maxJobs": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("jamie_developer/job-search").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 = {
    "titleKeywords": ["engineer"],
    "remoteOnly": True,
    "postedWithinDays": 7,
    "maxJobs": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("jamie_developer/job-search").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 '{
  "titleKeywords": [
    "engineer"
  ],
  "remoteOnly": true,
  "postedWithinDays": 7,
  "maxJobs": 25
}' |
apify call jamie_developer/job-search --silent --output-dataset

```

## MCP server setup

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

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/SngeYXJR9w7Zefyjz/builds/x2bPsi9BjtkwzSCiS/openapi.json
