# LinkedIn Jobs Scraper — No 1K Cap · No Minimum · No Login (`memo23/linkedin-jobs-scraper`) Actor

LinkedIn Jobs scraper, no login or cookies, fetched live. Works with existing n8n/Make workflows: other actors' input and field names are accepted. No 1K cap, no per-run minimum, free company data, cross-run dedup, CV match score. Salary, description, apply URL. From $0.25/1k.

- **URL**: https://apify.com/memo23/linkedin-jobs-scraper.md
- **Developed by:** [Muhamed Didovic](https://apify.com/memo23) (community)
- **Categories:** Jobs, Lead generation, Social media
- **Features:** Limited permissions
- **Stats:** 330 total users, 115 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $0.25 / 1,000 job 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

## LinkedIn Jobs Scraper

The fastest way to turn LinkedIn Jobs into structured, spreadsheet-ready data — **without a LinkedIn account, cookies, or login**.
Start from a search URL, a keyword + location, a single job URL, or a plain list of job IDs, and get clean job rows with title, company, salary, full description, apply URL, and recruiter contacts (best-effort) — as JSON or CSV.

**From $0.25 per 1,000 jobs** on higher plans ($0.69/1k on the free plan) — pay only for the jobs that land in your dataset. Filtered, duplicate, and previously-seen jobs cost nothing. **No minimum per run:** set `maxItems` to 10 and you are billed for at most 10 jobs.

**Switching from another LinkedIn jobs actor?** Keep your input JSON and your n8n / Make / Zapier workflow as they are and change only the actor ID. See [Switching from another LinkedIn jobs actor](#switching-from-another-linkedin-jobs-actor).

Every job is fetched **live from LinkedIn at run time** — not served from a pre-indexed job database — so you never pay for stale or already-closed postings, and coverage isn't limited to what a database indexed last week.

### Why Use This Scraper?

- ✅ **No login, no cookies, no account risk** — reads only LinkedIn's public Jobs surface
- ✅ **$0.25/1k on Gold and above** ($0.69/1k on the free plan) — you pay per job that lands in the dataset, and filtered, duplicate or already-seen jobs cost nothing
- ✅ **Recruiter & hiring-team contacts** (best-effort) — name, title, profile URL of the job poster
- ✅ **Bypass LinkedIn's ~1,000-results-per-search cap** — split by city or experience level automatically
- ✅ **Job monitoring built in** — paste job IDs, get back `isExpired` / `verifiedAt` / `verificationStatus` per listing
- ✅ **Free result filters** — title include/exclude, employer blocklist, and salary-required run *before* billing
- ✅ **Recruiter-grade search filters** — under-10-applicants only, Easy Apply only, minimum salary ($40K–$200K+)
- ✅ **🎯 Profile Match** — score every job 0–100 against your CV skills (`resumeKeywords`) and sort by fit
- ✅ **Cross-run dedup** — `onlyNewJobs` removes duplicate jobs across runs; you only ever pay for NEW jobs
- ✅ **No minimum per run** — `maxItems` can be any number; a daily alert capped at 20 jobs bills at most 20
- ✅ **Drop-in for existing workflows** — input and output field names used by other LinkedIn job actors are accepted, so an n8n/Make flow keeps working after you swap the actor ID
- ✅ **Company firmographics at no extra charge** — website, size, employees, HQ, industry, follower count (`scrapeCompanyDetails`)

### Overview

The LinkedIn Jobs Scraper is built for recruiters, lead-gen teams, job-market analysts, and job-board builders who need structured job-posting data from LinkedIn without maintaining a logged-in session.

The output is always **job-shaped rows**. Whether you start from a search URL, keyword filters, a single job URL, or a list of job IDs, every dataset item is one job posting with the same ~40-field schema — so your downstream pipeline never has to branch on input type.

Cookie-based LinkedIn scrapers require you to hand over a logged-in session (which gets accounts flagged) or buy session cookies from third parties. This actor reads the same public Jobs pages any anonymous browser can see, so no account is required and none is at risk. The trade-off is honest: a few fields LinkedIn reserves for logged-in viewers (some recruiter cards, some salary ranges) are best-effort.

### Supported Inputs

#### URL types

| URL type | Pattern | What it returns |
|---|---|---|
| Search URL | `linkedin.com/jobs/search?keywords=…&location=…` | Paginated job listings, filters read from the URL |
| Single-job URL | `linkedin.com/jobs/view/4410745146/` | One enriched row per URL, no search phase |
| Slug job URL | `linkedin.com/jobs/view/data-engineer-at-netflix-4350364210` | Same as above |

#### Copy-pasteable startUrls

```json
{
  "startUrls": [
    { "url": "https://www.linkedin.com/jobs/search?keywords=Software%20Engineer&location=United%20States&geoId=103644278" },
    { "url": "https://www.linkedin.com/jobs/view/4410745146/" }
  ]
}
```

#### Job IDs mode — scrape or re-verify exact listings

Paste raw job IDs, view URLs, or any LinkedIn URL carrying `currentJobId=` into the `jobIds` field. Each entry goes straight to the job detail page. Re-run the same list on a schedule to monitor listings — every row includes `isExpired`, `verifiedAt`, and `verificationStatus` (`active` / `closed` / `removed`), so dead listings are flagged instead of silently missing.

```json
{
  "jobIds": ["4410745146", "https://www.linkedin.com/jobs/view/4406118990/"]
}
```

#### Keyword / filter mode

Leave URLs empty and let the actor build the search: keywords × locations × time range × workplace type × experience level × contract type × company names.

```json
{
  "keywords": ["Data Engineer"],
  "location": "United States",
  "timeRange": "r604800",
  "remote": ["2"],
  "experienceLevels": ["4", "5"]
}
```

#### Unsupported inputs

- ❌ URLs behind the LinkedIn login wall (Sales Navigator, Recruiter seats, private postings)
- ❌ LinkedIn profile / company-page URLs (see the dedicated actors in *Explore More Scrapers*)
- ❌ Shortened or redirect URLs (`lnkd.in/…`)

### Switching from another LinkedIn jobs actor

Most LinkedIn job workflows are wired to one actor's field names. This actor reads those names too, so switching takes one change.

1. In your n8n HTTP node, Make module, Zapier step, or API script, replace the actor ID with `memo23~linkedin-jobs-scraper`.
2. Keep the same input JSON. Fields like `keyword`, `workType`, `jobType`, `publishedAt`, `companyInclude`, `jobTitleExclude`, text experience levels (`"entry-level"`) and raw salary amounts (`"80000"`) are translated to this actor's filters. The run log prints a `[COMPAT] input mapped:` line showing each translation.
3. Downstream nodes keep working. When the input uses those field names, every job row also carries the field names your workflow already reads, next to this actor's own fields. To get them with a native input, set `compatibleOutput: true`.

#### Input fields that are translated

| Field in your existing input | Read here as |
|---|---|
| `keyword` | `keywords` |
| `publishedAt` (`r86400`, `r604800`, `r2592000`) | `timeRange` |
| `jobType` (`full-time`, `contract`…) | `contractType` |
| `workType` (`on-site`, `remote`, `hybrid`) | `remote` |
| `experienceLevel` (`internship` … `executive`) | LinkedIn experience codes `1`–`6` |
| `salaryBase` (`"40000"` … `"120000"`) | LinkedIn salary buckets ($40K+ … $120K+) |
| `distance` (`"15"`) | nearest supported radius, rounded up (`"25"`) |
| `companyInclude` / `companyExclude` / `jobTitleExclude` | `companyNames` / `excludeCompanies` / `titleExclude` |
| `saveOnlyUniqueItems` | always on here (duplicates within a run are never saved) |
| `enrichCompanyData` | `scrapeCompanyDetails` (no extra charge) |

Fields with the same name and format (`startUrls`, `location`, `locations`, `maxItems`, `filterUnder10Applicants`, `filterEasyApply`, `requireSalaryInfo`, `resumeKeywords`, the company and job-function filters) are used as they are.

#### Output fields added for existing workflows

`jobId`, `jobTitle`, `companyName`, `companyUrl`, `salaryInfo` (e.g. `["$69100.00", "$129200.00"]`), `postedTime`, `publishedAt` (ISO), `searchString`, `jobDescription`, `applicationsCount` (e.g. `"Over 200 applicants"`), `contractType`, `experienceLevel`, `workType`, `sector`, `applyType` (`EASY_APPLY` / `EXTERNAL`), `posterFullName`, `posterProfileUrl`, and with `resumeKeywords`: `matchedKeywords`, `unmatchedKeywords`, `keywordMatchScorePercentage`. Shared names (`location`, `jobUrl`, `applyUrl`, `companyLogo`, `yearsOfExperience`, `dynamicFilterMatch`, company fields) already match.

#### Coming from another LinkedIn jobs scraper?

These are the complaints that show up on the largest public guest scraper. This actor's answer:

| Their issue | Here |
|---|---|
| Germany search returned US jobs | `location` / `geoId` from your input or pasted URL is what we send. Country names have fixed LinkedIn geoIds (Germany is `101282230`). City split does not overwrite a pasted URL. |
| City split used Warsaw, Indiana for Poland | Split cities go out as `Warsaw, Poland`. Typeahead picks the hit whose country matches. |
| `f_JT=C` still returned mostly Full-time | After AI search, Contract/Full-time/Remote are enforced on the job row. Mismatches are dropped and never charged. |
| `sortBy=DD` was not chronological | We do not claim LinkedIn rank. `postedAt` is the listing's ISO datetime so you sort the dataset yourself. Converted `sort by: most recent` is a keyword, not a sort. |
| Need an exact `postedAt` | `postedAt` comes from `<time datetime>` on the card and the detail page (ISO). `postedTimeAgo` is the relative string next to it. |
| Actor unusually slow | Company scrape and full details are off unless you turn them on. City split multiplies searches on purpose — raise `maxItems` if you want the extra cities. |
| Stopped at 1000 / Florida | LinkedIn's per-search ceiling. `splitByLocation` is country-wide (every major city), not one state. `autoSplit` is the experience-level alternative. |
| Jobs that require Japanese | Put `Japanese` (or `Nihongo`) in `descriptionExclude`. |

#### Differences to know about

- **No minimum per run.** `maxItems` accepts any number.
- **Duplicates across runs:** turn on `onlyNewJobs` and each scheduled run returns, and bills, only jobs it has not returned before. No dedup step needed in your workflow.
- **More than 1,000 jobs per search:** `splitByLocation` or `autoSplit` fan the search out automatically.
- **Company enrichment has no extra charge.** `enrichCompanyData: true` and the company-size / founded / followers / organization-type filters turn it on; each unique company page is fetched once per run, so runs take a little longer.
- **`maxItems` defaults to 1,000.** If your old input left it empty to mean "no limit", set a number explicitly.

### Use Cases

| Audience | Use case |
|---|---|
| **Recruiters & sourcing teams** | Pull hiring-manager contacts from open roles and reach out before the inbox floods |
| **B2B lead-gen / HR-tech vendors** | Companies actively hiring for X are in-market for your product — build the list daily |
| **Job boards & aggregators** | Backfill listings at $0.25–0.69/1k with cross-run dedup so you only ingest new postings |
| **Market & salary analysts** | Track demand by title, region, seniority, and published salary bands |
| **Sales intelligence teams** | A competitor's job posts reveal their stack, roadmap, and the decision-makers to pitch |
| **Agencies** | Deliver client-ready hiring datasets on a schedule without writing scrapers |

### How It Works

![How It Works](https://muhamed-didovic.github.io/assets/how-it-works-linkedin-jobs.png)

1. **Input** — paste search URLs, job URLs, job IDs, or set keyword + location filters
2. **Route** — search inputs paginate the public Jobs listing; job IDs and view URLs skip straight to the detail page
3. **Filter (free)** — title include/exclude and employer blocklist drop unwanted jobs *before* they're collected or billed
4. **Collect & enrich** — every kept job gets the full ~40-field row; optional company firmographics per unique employer
5. **Output** — JSON or CSV dataset, plus a `RUN_SUMMARY` record explaining exactly how the run ended

### Input Configuration

#### Input fields

| Field | Type | Required | Notes |
|---|---|---|---|
| `naturalLanguageQuery` | `string` | optional | 🤖 Plain-English brief (e.g. `remote senior backend engineers in Germany posted this week`) — one AI call maps it to keywords/location/remote/experience/contract/date at run start, filling only the fields you leave blank |
| `startUrls` | `array<{url}>` | one input mode | Mix of search URLs and single-job view URLs |
| `autoConvertToAiSearch` | `boolean` | optional | Default on. LinkedIn AI search dropped classic URL filters (experience, job type, workplace, salary, sort); those are rewritten into keywords. Date posted, company, Easy Apply, and under-10 stay as URL filters. Turn off to send the original `f_*` params |
| `jobIds` | `array<string>` | one input mode | Raw IDs, view URLs, or `currentJobId=` URLs — direct to detail page |
| `skipJobId` | `array<string>` | optional | Skip these listings (IDs or view URLs) so a rerun does not rebill jobs you already have — free, never charged |
| `keywords` | `array<string>` | one input mode | Used when no URLs are given |
| `location` / `locations` | `string` / `array` | optional | City, region, or country in plain words. Each name is looked up to a LinkedIn `geoId` (United States / UK / Germany / … are known without a fetch). `Remote` is left as text — use the workplace filter for remote jobs |
| `geoId` | `string` | optional | Override for a **single** location. Leave empty to auto-resolve. Ignored when searching several locations or splitting by city |
| `placeIds` | `array` | optional | Extra LinkedIn place filters (`f_PP`) on top of the resolved geo |
| `timeRange` | enum | optional | `any time`, last 24h (`r86400`), week (`r604800`), month (`r2592000`) |
| `postedWithinHours` | `integer` | optional | Custom freshness in hours (e.g. `1`, `72`) — overrides Time range. LinkedIn's dropdown only offers day/week/month |
| `postedAfter` | `string` | optional | Drop jobs whose exact `postedAt` calendar date is before this day (`YYYY-MM-DD`) — free, never charged |
| `remote` / `jobTypes` | `array` enum | optional | On-site `1` / Remote `2` / Hybrid `3` |
| `experienceLevels` | `array` enum | optional | `1` intern, `2` entry, `3` associate, `4` mid-senior, `5` director, `6` executive (text values like `"entry-level"` are accepted too) |
| `contractType` | `array` enum | optional | `F`/`P`/`C`/`T`/`I`/`V`/`O` |
| `companyNames` | `array<string>` | optional | Only include these employers (resolved to a LinkedIn company ID, filtered server-side) |
| `companyIds` | `array<string>` | optional | Same, but by raw LinkedIn company ID (`f_C`, e.g. `1035`) — used verbatim, skips the name→ID lookup |
| `filterUnder10Applicants` | `boolean` | optional | LinkedIn's own "early applicant" filter — jobs with <10 applicants |
| `filterEasyApply` | `boolean` | optional | Only jobs that support Easy Apply (apply without leaving LinkedIn) |
| `excludeEasyApply` | `boolean` | optional | Drop Easy Apply jobs and keep employer-site apply links — free, never charged. Ignored when Easy Apply only is on |
| `salaryBase` | enum `1`–`9` | optional | LinkedIn's minimum-salary filter, $40K+ → $200K+ (mostly US) |
| `distance` | enum (miles) | optional | Search radius around the location (LinkedIn's own `distance` filter). `0` = exact location only |
| `titleMustInclude` | `array<string>` | optional | Keep only titles containing at least one term — free, pre-billing |
| `titleExclude` | `array<string>` | optional | Drop titles containing any term — free, pre-billing |
| `excludeCompanies` | `array<string>` | optional | Drop these employers — free, pre-billing |
| `subLocationExclude` | `array<string>` | optional | Drop jobs whose location contains any term (e.g. a city) — free, pre-billing |
| `descriptionInclude` | `array<string>` | optional | Keep only jobs whose description body contains at least one term — dropped jobs never charged |
| `descriptionExclude` | `array<string>` | optional | Drop jobs whose description body contains any term (e.g. `security clearance`) — never charged |
| `requireSalaryInfo` | `boolean` | optional | Drop jobs without a published salary — free, pre-billing |
| `requirePoster` | `boolean` | optional | Drop jobs that do not name a recruiter — free, never charged (unlike `requireRecruiterProfile`, which only flags the row) |
| `resumeKeywords` | `array` | optional | 🎯 Profile Match: score each job 0–100 vs your CV skills (never filters) |
| `scrapeCompanyDetails` | `boolean` | optional | Company firmographics, fetched once per **unique** company; no extra charge |
| `scrapeJobDetails` | `boolean` | optional | Off: search-card fields only (title/company/location/URL/date) — faster monitoring. On (default): full description, salary, recruiter |
| `includeDescriptionHtml` | `boolean` | optional | Off omits `descriptionHtml` to shrink the dataset. On by default |
| `compatibleOutput` | `boolean` | optional | Add the output field names other LinkedIn job actors use (`jobTitle`, `companyName`, `salaryInfo`…) next to ours. On automatically when the input uses those actors' field names |
| `aiEnrich` | `boolean` | optional | 🤖 AI-normalize each job into an `aiEnrichment` block (visa / category / language / seniority / remote type); billed per **job** (one AI call each) |
| `onlyNewJobs` | `boolean` | optional | Cross-run dedup — only jobs not returned in previous runs |
| `autoSplit` | `boolean` | optional | Bypass ~1K cap by splitting the search across experience levels |
| `splitByLocation` + `splitCountry` | `boolean` + enum | optional | Bypass ~1K cap by fanning out across a country's major cities. Empty `splitCountry` follows `location` (Germany → German cities). A leftover `US` does not override a non-US location |
| `maxItems` | `integer` | optional | Hard cap on charged dataset rows |
| `maxItemsPerSearch` | `integer` | optional | Cap each keyword × location query so one broad search cannot consume `maxItems` |
| `minDelay`/`maxDelay`, `minConcurrency`/`maxConcurrency`, `maxRequestRetries` | `integer` | optional | Crawl pacing |
| `proxy` | `object` | optional | Residential recommended |

#### 🔎 Dynamic filters

Unlike the free filters above (which drop jobs *before* you pay), dynamic filters are evaluated **after** each job — and, for the company-based ones, its company page — is fetched. So every job is still saved and charged; non-matching jobs are stamped **`dynamicFilterMatch: false`** so you can discard them downstream (e.g. keep only `dynamicFilterMatch == true` in your N8N/Make flow). Per dimension, an `*Include` list takes priority over its `*Exclude` sibling. Company-based filters auto-enable company enrichment; companies missing a value always pass.

| Field | Type | Notes |
|---|---|---|
| `jobFunctionInclude` / `jobFunctionExclude` | `array` enum | Keep / flag by LinkedIn job function |
| `jobIndustryInclude` / `jobIndustryExclude` | `array<string>` | Keep / flag by industry name (e.g. `Staffing and Recruiting`) |
| `excludeRecruitingAgencies` | `boolean` | Flag staffing / recruiting-agency postings |
| `companySizeMin` / `companySizeMax` | `integer` | Flag by company employee count |
| `companyOrganizationTypeInclude` / `…Exclude` | `array` enum | Flag by company type (Public Company, Non Profit, Privately Held, …) |
| `companyFoundedDateMin` / `companyFoundedDateMax` | `integer` (year) | Flag by company founding year |
| `companyFollowersCountMin` / `companyFollowersCountMax` | `integer` | Flag by LinkedIn follower count |
| `requireRecruiterProfile` | `boolean` | Flag postings that expose no recruiter / hiring-team profile |

#### Common scenarios

**1. Daily new-jobs monitor (schedule this)**

```json
{
  "keywords": ["DevOps Engineer"],
  "location": "Germany",
  "timeRange": "r86400",
  "onlyNewJobs": true,
  "titleExclude": ["senior", "principal"],
  "maxItems": 200
}
```

**2. Re-verify a list of tracked listings**

```json
{
  "jobIds": ["4410745146", "4406118990", "4350364210"]
}
```

**3. Break the 1,000-result cap for a whole country**

```json
{
  "keywords": ["Nurse"],
  "splitByLocation": true,
  "splitCountry": "GB",
  "maxItems": 5000
}
```

### Output Overview

Each dataset item is one job posting containing:

- **Core fields** — `id`, `title`, `company`, `location`, `postedAt`, `jobUrl`, full `description` (+ `descriptionHtml`)
- **Compensation & criteria** — parsed `salary` (`minAmount` / `maxAmount` / `currency` / `period`), `seniorityLevel`, `employmentType`, `jobFunction`, `industries`, `benefits`
- **Application** — `applyUrl` (external careers site when exposed), `easyApply`, `contactEmails[]` (addresses found in the description), `numberOfApplicants` / `applicantsCount`
- **People** — `hiringTeam[]` and flat `jobPosterName` / `jobPosterTitle` / `jobPosterProfileUrl` (best-effort; empty means LinkedIn hid it from anonymous viewers)
- **Company** — `companyLinkedinUrl` always; with `scrapeCompanyDetails` also website, industry, size, employee count, HQ, logo, follower count, organization type, founded year, specialties, office locations
- **Targeting** — `yearsOfExperience[]` parsed from the description, `visaSponsorship` (`sponsored` / `not_sponsored` / `unknown`, inferred from the description — free), and `dynamicFilterMatch` (the 🔎 dynamic-filter verdict) on every row
- **Verification** — `isExpired`, `verifiedAt`, `verificationStatus` (`active` / `closed` / `removed`) on every row
- **Provenance** — `scrapedFrom` (`search-results` / `job-view-url` / `job-id-input`), `sourceUrl`

Every run also writes a **`RUN_SUMMARY`** record to the key-value store: requested vs saved counts, pages parsed, jobs dropped per filter, expired jobs found, `pagingLimitHit` when a query hit LinkedIn's ~1,000-result wall, and a `stopReason` — so "why did I get 400 instead of 1000?" is answered by the run itself.

### Output Samples

#### Search start (trimmed real row)

```jsonc
{
  "id": "4350364210",
  "title": "Data Engineer (L5)",
  "company": "Netflix",
  "companyLinkedinUrl": "https://www.linkedin.com/company/netflix",
  "location": "United States",
  "seniorityLevel": "Not Applicable",
  "employmentType": "Full-time",
  "industries": ["Entertainment Providers"],
  "postedAt": "2026-07-02",
  "postedTimeAgo": "5 days ago",
  "numberOfApplicants": "Over 200 applicants",
  "applicantsCount": 200,
  "easyApply": false,
  "description": "At Netflix, our mission is to entertain the world. Together, we are writing the next episode…",
  "isExpired": false,
  "verifiedAt": "2026-07-08T03:58:41.120Z",
  "verificationStatus": "active",
  "jobUrl": "https://www.linkedin.com/jobs/view/data-engineer-l5-at-netflix-4350364210",
  "scrapedFrom": "search-results"
  /* …plus salary, applyUrl, hiringTeam, jobPoster*, company* fields… */
}
```

#### Job-ID start — removed listing (monitoring)

```jsonc
{
  "id": "3000000001",
  "title": null,
  "company": null,
  "jobUrl": "https://www.linkedin.com/jobs/view/3000000001/",
  "isExpired": true,
  "verifiedAt": "2026-07-08T03:57:33.319Z",
  "verificationStatus": "removed",
  "scrapedFrom": "job-id-input"
}
```

### Key Output Fields

#### Job core

- `id`, `title`, `company`, `location`, `postedAt`, `postedTimeAgo`, `jobUrl`, `sourceUrl`
- `foundBy[]`, `search` — which keyword × location query produced the row
- `detailScraped` — `false` on card-only runs (`scrapeJobDetails: false`)

#### Description & criteria

- `description`, `descriptionHtml`, `criteria[]`, `seniorityLevel`, `employmentType`, `jobFunction`, `industries[]`, `benefits[]`
- `workplaceType` — `Remote` / `Hybrid` / `On-site`. LinkedIn's own label when the page shows one (`workplaceTypeSource: "labeled"`); otherwise read from the title, location line, and description (`"inferred"`). `null` when the posting never states an arrangement — we do not invent On-site from a headquarters mention
- `yearsOfExperience[]` — structured experience requirements parsed from the description (`{ years: "5+" | "2-5" | "3", context, lang }`); empty when none found
- `visaSponsorship` — `sponsored` / `not_sponsored` / `unknown`, inferred from the description text (deterministic, free on every row); `unknown` when the description says nothing either way
- `aiEnrichment` — present only with `aiEnrich`; an AI-normalized block `{ visaSponsorship, jobCategory, language, seniority, remoteType }` read from the posting (billed per job). The free rule-based top-level `visaSponsorship` is always there regardless
- `dynamicFilterMatch` — `true` when the job passed every active 🔎 dynamic filter (or none were set); `false` flags a non-match (the row is still returned)

#### Compensation & application

- `salary.raw`, `salary.minAmount`, `salary.maxAmount`, `salary.currency`, `salary.period`
- `applyUrl`, `easyApply`, `contactEmails`, `numberOfApplicants`, `applicantsCount`

#### Recruiter / hiring team (best-effort)

- `hiringTeam[].name`, `hiringTeam[].title`, `hiringTeam[].linkedinUrl`, `hiringTeam[].photo`
- `jobPosterName`, `jobPosterTitle`, `jobPosterProfileUrl`, `jobPosterPhoto`

#### Company (flat fields filled by `scrapeCompanyDetails`)

- `companyLinkedinUrl`, `companyLinkedinSlug`, `companyWebsite`, `companyIndustry`, `companySize`, `companyEmployeesCount`, `companyHeadquarters`, `companyFollowerCount`, `companyLogo`, `companyDetails`
- `companyOrganizationType`, `companyFoundedDate`, `companySpecialties[]`, `companyOfficeLocations[]` (best-effort from the public company page)

#### Profile Match (with `resumeKeywords`)

- `profileMatchScore` (0–100 = % of your skills found in title + description)
- `profileMatch.matchedKeywords[]`, `profileMatch.missingKeywords[]`, `profileMatch.matchCount`, `profileMatch.totalKeywords`

#### Verification & provenance

- `isExpired`, `verifiedAt`, `verificationStatus`, `scrapedFrom`

### FAQ

#### Do I need a LinkedIn account or cookies?

No. The actor reads only the public Jobs surface that any anonymous browser can access. No login, no cookies, no account at risk.

#### Why did I get fewer jobs than `maxItems`?

Check the `RUN_SUMMARY` record in the run's key-value store — it reports pages parsed, candidates seen, how many jobs each filter dropped, and the `stopReason`. The most common causes: the search genuinely has fewer results, LinkedIn's ~1,000-per-search ceiling (`pagingLimitHit: true` — enable `autoSplit` or `splitByLocation`), `maxItemsPerSearch` capping one query, or your title/company filters doing their job.

#### Are the title and company filters really free?

Yes. `titleMustInclude`, `titleExclude`, and `excludeCompanies` run before a job is collected, so filtered jobs are never charged and never consume detail-page requests.

#### Are recruiter contacts always available?

No — best-effort. LinkedIn shows the hiring-team card to anonymous viewers on some jobs and hides it on others. `hiringTeam: []` means LinkedIn hid it, not that the parser failed. Same honesty applies to `salary` and `applyUrl`: they're populated when the employer published them.

#### How do I monitor jobs for expiry?

Put the job IDs (or view URLs) in `jobIds` and run on a schedule. Live listings return full rows with `verificationStatus: "active"`; closed listings are flagged `"closed"`; listings deleted from LinkedIn come back as sparse rows with `"removed"` and `isExpired: true`.

#### How do I get more than 1,000 jobs per search?

LinkedIn caps every guest search at ~1,000 results. Enable `splitByLocation` (fans the search out across a country's major cities) or `autoSplit` (splits across the six experience levels). Results are deduplicated automatically.

#### How do I find jobs with the best odds of a reply?

Turn on `filterUnder10Applicants` — LinkedIn's own "early applicant" filter — so every returned job has fewer than 10 applicants. Combine with `timeRange: "r86400"` (last 24h) and a scheduled daily run with `onlyNewJobs` for a fully automated early-bird pipeline.

#### How does Profile Match (`resumeKeywords`) work?

Add your skills as plain strings or `{ "keyword": "JavaScript", "aliases": ["JS"] }` objects. Every job is scored — `profileMatchScore` is the % of your skills found in its title + description, and `profileMatch` lists exactly which matched and which are missing. It never drops jobs; sort the dataset by `profileMatchScore` descending, or have your N8N/Make workflow act only on scores above a threshold.

#### Is the dataset in LinkedIn order?

No. Guest search order is not a stable rank, and `sortBy` on a pasted URL is turned into a keyword (`sort by: most recent`) when AI conversion is on. Sort the dataset by `postedAt` (ISO datetime from the listing) if you need newest first.

#### Why did my experience / job-type / workplace URL filters disappear?

LinkedIn's AI job search (August 2026) stopped honoring those URL facets. With `autoConvertToAiSearch` on (the default), the actor turns them into keywords (`Mid-Senior level`, `Full-time`, `Remote`, `$100,000+`, `sort by: most recent`) and leaves date, company, Easy Apply, and under-10 as real URL filters. Turn the option off if you want the original `f_*` params sent unchanged.

#### Can I reuse the input from another LinkedIn jobs actor?

Yes. Paste it unchanged and replace only the actor ID. Alternative field names and value formats are translated (the run log lists each one under `[COMPAT]`), and the output also carries the field names your workflow already reads. Details in [Switching from another LinkedIn jobs actor](#switching-from-another-linkedin-jobs-actor).

#### Is there a minimum number of results per run?

No. `maxItems: 10` returns and bills at most 10 jobs.

#### What does `onlyNewJobs` do?

It remembers the job IDs from your previous runs and skips them, so a scheduled run only returns postings it has never sent you before.

#### How is `skipJobId` different from `onlyNewJobs`?

`onlyNewJobs` remembers every ID this actor has already returned. `skipJobId` is an explicit list you pass in (IDs or view URLs) — useful when the previous dataset lived somewhere else. Direct `jobIds` / view URLs are never skipped.

#### How do I keep only employer-site apply links?

Turn on `excludeEasyApply`. Jobs that apply on LinkedIn are dropped and never charged. `filterEasyApply` does the opposite (Easy Apply only).

#### Can I scrape private or logged-in-only postings?

No. Anything behind the LinkedIn login wall (Sales Navigator, Recruiter, private postings) is out of scope.

### Support

Found a bug or have a feature request? Open an issue on the actor's [Issues tab](https://apify.com/memo23/linkedin-jobs-scraper/issues) or email me at muhameddidovic@gmail.com.

### Additional Services

Need a custom export shape, an extra field, or a managed daily feed delivered to your database? I do tailored work — drop me a line at muhameddidovic@gmail.com.

### Explore More Scrapers

If you found this useful, you might also like:

- [**LinkedIn Posts Scraper**](https://apify.com/memo23/linkedin-posts-scraper) — posts from profiles and company pages, no cookies
- [**LinkedIn Company Employees Scraper**](https://apify.com/memo23/linkedin-company-people-scraper) — people behind any company page
- [**LinkedIn Ads Scraper**](https://apify.com/memo23/linkedin-ads-scraper) — ad library search and details
- [**LinkedIn Profile Scraper**](https://apify.com/memo23/linkedin-profile-scraper) — public profile data at $1/1k

Full list at [apify.com/memo23](https://apify.com/memo23).

***

### 🤖 For AI Agents & LLM Apps

Compact reference for AI agents calling this actor via the [Apify MCP server](https://mcp.apify.com) or the Apify API (actor: `memo23/linkedin-jobs-scraper`).

**Purpose:** Turn LinkedIn Jobs into structured rows (title, company, salary, full description, apply URL, recruiter contacts) with no login or cookies; every input mode yields the same ~40-field job schema.

**Minimal input:**

```json
{
  "keywords": ["Data Engineer"],
  "location": "United States",
  "maxItems": 25
}
```

URL mode instead: `"startUrls": [{ "url": "https://www.linkedin.com/jobs/search?keywords=Software%20Engineer&location=United%20States" }]`. Monitoring mode: `"jobIds": ["4410745146"]` (each row carries isExpired / verifiedAt / verificationStatus).

**Output:** one row per job — id, title, company, location, postedAt, jobUrl, description, descriptionHtml, seniorityLevel, employmentType, jobFunction, industries, benefits, salary {raw, minAmount, maxAmount, currency, period}, applyUrl, easyApply, contactEmails\[], numberOfApplicants, applicantsCount, hiringTeam\[] {name, title, linkedinUrl, photo}, jobPosterName, jobPosterTitle, jobPosterProfileUrl, companyLinkedinUrl, yearsOfExperience\[], visaSponsorship, dynamicFilterMatch, isExpired, verifiedAt, verificationStatus, scrapedFrom, sourceUrl; company\* firmographics only with scrapeCompanyDetails; profileMatchScore + profileMatch only with resumeKeywords.

**Behaviors an agent should know:**

- `maxItems` defaults to 1000 — always set it; a search can approach LinkedIn's ~1000-result ceiling.
- A pasted search URL in `startUrls` runs as pasted — keyword/result/dynamic filters are ignored (only onlyNewJobs, skipJobId, resumeKeywords, scrapeCompanyDetails still apply). With `autoConvertToAiSearch` (default on), classic `f_E` / `f_JT` / `f_WT` / salary / sort params are appended to keywords because LinkedIn AI search dropped those facets.
- Free result filters (titleMustInclude, titleExclude, excludeCompanies, subLocationExclude, requireSalaryInfo, skipJobId, excludeEasyApply) drop jobs before billing; 🔎 dynamic filters only flag `dynamicFilterMatch: false` (the job is still fetched and charged).
- Break the ~1000 cap with `autoSplit` (by experience) or `splitByLocation` + `splitCountry` (by city); `onlyNewJobs` dedups across runs via the key-value store.
- Billing is per job delivered ($0.69/1k free plan, down to $0.25/1k on higher plans); filtered, duplicate, and previously-seen jobs cost nothing; `scrapeCompanyDetails` (company firmographics) has no extra charge.
- Login-gated data (Sales Navigator, Recruiter, private postings) is out of scope; recruiter cards, salary, and applyUrl are best-effort (empty when LinkedIn hides them from anonymous viewers).

***

### ⚠️ Disclaimer

This Actor is an independent tool and is not affiliated with, endorsed by, or sponsored by LinkedIn Corporation or any of its subsidiaries. All trademarks mentioned are the property of their respective owners.

The scraper accesses only publicly available LinkedIn Jobs pages — no authenticated endpoints, paid features, or content behind the linkedin.com login wall. Users are responsible for ensuring their use complies with LinkedIn's Terms of Service, applicable data-protection law (GDPR, CCPA, etc.), and any contractual obligations of their own organization.

***

### SEO Keywords

linkedin jobs scraper, scrape linkedin jobs, linkedin job scraper, linkedin jobs API, linkedin job postings scraper, Apify linkedin jobs, job listings scraper, job board scraper, hiring data scraper, recruiter contact scraper, job monitoring tool, job posting verification, linkedin jobs without login, no-cookie linkedin scraper, job market analysis data, salary benchmark data, recruitment lead generation, hiring manager contacts, employment data extraction, HR tech data, talent sourcing data

# Actor input Schema

## `startUrls` (type: `array`):

Paste any mix of LinkedIn Jobs URLs. Examples: search URL <code>https://www.linkedin.com/jobs/search?keywords=…</code>, single-job URL <code>https://www.linkedin.com/jobs/view/4410745146/</code>, or slug-with-id form <code>https://www.linkedin.com/jobs/view/mobile-engineer-at-jll-4410745146</code>. Search URLs extract filters (keywords, location, geoId, f_PP, workplace type, experience, contract type) from the URL itself. View URLs go straight to the detail page — no search step.

## `autoConvertToAiSearch` (type: `boolean`):

LinkedIn's AI job search dropped classic URL filters (experience, job type, workplace, salary, sort). When this is on, those filters in pasted search URLs and the matching fields below are turned into plain-language terms and appended to keywords. Date posted (<code>f_TPR</code>), company (<code>f_C</code>), Easy Apply (<code>f_AL</code>), and under 10 applicants (<code>f_EA</code>) stay as URL filters. Best-effort: LinkedIn does not apply the converted phrases as real facets. Turn off to send the original <code>f\_\*</code> params unchanged.

## `jobIds` (type: `array`):

Scrape (or re-verify) exact LinkedIn job listings — one entry per line. Accepts raw job IDs (<code>4410745146</code>), view URLs (<code>https://www.linkedin.com/jobs/view/4410745146/</code>), or any LinkedIn URL carrying <code>currentJobId=…</code>. Each entry goes straight to the job detail page — no search phase. Re-run the same list on a schedule to monitor listings: every row includes <code>isExpired</code>, <code>verifiedAt</code>, and <code>verificationStatus</code> (<code>active</code> / <code>closed</code> / <code>removed</code>), so dead listings are flagged instead of silently missing.

## `skipJobId` (type: `array`):

Do not collect these listings — raw IDs or <code>/jobs/view/</code> URLs. Use this when piping a previous dataset back in so you do not rebill jobs you already have. Distinct from 'only new jobs', which remembers IDs across runs automatically. Direct Job IDs / view URLs you paste above are always returned. Dropped search hits are never charged.

## `naturalLanguageQuery` (type: `string`):

Plain-English brief — e.g. <code>remote senior backend engineers in Germany posted this week</code>. An AI model maps it to keywords, location, remote/experience/contract, and date range, filling only the fields you leave blank. Leave empty to set filters yourself.

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

Keywords or job titles to search for in the job listings.

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

City, region or country in plain words — <code>London</code>, <code>New York</code>, <code>Germany</code>. The actor looks it up on LinkedIn and fills <code>geoId</code> for you. Set Geo ID below only if you already have one (it wins over lookup for a single location).

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

Search several locations in one run. Each name is looked up to a LinkedIn geoId and combined with every keyword above. Example: <code>London</code>, <code>Berlin</code>, <code>Remote</code> (<code>Remote</code> is left as text — use the workplace filter for remote jobs). When set, this takes precedence over the single Broad location field. Ignored when Start URLs are provided.

## `geoId` (type: `string`):

Optional override. LinkedIn geoId for a <strong>single</strong> location (e.g. <code>103644278</code> United States). Leave empty to auto-resolve from the location name. Ignored when you search several locations or split by city — those are looked up individually. Place filter IDs below still apply on top.

## `placeIds` (type: `array`):

Optional LinkedIn place filter IDs (`f_PP`) for city-level or sub-location filtering. Example: `104555257` for Santiago. Use one or more IDs copied from a LinkedIn Jobs URL.

## `timeRange` (type: `string`):

How recently the job was posted. LinkedIn's own <code>f_TPR</code> filter. Overridden by 'Posted within hours' when that is set.

## `postedWithinHours` (type: `integer`):

Custom freshness window in hours, overriding Time range. LinkedIn's dropdown only offers 24 hours / 1 week / 1 month — this accepts any window, so you can poll for jobs posted in the last hour (<code>1</code>) or the last 3 days (<code>72</code>). Keyword-search only — a pasted search URL runs verbatim (put <code>f_TPR=r3600</code> in the URL instead).

## `jobTypes` (type: `array`):

Select the types of work arrangements to include in the search.

## `remote` (type: `array`):

Select the workplace type filters to include in the search. LinkedIn AI search treats workplace as a soft keyword; after each job is fetched, rows whose workplace type does not match are dropped and never charged. Jobs with no labeled type are kept.

## `experienceLevels` (type: `array`):

Select the experience levels to include in the search.

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

Alias for Experience Levels; select one or more experience codes to include.

## `contractType` (type: `array`):

Select contract types to include in the search. LinkedIn AI search treats job type as a soft keyword; after each job is fetched, rows whose employment type does not match are dropped and never charged. Jobs with no labeled type are kept.

## `companyNames` (type: `array`):

Only return jobs from these companies. Each name is resolved to its LinkedIn company ID and filtered server-side (exact); if a name can't be resolved it falls back to name matching. <strong>Ignored when a search URL is provided</strong> — that URL defines the search.

## `companyIds` (type: `array`):

Only return jobs from these companies by raw LinkedIn company ID — the numeric <code>f_C</code> value (e.g. <code>1035</code> Microsoft, <code>1441</code> Google). Used verbatim server-side, skipping the name→ID lookup that Company names pays for. Find the ID in a company's LinkedIn URL or its jobs-search <code>f_C=</code> param. Combine with or use instead of Company names. Ignored when a search URL is provided.

## `filterUnder10Applicants` (type: `boolean`):

Only return jobs with fewer than 10 applicants (LinkedIn's own 'early applicant' filter, <code>f_EA=true</code>). The best odds of a reply — apply before the crowd arrives. Keyword-search only — a pasted search URL runs verbatim (add <code>f_EA=true</code> to the URL instead).

## `filterEasyApply` (type: `boolean`):

Only return jobs that support LinkedIn Easy Apply (<code>f_AL=true</code>) — apply directly on LinkedIn without leaving the site. Keyword-search only — a pasted search URL runs verbatim (add <code>f_AL=true</code> to the URL instead).

## `excludeEasyApply` (type: `boolean`):

Drop jobs that apply on LinkedIn (Easy Apply) and keep only those that link out to an employer careers page. LinkedIn has no inverse of the Easy Apply search filter, so this is applied after the apply URL is known — dropped jobs are never charged. Ignored when 'Easy Apply only' is on. Keyword-search only.

## `salaryBase` (type: `string`):

Only return jobs at or above this base salary, using LinkedIn's own salary filter (<code>f_SB2</code>). LinkedIn applies it where salary data exists — mostly US searches.

## `distance` (type: `string`):

Search radius in miles around the location, using LinkedIn's own <code>distance</code> filter. Applies to keyword searches; for a pasted search URL the URL's own <code>distance</code> value wins.

## `titleMustInclude` (type: `array`):

Keep only jobs whose title contains at least ONE of these terms (case-insensitive). Example: <code>senior</code>, <code>staff</code> keeps "Senior Backend Engineer" and "Staff Engineer" but drops "Junior Developer". Leave empty to keep all titles.

## `titleExclude` (type: `array`):

Drop jobs whose title contains ANY of these terms (case-insensitive). Example: <code>intern</code>, <code>junior</code>, <code>unpaid</code>. Applied after 'Title must include'.

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

Drop jobs from these companies (case-insensitive name match, so <code>Microsoft</code> also skips "Microsoft Corporation"). Useful for filtering out staffing agencies or your own company's postings.

## `subLocationExclude` (type: `array`):

Drop jobs whose location contains ANY of these terms (case-insensitive substring). Example: <code>Laval</code> skips every "Laval, Quebec, Canada" job. Applied BEFORE the job is fetched, so skipped jobs are never charged. Applies to search results; direct Job IDs / view URLs are always returned.

## `requireSalaryInfo` (type: `boolean`):

Only keep jobs whose posting publishes a salary. Jobs without a visible salary block are dropped for free — they never reach the dataset and are never charged. (Direct Job IDs / view URLs are always returned as requested.)

## `postedAfter` (type: `string`):

Drop jobs whose exact calendar posting date is before this day (YYYY-MM-DD). Uses <code>postedAt</code>, not a rounded "3 weeks ago". Jobs with no date are kept. Dropped jobs are never charged. (Direct Job IDs / view URLs are always returned.)

## `requirePoster` (type: `boolean`):

Drop jobs that do not name the person who posted them, so every kept row has a human to contact. Dropped jobs are never charged. Distinct from 'Recruiter profile required' below, which only flags <code>dynamicFilterMatch</code> and still saves the row. (Direct Job IDs / view URLs are always returned.)

## `descriptionInclude` (type: `array`):

Keep only jobs whose <strong>description body</strong> contains at least ONE of these terms (case-insensitive substring). Example: <code>Kubernetes</code>, <code>Terraform</code>. Applied once the description is available, so non-matching jobs are dropped and never charged. (Direct Job IDs / view URLs are always returned.)

## `descriptionExclude` (type: `array`):

Drop jobs whose <strong>description body</strong> contains ANY of these terms (case-insensitive). Example: <code>security clearance</code>, <code>unpaid</code>. Dropped jobs are never charged. (Direct Job IDs / view URLs are always returned.)

## `jobFunctionInclude` (type: `array`):

Keep (dynamicFilterMatch:true) only jobs whose LinkedIn job function is one of these. Takes priority over Exclude when both are set.

## `jobFunctionExclude` (type: `array`):

Flag jobs whose LinkedIn job function is any of these as <code>dynamicFilterMatch: false</code>. Ignored when Include job functions is set.

## `jobIndustryInclude` (type: `array`):

Keep (dynamicFilterMatch:true) only jobs whose industry matches ONE of these terms (case-insensitive). Free text — use LinkedIn industry names, e.g. <code>Software Development</code>, <code>Staffing and Recruiting</code>. Takes priority over Exclude industries when both are set.

## `jobIndustryExclude` (type: `array`):

Flag jobs whose industry matches ANY of these terms as <code>dynamicFilterMatch: false</code> (case-insensitive). Ignored when Include industries is set. Example: <code>Staffing and Recruiting</code>.

## `excludeRecruitingAgencies` (type: `boolean`):

Flag jobs posted by staffing and recruiting agencies as <code>dynamicFilterMatch: false</code> (matched on company industry). Automatically enables company enrichment.

## `companySizeMin` (type: `integer`):

Flag jobs from companies with FEWER employees than this as <code>dynamicFilterMatch: false</code>. Companies with no employee count on LinkedIn always pass. Automatically enables company enrichment.

## `companySizeMax` (type: `integer`):

Flag jobs from companies with MORE employees than this as <code>dynamicFilterMatch: false</code>. Companies with no employee count on LinkedIn always pass. Automatically enables company enrichment.

## `companyOrganizationTypeInclude` (type: `array`):

Keep (dynamicFilterMatch:true) only jobs whose company organization type is one of these. Companies with no type available always pass. Takes priority over Exclude when both are set. Automatically enables company enrichment.

## `companyOrganizationTypeExclude` (type: `array`):

Flag jobs whose company organization type is any of these as <code>dynamicFilterMatch: false</code>. Companies with no type available always pass. Ignored when Include is set. Automatically enables company enrichment.

## `companyFoundedDateMin` (type: `integer`):

Keep (dynamicFilterMatch:true) only jobs from companies founded in or after this year. Companies with no founding year always pass. Automatically enables company enrichment.

## `companyFoundedDateMax` (type: `integer`):

Keep (dynamicFilterMatch:true) only jobs from companies founded in or before this year. Companies with no founding year always pass. Automatically enables company enrichment.

## `companyFollowersCountMin` (type: `integer`):

Keep (dynamicFilterMatch:true) only jobs from companies with at least this many LinkedIn followers. Companies with no follower count always pass. Automatically enables company enrichment.

## `companyFollowersCountMax` (type: `integer`):

Keep (dynamicFilterMatch:true) only jobs from companies with at most this many LinkedIn followers. Companies with no follower count always pass. Automatically enables company enrichment.

## `requireRecruiterProfile` (type: `boolean`):

Flag jobs that have NO recruiter/hiring-team profile on the posting as <code>dynamicFilterMatch: false</code>.

## `resumeKeywords` (type: `array`):

⚠️ <strong>This is NOT a search filter</strong> — it does not change which jobs LinkedIn returns. Each job in the output is scored against these skills: <code>profileMatchScore</code> (0–100 = % of your keywords found in the job title + description), plus <code>profileMatch.matchedKeywords</code> / <code>profileMatch.missingKeywords</code>. Entries are plain strings (<code>"TypeScript"</code>) or objects with aliases so abbreviations count as the same skill: <code>{ "keyword": "JavaScript", "aliases": \["JS"] }</code>. Matching is case-insensitive on whole words — <code>Java</code> will not match <code>JavaScript</code>.

## `scrapeCompanyDetails` (type: `boolean`):

Enrich each row with company website, industry, size, headquarters, logo, description, and follower count (best-effort, from the public company page) — surfaced as flat <code>company\*</code> fields plus a full <code>companyDetails</code> object. Adds one request per <strong>unique</strong> company (cached within a run). No extra charge: you pay only the per-result price. Off by default because the extra page per company makes runs slower.

## `scrapeJobDetails` (type: `boolean`):

On (default): fetch the job page for description, salary, seniority, employment type, industry, applicant count, apply URL and the named recruiter. Off: only the fields on the search card (title, company, location, URL, posted date) — much faster and cheaper, useful for monitoring which jobs exist. Filters that need the description, salary, or recruiter force this back on. Direct Job IDs / view URLs always fetch the detail page.

## `includeDescriptionHtml` (type: `boolean`):

Adds <code>descriptionHtml</code> alongside the plain-text description, preserving the employer's formatting. Off keeps the dataset noticeably smaller.

## `compatibleOutput` (type: `boolean`):

Adds the field names other LinkedIn job actors use (<code>jobTitle</code>, <code>companyName</code>, <code>salaryInfo</code>, <code>applicationsCount</code>, <code>applyType</code>, <code>keywordMatchScorePercentage</code>…) next to ours, so an existing n8n / Make / Zapier workflow keeps working after you switch the actor ID. Turns on automatically when your input uses those actors' field names (<code>keyword</code>, <code>workType</code>, <code>publishedAt</code>…). Our own field names are always kept.

## `aiEnrich` (type: `boolean`):

Run every returned job through an AI model that reads the posting and adds a normalized <code>aiEnrichment</code> block: <code>visaSponsorship</code>, <code>jobCategory</code>, <code>language</code> (ISO code), <code>seniority</code>, and <code>remoteType</code> — fields that can't be derived reliably by rules. <strong>Billed as a separate per-job event</strong> on top of the per-result price (one AI call per job). Off by default — enable only when you need these normalized fields. The free rule-based <code>visaSponsorship</code> field is always included regardless of this toggle.

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

Remember job IDs across runs and skip any job already returned in a previous run — you only ever receive (and pay for) NEW jobs. Ideal for scheduled daily job-alert runs. Stored in the actor's key-value store.

## `autoSplit` (type: `boolean`):

LinkedIn's guest search caps each query at ~1000 results. When enabled (and no experience level is pinned), the search is split across all six experience levels and the deduplicated results are merged, returning well beyond 1000 jobs per search. This and 'Split by city' are alternative cap-busting strategies — if both are on, 'Split by city' takes precedence (it yields more unique jobs per request).

## `splitByLocation` (type: `boolean`):

LinkedIn caps each search at ~1000 results. When enabled, the search fans out across the major cities of the chosen Split country and merges the deduplicated results — typically returning many times more jobs than a single country query. Raise Max items to capture them, since each city is a separate search. Takes precedence over 'Split by experience' when both are on. Ignored when Start URLs are provided.

## `splitCountry` (type: `string`):

Country whose major cities are fanned out when 'Split by city' is enabled. Each city is searched as <code>City, Country</code> (Warsaw, Poland, not Warsaw, Indiana). Leave empty to follow Broad location (Germany → German cities). A leftover United States value does not override a non-US location. Does not overwrite a pasted search URL.

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

Maximum number of items that will be scraped.

## `maxItemsPerSearch` (type: `integer`):

Cap each individual keyword × location search so one broad query cannot consume the whole run. LinkedIn's public search will not return more than ~1,000 jobs for any single query — to go past that, enable Split by city / Split by experience. Leave empty for no per-search cap (only Max items applies).

## `minDelay` (type: `integer`):

Minimum pause, in seconds, before fetching the next page of search results. On the default proxy setting the pause stays at 1-2 s whatever you set here; a longer pause only matters if you bring your own fixed proxies.

## `maxDelay` (type: `integer`):

Maximum pause, in seconds, before fetching the next page of search results. On the default proxy setting it stays capped at 2 s.

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

Maximum number of pages that can be processed at the same time.

## `minConcurrency` (type: `integer`):

Minimum number of pages that will be processed at the same time.

## `maxRequestRetries` (type: `integer`):

Number of times the crawler will retry a failed request before giving up.

## `proxy` (type: `object`):

Leave empty — the actor already routes all traffic through its own built-in residential proxy at no extra cost to you. Only set this if you want to use your own proxies.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.linkedin.com/jobs/search?keywords=Software%20Engineer&location=United%20States&geoId=103644278"
    }
  ],
  "autoConvertToAiSearch": true,
  "jobIds": [],
  "skipJobId": [],
  "keywords": [
    "Software Engineer"
  ],
  "location": "United States",
  "locations": [
    "United States",
    "United Kingdom"
  ],
  "placeIds": [
    "104555257"
  ],
  "timeRange": "any time",
  "jobTypes": [
    "1",
    "3"
  ],
  "remote": [
    "2"
  ],
  "experienceLevels": [
    "4",
    "5",
    "6"
  ],
  "experienceLevel": [
    "5"
  ],
  "contractType": [
    "F"
  ],
  "companyNames": [],
  "companyIds": [],
  "filterUnder10Applicants": false,
  "filterEasyApply": false,
  "excludeEasyApply": false,
  "salaryBase": "",
  "distance": "",
  "titleMustInclude": [],
  "titleExclude": [],
  "excludeCompanies": [],
  "subLocationExclude": [],
  "requireSalaryInfo": false,
  "requirePoster": false,
  "descriptionInclude": [],
  "descriptionExclude": [],
  "jobFunctionInclude": [],
  "jobFunctionExclude": [],
  "jobIndustryInclude": [],
  "jobIndustryExclude": [],
  "excludeRecruitingAgencies": false,
  "companyOrganizationTypeInclude": [],
  "companyOrganizationTypeExclude": [],
  "requireRecruiterProfile": false,
  "resumeKeywords": [
    {
      "keyword": "TypeScript",
      "aliases": [
        "TS"
      ]
    },
    {
      "keyword": "React"
    },
    {
      "keyword": "Node.js",
      "aliases": [
        "NodeJS",
        "Node"
      ]
    }
  ],
  "scrapeCompanyDetails": false,
  "scrapeJobDetails": true,
  "includeDescriptionHtml": true,
  "compatibleOutput": false,
  "aiEnrich": false,
  "onlyNewJobs": false,
  "autoSplit": false,
  "splitByLocation": false,
  "splitCountry": "",
  "maxItems": 10,
  "minDelay": 1,
  "maxDelay": 2,
  "maxConcurrency": 2,
  "minConcurrency": 1,
  "maxRequestRetries": 8
}
```

# Actor output Schema

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

Each row carries the job itself (title, company, location, workplace type, seniority, employment type, salary, posted date, applicant count, apply URL), the poster or hiring contact where LinkedIn shows one, and optional company details and a 0-100 profile-match score.

# 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 = {
    "startUrls": [
        {
            "url": "https://www.linkedin.com/jobs/search?keywords=Software%20Engineer&location=United%20States&geoId=103644278"
        }
    ],
    "keywords": [
        "Software Engineer"
    ],
    "location": "United States",
    "locations": [
        "United States",
        "United Kingdom"
    ],
    "placeIds": [
        "104555257"
    ],
    "jobTypes": [
        "1",
        "3"
    ],
    "remote": [
        "2"
    ],
    "experienceLevels": [
        "4",
        "5",
        "6"
    ],
    "experienceLevel": [
        "5"
    ],
    "contractType": [
        "F"
    ],
    "resumeKeywords": [
        {
            "keyword": "TypeScript",
            "aliases": [
                "TS"
            ]
        },
        {
            "keyword": "React"
        },
        {
            "keyword": "Node.js",
            "aliases": [
                "NodeJS",
                "Node"
            ]
        }
    ],
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("memo23/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 = {
    "startUrls": [{ "url": "https://www.linkedin.com/jobs/search?keywords=Software%20Engineer&location=United%20States&geoId=103644278" }],
    "keywords": ["Software Engineer"],
    "location": "United States",
    "locations": [
        "United States",
        "United Kingdom",
    ],
    "placeIds": ["104555257"],
    "jobTypes": [
        "1",
        "3",
    ],
    "remote": ["2"],
    "experienceLevels": [
        "4",
        "5",
        "6",
    ],
    "experienceLevel": ["5"],
    "contractType": ["F"],
    "resumeKeywords": [
        {
            "keyword": "TypeScript",
            "aliases": ["TS"],
        },
        { "keyword": "React" },
        {
            "keyword": "Node.js",
            "aliases": [
                "NodeJS",
                "Node",
            ],
        },
    ],
    "maxItems": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("memo23/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 '{
  "startUrls": [
    {
      "url": "https://www.linkedin.com/jobs/search?keywords=Software%20Engineer&location=United%20States&geoId=103644278"
    }
  ],
  "keywords": [
    "Software Engineer"
  ],
  "location": "United States",
  "locations": [
    "United States",
    "United Kingdom"
  ],
  "placeIds": [
    "104555257"
  ],
  "jobTypes": [
    "1",
    "3"
  ],
  "remote": [
    "2"
  ],
  "experienceLevels": [
    "4",
    "5",
    "6"
  ],
  "experienceLevel": [
    "5"
  ],
  "contractType": [
    "F"
  ],
  "resumeKeywords": [
    {
      "keyword": "TypeScript",
      "aliases": [
        "TS"
      ]
    },
    {
      "keyword": "React"
    },
    {
      "keyword": "Node.js",
      "aliases": [
        "NodeJS",
        "Node"
      ]
    }
  ],
  "maxItems": 10
}' |
apify call memo23/linkedin-jobs-scraper --silent --output-dataset

```

## MCP server setup

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