# LinkedIn Jobs Scraper (`datahamster/linkedin-jobs-scraper`) Actor

LinkedIn jobs scraper with no login or cookies. Search LinkedIn job postings by keyword and location, filter by date posted, job type and seniority, and get title, company, location and posted date, plus the salary range and full description on demand. Monitor mode returns only new jobs.

- **URL**: https://apify.com/datahamster/linkedin-jobs-scraper.md
- **Developed by:** [Viktor Dubnytskiy](https://apify.com/datahamster) (community)
- **Categories:** Jobs
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.12 / 1,000 result items

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
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

## LinkedIn Jobs Scraper

Search LinkedIn job postings by keyword and location and get one flat row per job: title, company, company page, location, posted date, and on request the salary range, the full description, seniority level, employment type, job function and industries. No LinkedIn account, no cookies, no session to keep alive. Monitor mode turns the same search into an alert on new jobs.

**Try it:** the prefilled example returns 20 rows in about 10 seconds and costs about $0.03. Results cost $1.40 per 1,000.

### What you get

Real rows from the example dataset, which is the prefilled input (`queries: ["python developer", "data engineer"]`, `location: "United States"`, 10 per keyword):

| title / company | location | posted | query / rank |
|---|---|---|---|
| Python Developer III / Bank of America | Jersey City, NJ | 2026-10-18 | python developer / 10 |
| Senior Python Backend Developer - Onsite / Cognizant | Washington, DC | 2026-10-30 | python developer / 9 |
| Senior Data Engineer, DX / Atlassian | Salt Lake City, UT | 2026-10-18 | data engineer / 1 |
| Data Engineer, Amazon Security Experience / Amazon | Seattle, WA | 2026-10-20 | data engineer / 5 |
| Data Engineer III / Deloitte | Greater Indianapolis | 2026-10-28 | data engineer / 2 |

One full row:

```json
{
  "id": "4475996626",
  "jobId": "4475996626",
  "url": "https://www.linkedin.com/jobs/view/4475996626/",
  "title": "Senior Python Backend Developer - Onsite",
  "company": "Cognizant",
  "companyUrl": "https://www.linkedin.com/company/cognizant",
  "location": "Washington, DC",
  "postedAt": "2026-10-30",
  "salary": null,
  "workplaceType": "onsite",
  "query": "python developer",
  "rank": 9,
  "detailsFetched": false
}
```

With `includeDetails: true` the same row also gets `description`, `salary` (when the employer publishes a pay range, e.g. `$102,900.00/yr - $179,900.00/yr` on the Bank of America posting), `seniorityLevel`, `employmentType`, `industries` and `applicantsText`.

Full row: `id` / `jobId`, `url`, `title`, `company`, `companyUrl`, `location`, `postedAt`, `salary`, `workplaceType`, `query`, `rank`, `detailsFetched`, `scrapedAt`, and with `includeDetails: true` also `description`, `seniorityLevel`, `employmentType`, `jobFunction`, `industries`, `applicantsText`.

`id` is LinkedIn's numeric job id, so a job found by two keywords is one row, and monitor mode tracks it across runs. Rows hold job and employer data only. No recruiter names, no poster profiles, no "who was hired" people.

### Use cases

- **Daily job alert.** One saved task per search (`"queries": ["react developer"]`, `"location": "Germany"`, `"postedWithin": "24h"`) in monitor mode, scheduled every morning. Each run returns only the jobs that were not there yesterday.
- **Hiring signal for sales.** Track which companies post roles in your niche ("data engineer", "SOC analyst") in your region, and act on the `company` / `companyUrl` list.
- **Market research.** Pull a few hundred postings for a role and compare locations, seniority and employment types across countries.

### Try it

Hit **Start**: the prefilled input searches `python developer` and `data engineer` in the United States, 10 jobs per keyword, 20 rows in total, search card fields only. Set `includeDetails: true` to add the description, salary range, seniority and employment type.

**Monitor mode.** Save the input as a task and set:

```json
{
  "mode": "monitor",
  "monitorStateId": "python-germany-daily",
  "queries": ["python developer"],
  "location": "Germany",
  "postedWithin": "24h",
  "webhookUrl": "https://your-endpoint.example.com/hook"
}
```

Put the task on a schedule (Apify, Schedules, cron `0 7 * * *` for daily). Each run compares against the previous one and returns only jobs whose id it has not seen before. A job does not come back because its applicant count grew or its text was edited. Point `webhookUrl` or the Telegram fields at your own channel to get the new jobs pushed to you.

### Input

| Field | Meaning | Default |
|---|---|---|
| `queries` | Keywords, one per line | `["python developer"]` |
| `location` | Country, region, city or metro area as typed on LinkedIn | `United States` |
| `postedWithin` | `any`, `24h`, `week`, `month` | `any` |
| `jobType` | `full-time`, `part-time`, `contract`, `temporary`, `internship`, `volunteer`, `other` | all |
| `experienceLevel` | `internship`, `entry`, `associate`, `mid-senior`, `director`, `executive` | all |
| `includeDetails` | Open each job page for description, seniority, employment type, job function, industries | `false` |
| `maxResultsPerQuery` | Stop each keyword after this many jobs | `100` |
| `strictQueryMatch` | Skip keywords LinkedIn answers with unrelated padding | `true` |
| `maxItems` | Stop the whole run after this many rows | `100` |
| `mode` | `scrape`, or `monitor` for new jobs only | `scrape` |
| `monitorStateId`, `webhookUrl`, `telegramBotToken`, `telegramChatId` | Monitor-mode state key and alert targets | empty |

### Pricing

| Event | Price |
|---|---|
| result | $0.0014 per job ($1.40 per 1,000) |
| monitor-check | $0.005 per monitor run |
| change | $0.0014 per new job in monitor mode |

Apify SILVER subscribers get 10% off these prices, GOLD and higher get 20% off. You pay only for rows actually pushed, and a search that finds nothing charges no result events. Apify platform usage (compute, and proxy traffic on the rare run that has to leave the free tier) is billed by Apify on top.

**Found it useful?** A short review on the Store page helps other people find this actor. If a search misbehaves, open an issue on the actor page.

### Related actors

- [Glassdoor Jobs Scraper: Salary Estimates, Ratings, Monitor](https://apify.com/datahamster/glassdoor-jobs) for employer ratings and salary estimates next to the postings.
- [Wellfound Startup Jobs Scraper](https://apify.com/datahamster/wellfound-startup-jobs) for startup roles with salary and equity ranges.
- [Career Site Jobs Monitor: Greenhouse, Lever, Ashby, Workday](https://apify.com/datahamster/ats-jobs) to watch a specific employer's own career site.
- [Arbeitsagentur Jobs Scraper: Bundesagentur Jobbörse](https://apify.com/datahamster/bundesagentur-jobs) for the German federal job board, including jobs that never reach LinkedIn.

### Limits

- LinkedIn shows a logged-out visitor at most about 1,000 jobs per search (100 pages of 10). For more, split the search by location or by keyword.
- Of LinkedIn's own search filters only "Date posted" works for logged-out visitors. LinkedIn ignores the remote/hybrid, job type and experience filters for them, so `jobType` and `experienceLevel` are checked on each job page instead, which costs one extra request per job.
- There is no remote/hybrid filter. `workplaceType` is filled only when the title or location says so explicitly ("100% Remote", "Hybrid"); LinkedIn does not print the workplace type on the public pages.
- `salary` comes from the job page, so it needs `includeDetails: true`, and it appears only when the employer publishes a base pay range. US postings often do, most others do not.
- `jobFunction` is filled only when LinkedIn prints a "Job function" line on the public job page. Most public pages show only seniority, employment type and industries, so expect it to be empty.
- LinkedIn never returns an empty answer for a keyword it cannot match. With `strictQueryMatch` on, a keyword is skipped when no job on its first page names every significant word of it in the title or company (generic words such as "developer", "manager" or "senior" do not count on their own). Turn it off when your keyword and the job titles are in different languages.
- With `jobType` or `experienceLevel` set, each keyword may open at most 5 job pages per row you asked for (`5 x min(maxResultsPerQuery, maxItems)`, at least 50). A filter that almost nothing matches therefore stops the keyword after that budget instead of opening up to 1,000 job pages; `LINKEDIN_STATS.detailBudgetHit` lists the keywords where this happened.
- E-mail addresses and phone numbers that recruiters type into the job text are removed from `description`.
- LinkedIn rate-limits logged-out traffic. The actor paces its requests, retries a rate limit with backoff and moves to a datacenter, then a residential proxy only when the free tier keeps refusing. A run that is refused on every tier stops with `blocked` or `rate_limited` in the run summary instead of an empty dataset.

### FAQ

**Do I need a LinkedIn account or cookies?** No. Every request is made as a logged-out visitor.

**Why did my keyword return nothing?** Either LinkedIn has no jobs for it in that location, or it padded the answer with unrelated jobs and `strictQueryMatch` skipped it. Both end as `no_matches` in the `RUN_SUMMARY` record of the run's key-value store; the skipped keywords are listed in `LINKEDIN_STATS.skippedQueries` and in the run log. A run that LinkedIn refused ends as `blocked` or `rate_limited` instead.

**Can I search several locations?** Run one task per location. A location is one LinkedIn search parameter, and splitting keeps each task's monitor state clean.

### Changelog

- 0.1: initial release with keyword and location search, date-posted filter, job type and seniority filters on job pages, optional job details, and monitor mode for new jobs.

# Actor input Schema

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

Stop the whole run after this many jobs (you are charged only for pushed jobs). Example: 20.

## `mode` (type: `string`):

scrape = full results; monitor = only new/changed items since the previous run of this task

## `monitorStateId` (type: `string`):

Optional state id when not running as a saved task (monitor mode)

## `webhookUrl` (type: `string`):

POST a change summary here in monitor mode

## `telegramBotToken` (type: `string`):

Optional: bot token for monitor-mode change summaries

## `telegramChatId` (type: `string`):

Optional: chat id that receives monitor-mode summaries

## `queries` (type: `array`):

Job keywords to search on LinkedIn, one per line, e.g. "python developer", "data analyst", "registered nurse". Each keyword is a separate LinkedIn job search in the chosen location.

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

Where to search, written the way you would type it into LinkedIn: a country ("Germany"), a state or region ("California, United States"), a city ("London, England, United Kingdom") or a metro area ("San Francisco Bay Area"). Leave empty and LinkedIn picks the location from the exit IP.

## `postedWithin` (type: `string`):

Only jobs posted within this window. LinkedIn applies it on its side, so a narrow window costs fewer requests. Example: 24h for a daily alert.

## `jobType` (type: `array`):

Keep only these employment types, e.g. \["full-time", "contract"]. LinkedIn does not apply this filter for logged-out visitors, so the actor opens each job page to check it (one extra request per job, details are included in the row). Empty = all types.

## `experienceLevel` (type: `array`):

Keep only these seniority levels, e.g. \["entry", "associate"]. Checked on each job page like Job type (one extra request per job). Empty = all levels.

## `includeDetails` (type: `boolean`):

true = open every job page and add the full description (plain text), seniority level, employment type, job function, industries and the applicants line. One extra request per job, so slower. false = search card fields only. Example: false.

## `maxResultsPerQuery` (type: `integer`):

Stop each keyword after this many jobs, so one broad keyword cannot use up Max items for the others. LinkedIn itself shows at most about 1,000 jobs per search. Example: 100.

## `strictQueryMatch` (type: `boolean`):

LinkedIn never answers "no results" to a keyword it cannot match; it shows 10 unrelated local jobs instead. true = when no job among the first 10 names every significant word of your keyword in its title or company (generic words like "developer" or "manager" do not count on their own), the keyword is skipped and nothing is charged. Turn it off when your keyword is in a different language than the job titles (e.g. "accountant" for jobs titled "Buchhalter"). Example: true.

## Actor input object example

```json
{
  "maxItems": 20,
  "mode": "scrape",
  "queries": [
    "python developer",
    "data engineer"
  ],
  "location": "United States",
  "postedWithin": "any",
  "includeDetails": false,
  "maxResultsPerQuery": 10,
  "strictQueryMatch": true
}
```

# Actor output Schema

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

All pushed rows (dataset, JSON)

## `resultsTable` (type: `string`):

Dataset in the Console viewer

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

RUN_SUMMARY record

# 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 = {
    "maxItems": 20,
    "queries": [
        "python developer",
        "data engineer"
    ],
    "location": "United States",
    "maxResultsPerQuery": 10
};

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

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

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

```

## Python example

```python
from apify_client import ApifyClient

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

# Prepare the Actor input
run_input = {
    "maxItems": 20,
    "queries": [
        "python developer",
        "data engineer",
    ],
    "location": "United States",
    "maxResultsPerQuery": 10,
}

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

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

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

```

## CLI example

```bash
echo '{
  "maxItems": 20,
  "queries": [
    "python developer",
    "data engineer"
  ],
  "location": "United States",
  "maxResultsPerQuery": 10
}' |
apify call datahamster/linkedin-jobs-scraper --silent --output-dataset

```

## MCP server setup

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

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

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/H67DxQ8gHQAJtIMUs/builds/cvIXvGrH0nqUnDs0e/openapi.json
