# Job Scraper - LinkedIn Jobs, Indeed & Multi-City Job Alerts (`groupoject/multi-job-board-scraper`) Actor

Job scraper for LinkedIn Jobs and Indeed in one run. Search many keywords across up to 20 cities, filter titles, companies and salary, dedupe across boards, and schedule alerts that return only new jobs. Exports salary, company data and apply links. $2 per 1,000 jobs, no login.

- **URL**: https://apify.com/groupoject/multi-job-board-scraper.md
- **Developed by:** [Group Oject](https://apify.com/groupoject) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 32 total users, 9 monthly users, 98.5% runs succeeded, 1 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $1.85 / 1,000 results

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 Scraper - LinkedIn Jobs, Indeed & Multi-City Job Alerts

**Scrape LinkedIn Jobs and Indeed in one run, across up to 20 cities, and get one clean, deduplicated row per job with salary, company, description and apply link. Schedule it to receive only new jobs. No job-board login, cookies or API key.**

One job scraper instead of several: search many keywords and locations, choose your job boards, filter out irrelevant titles and companies, and export to CSV, Excel, JSON or straight into your pipeline. Indeed and LinkedIn are the tested sources; Google Jobs, Glassdoor, ZipRecruiter, Bayt and Naukri are optional beta boards.

[Run the job scraper](https://console.apify.com/actors/LzsIpToRTUZSnqjbS/input) | [Pricing](https://apify.com/groupoject/multi-job-board-scraper/pricing) | [Input fields](https://apify.com/groupoject/multi-job-board-scraper/input-schema)

![Job Scraper output in Apify: job title, company, location, job board, salary range, posted date and hiring signal score](https://api.apify.com/v2/key-value-stores/lwQu1lCMCLQIIhSMQ/records/multi-job-board-scraper-output.png)

*Real output from a default run of Job Scraper in Apify Console.*

### What can this job scraper do?

- **LinkedIn jobs scraper + Indeed scraper in one dataset** - pick one board or several; every row has the same schema and a `jobBoard` field.
- **Multi-keyword, multi-city search** - several search terms and up to 20 locations per run. Each row keeps the `searchTerm` and `searchLocation` that found it.
- **Job alerts that only return new jobs** - turn on `onlyNewJobs` with a `stateKey`, schedule the run, and each run delivers only jobs not delivered before.
- **Relevance filters** - include/exclude title keywords, exclude exact company names, require salary data, remote only, job type, posted within N hours, search radius, Easy Apply.
- **Salary data** - min/max, currency, interval, plus optional conversion of hourly/daily/weekly/monthly pay to annual estimates.
- **Cross-board deduplication** - shared listing URLs and shared direct-apply URLs are merged so you do not pay twice for the same posting.
- **Transparent diagnostics** - per board/term/location outcomes (`returned`, `delivered`, `durationSeconds`, error) in `SEARCH-OUTCOMES`.

### What data can I extract?

| Field | Example / meaning |
|---|---|
| `title`, `company`, `location` | Job title, employer, job location |
| `jobBoard` | `indeed`, `linkedin`, `google`, `glassdoor`, `zip_recruiter`, `bayt`, `naukri` |
| `jobType`, `isRemote` | Employment type and remote flag (null when the board does not say) |
| `salary`, `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryInterval`, `salarySource` | Pay range as published or extracted by the source |
| `datePosted`, `jobAgeDays` | Posting date and age in days |
| `jobUrl`, `directApplyUrl` | Listing URL and the employer's direct application URL when available |
| `companyUrl`, `companyWebsite`, `companyIndustry`, `companySize`, `companyRevenue`, `companyRating`, `companyReviewsCount`, `companyLogo` | Company details when the board supplies them (mostly Indeed) |
| `description` | Job description in Markdown or HTML |
| `skills`, `emails`, `jobLevel`, `jobFunction`, `experienceRange`, `vacancyCount`, `easyApply`, `listingType` | Extra fields some boards provide |
| `hiringSignalScore` | 0-100 freshness/completeness heuristic (not proof of buying intent) |
| `searchTerm`, `searchLocation`, `scrapedAt` | Provenance for every row |

Fields are only filled when the source board publishes them. Missing salary is not zero salary.

### How much does it cost to scrape jobs?

Pay per result: you pay for jobs saved to the dataset, not for searches that return nothing.

| Apify plan discount tier | Price per 1,000 jobs |
|---|---:|
| Free / no discount | $2.00 |
| Bronze | $1.95 |
| Silver | $1.90 |
| Gold, Platinum, Diamond | $1.85 |

- 100 jobs = $0.20, 1,000 jobs = $2.00, 10,000 jobs = $20 (base rate).
- Plus a tiny Actor start charge of $0.00005 per GB of memory per run.
- `maxItems` (default 100) caps output, and the Actor also stops at your **maximum run charge**.

The [Pricing tab](https://apify.com/groupoject/multi-job-board-scraper/pricing) is authoritative for current rates.

### How to scrape LinkedIn and Indeed jobs

1. Open the [input form](https://console.apify.com/actors/LzsIpToRTUZSnqjbS/input).
2. Enter a **Search term** (for example `software engineer`) and a **Location** (or a list of **Multiple locations**).
3. Choose **Job boards**: Indeed (default), LinkedIn, or both. Beta boards are optional.
4. Set **Max results per board** and **Max total jobs**. Start small (20 / 50) to check relevance.
5. Click **Start**, then export the dataset as CSV, Excel or JSON, or read it through the API.
6. For job alerts, enable **Only new jobs**, set a **Monitor state key**, save the input as a task and add a schedule.

### Input example

```json
{
  "searchTerm": "software engineer",
  "locations": ["New York, NY", "Austin, TX", "Boston, MA"],
  "jobBoards": ["indeed", "linkedin"],
  "includeTitleKeywords": ["engineer", "developer"],
  "excludeTitleKeywords": ["intern"],
  "hoursOld": 72,
  "maxResultsPerSite": 20,
  "maxItems": 100,
  "countryIndeed": "USA"
}
```

More ready-made inputs: remote AI jobs, healthcare, finance, sales hiring alerts, salary research and others in [`examples/`](examples/).

### Output example

```json
{
  "title": "Senior Software Engineer",
  "company": "Acme",
  "location": "New York, NY, US",
  "jobBoard": "indeed",
  "jobType": "fulltime",
  "isRemote": false,
  "salary": "$120,000 - $150,000 / year",
  "salaryMin": 120000,
  "salaryMax": 150000,
  "salaryCurrency": "USD",
  "salaryInterval": "yearly",
  "datePosted": "2026-10-20",
  "jobAgeDays": 3,
  "jobUrl": "https://www.indeed.com/viewjob?jk=...",
  "directApplyUrl": "https://careers.acme.com/jobs/123",
  "companyIndustry": "Software",
  "description": "...",
  "hiringSignalScore": 82,
  "searchTerm": "software engineer",
  "searchLocation": "New York, NY"
}
```

The run's key-value store also holds `SUMMARY` (totals, per-board counts, warnings), `SEARCH-OUTCOMES` (one entry per board/term/location search) and `SEARCH-PROGRESS` (live checkpoint).

### Supported job boards

| Board | Status | Notes |
|---|---|---|
| Indeed | Tested default | Most reliable and fastest. Use `countryIndeed` for non-US searches. Company details included. |
| LinkedIn | Tested | Public guest job search, no login. Slower (pages of about 10 jobs with pauses between them). Enable `linkedinFetchDescription` for full descriptions and direct apply URLs (slower again). |
| Google Jobs | Beta | Uses Apify's Google SERP proxy; up to 10 jobs per query; descriptions may be shortened. |
| Glassdoor, ZipRecruiter, Bayt, Naukri | Beta | Often challenged by anti-bot systems; may return no data even with a residential proxy. |

### Job alerts: get only new jobs on a schedule

1. Configure your search and set `"onlyNewJobs": true` and a stable `"stateKey"` such as `"sales-hiring-alerts"`.
2. Save it as an Apify task and add a schedule (for example every morning).
3. The first run delivers all matches; later runs deliver only jobs that were not delivered before under that state key. Jobs removed by filters or the output cap are not marked as seen.
4. Send new rows to Slack, email, Google Sheets or a CRM with an Apify webhook, Make, Zapier or n8n. The Actor itself does not send notifications.

A repeat run that returns zero rows usually means "no new jobs". Check `SEARCH-OUTCOMES` to tell that apart from a source failure. Do not run two monitors with the same `stateKey` at the same time.

### Keep big runs fast and reliable

Searches run one after another: the number of searches is **search terms x locations x job boards**. Each board search is stopped after 120 seconds, and the whole run is limited by the run timeout (1 hour by default).

| Plan | Searches | Typical fit |
|---|---:|---|
| 1 term, 1 city, Indeed | 1 | Seconds |
| 3 terms, 5 cities, Indeed | 15 | Comfortable |
| 3 terms, 5 cities, Indeed + LinkedIn | 30 | Keep LinkedIn at 20-50 results per search |
| 5 terms, 20 cities, 2+ boards | 200+ | Split into several saved tasks |

Tips:

- Keep LinkedIn at **100 results per search or fewer**; add more locations or keywords instead of raising `maxResultsPerSite`.
- Leave `linkedinFetchDescription` off unless you need full LinkedIn text.
- Add beta boards one at a time and check `SEARCH-OUTCOMES` before scheduling them.
- Jobs are saved after every search, so results collected before a timeout stay in the dataset.

### Use the job scraper via API, Python or JavaScript

**Python**

```python
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("groupoject/multi-job-board-scraper").call(run_input={
    "searchTerm": "data engineer",
    "location": "Remote",
    "remoteOnly": True,
    "jobBoards": ["indeed", "linkedin"],
    "maxItems": 100,
})
for job in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(job["title"], job["company"], job["jobUrl"])
```

**JavaScript**

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('groupoject/multi-job-board-scraper').call({
    searchTerms: ['account executive', 'sales manager'],
    locations: ['New York, NY', 'Chicago, IL'],
    jobBoards: ['indeed'],
    maxItems: 200,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

**Integrations:** Make, Zapier, n8n, Google Sheets, Airbyte, webhooks and the Apify API. In n8n, call the Actor by its ID `LzsIpToRTUZSnqjbS`, wait for the run to finish, then read the dataset and upsert rows by `jobUrl`.

**AI agents / MCP:** the Actor can be called from the Apify MCP server, so Claude, ChatGPT, Cursor or your own agent can ask for "remote senior Python jobs posted this week with salary ranges" and get structured rows back.

### Who uses it

- **Recruiters and sourcers** - fresh openings from several boards in one sheet.
- **Job seekers and career coaches** - repeatable searches with daily new-job alerts.
- **Job-board and newsletter builders** - a feed of niche jobs by keyword and city.
- **Sales and lead generation teams** - companies hiring for roles that signal a need for your product.
- **Labor-market and salary researchers** - salary ranges and posting volume by role and city.

### FAQ

**Is it legal to scrape LinkedIn and Indeed jobs?**
The Actor collects publicly listed job postings without logging in. Job postings are generally not personal data, but you are responsible for how you use the data and for complying with applicable laws and the boards' terms. Do not use it to collect private profiles.

**Do I need a LinkedIn or Indeed account, cookies or an API key?**
No. You only need an Apify account (and an Apify API token for programmatic calls).

**Do I need a proxy?**
Apify Proxy is used automatically. For LinkedIn or the beta boards, a residential proxy can help: `{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}`. The `proxyConfiguration` field is stored encrypted.

**Why did I get fewer jobs than I asked for?**
`maxResultsPerSite` and `maxItems` are ceilings. Filters, deduplication, the run-charge limit and what the board actually has all reduce output.

**One board returned nothing. Did the run fail?**
No, as long as another board delivered jobs. `SEARCH-OUTCOMES` shows each board's status (`ok`, `empty`, `failed`) and `SUMMARY.partialResults` is true. If every search fails, the run fails.

**My run timed out. What should I change?**
Reduce terms x locations x boards per run (see "Keep big runs fast and reliable"), keep LinkedIn at 100 results per search or fewer, and split large plans into several scheduled tasks. Jobs saved before the timeout remain in the dataset.

**Can I search outside the US?**
Yes. Set the location and `countryIndeed` (for example `UK`, `Canada`, `Germany`). Naukri (India) and Bayt (Middle East) are optional beta boards.

### Related job scrapers

- [Greenhouse & Lever Jobs Scraper](https://apify.com/groupoject/hiring-signal-jobs-scraper) - jobs straight from company ATS career boards, with hiring-signal scoring.
- [Teamtailor Jobs Scraper](https://apify.com/groupoject/teamtailor-jobs-scraper) - jobs from any Teamtailor career site.
- [SmartRecruiters Jobs Scraper](https://apify.com/groupoject/smartrecruiters-jobs-scraper) - jobs from SmartRecruiters company boards.

### Feedback and support

Found a problem or missing field? [Open an issue](https://console.apify.com/actors/LzsIpToRTUZSnqjbS/issues) with the board, your non-sensitive input and the run ID. Never post API tokens or proxy passwords. If the scraper is useful, a [review](https://console.apify.com/actors/LzsIpToRTUZSnqjbS/reviews) helps other users find it.

Built on the open-source [python-jobspy](https://github.com/cullenwatson/JobSpy) library. Old API identifier `groupoject~indeed-jobs-scraper` has moved: use the Actor ID `LzsIpToRTUZSnqjbS`. See [CHANGELOG.md](CHANGELOG.md).

# Changelog

This Actor's version history is a separate document: https://apify.com/groupoject/multi-job-board-scraper/changelog.md

# Actor input Schema

## `searchTerm` (type: `string`):

Job title or keyword to search for, e.g. "software engineer".

## `searchTerms` (type: `array`):

Run several searches in one go (in addition to or instead of the single term above).

## `location` (type: `string`):

City, state, or country, e.g. "New York, NY" or "Remote".

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

Search up to 20 cities or regions in one run. A non-empty list replaces the single Location field. Each term is searched in each location on every selected board (terms x locations x boards searches, run one after another). If the plan cannot finish within the run timeout, the Actor stops early and keeps the jobs already saved.

## `jobBoards` (type: `array`):

Indeed and LinkedIn are the principal tested sources. Google Jobs is beta: up to 10 cards per query, possible SERP timeouts, and descriptions may be shortened. Glassdoor, ZipRecruiter, Bayt, and Naukri are optional beta sources and may require residential proxy or return no data when challenged.

## `maxResultsPerSite` (type: `integer`):

How many results to request from each board per search term and location. Google Jobs returns up to 10 per query. LinkedIn pages slowly (about 10 jobs per 5-8 s), so keep LinkedIn at 100 or less per search and add locations instead.

## `maxItems` (type: `integer`):

Hard cap on total jobs across all terms, locations, and boards. Defaults to 100; set 0 for unlimited, subject to your run-charge limit.

## `hoursOld` (type: `integer`):

Only jobs posted within this many hours. Leave empty for no limit.

## `includeTitleKeywords` (type: `array`):

Keep titles containing at least one of these phrases (case-insensitive substring match). Filtering happens after scraping, so output can be below the requested maximum.

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

Remove titles containing any of these phrases, such as intern or principal. Exclusions take priority over title inclusions.

## `excludeCompanies` (type: `array`):

Remove exact company-name matches, ignoring case. Useful for excluding your own company or existing accounts.

## `salaryOnly` (type: `boolean`):

Only deliver jobs with a numeric minimum or maximum salary supplied or extracted by the source. This does not estimate missing salaries.

## `jobType` (type: `string`):

Filter by employment type.

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

Only return remote positions.

## `countryIndeed` (type: `string`):

Country for Indeed/Glassdoor, e.g. USA, UK, Canada, Germany.

## `distance` (type: `integer`):

Search radius around the location where supported.

## `offset` (type: `integer`):

Skip the first N results for pagination where supported.

## `easyApply` (type: `boolean`):

Return quick-apply jobs where the selected board supports this filter.

## `linkedinFetchDescription` (type: `boolean`):

Fetch complete LinkedIn descriptions and direct application URLs. Slower but richer.

## `linkedinCompanyIds` (type: `array`):

Optional LinkedIn company IDs to restrict the search.

## `normalizeAnnualSalary` (type: `boolean`):

Convert hourly, daily, weekly, and monthly salary ranges to annual estimates.

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

Format descriptions from JobSpy boards. Google Jobs descriptions remain plain text. Enable LinkedIn full descriptions to collect complete LinkedIn text.

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

For scheduled runs, deliver only previously undelivered jobs using the same state key. Filtered and capped-out jobs are not consumed. Avoid overlapping runs sharing a state key.

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

Label separating saved history for recurring monitors (stored encrypted). Use a different value per monitor, e.g. nyc-data-jobs. Leave empty to use "default".

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

Encrypted proxy settings. Omit for automatic Apify Proxy. Use {"useApifyProxy": true, "apifyProxyGroups": \["RESIDENTIAL"]} for residential proxy, or {"useApifyProxy": false, "proxyUrls": \["http://user:password@host:port"]} for custom proxy. Never put credentials in other fields.

## `debugMode` (type: `boolean`):

Verbose logging.

## Actor input object example

```json
{
  "searchTerm": "software engineer",
  "location": "New York, NY",
  "jobBoards": [
    "indeed"
  ],
  "maxResultsPerSite": 50,
  "maxItems": 100,
  "salaryOnly": false,
  "jobType": "",
  "remoteOnly": false,
  "countryIndeed": "USA",
  "distance": 50,
  "offset": 0,
  "easyApply": false,
  "linkedinFetchDescription": false,
  "normalizeAnnualSalary": false,
  "descriptionFormat": "markdown",
  "onlyNewJobs": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "debugMode": false
}
```

# Actor output Schema

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

One rich record per job with salary, company intelligence, descriptions, skills, direct apply URL, age, and hiring-signal score.

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

Search terms, locations, delivery counts, applied filters, limits, and per-board outcomes.

## `searchOutcomes` (type: `string`):

Status, returned rows, delivered rows, elapsed time and errors for each board/term/location search. Diagnostics are not paid job rows.

# 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 = {
    "searchTerm": "software engineer",
    "location": "New York, NY",
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("groupoject/multi-job-board-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 = {
    "searchTerm": "software engineer",
    "location": "New York, NY",
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("groupoject/multi-job-board-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 '{
  "searchTerm": "software engineer",
  "location": "New York, NY",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call groupoject/multi-job-board-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,groupoject/multi-job-board-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/LzsIpToRTUZSnqjbS/builds/aSGocFRAJhpG6bZL2/openapi.json
