# LinkedIn Jobs Scraper (No Cookies) (`zkill3r/linkedin-jobs-scraper-no-cookies`) Actor

Scrape public LinkedIn job postings by keyword and location without login or cookies: title, company, location, posting date, salary, plus optional full details (description, seniority, employment type, applicants, external apply link, recruiter).

- **URL**: https://apify.com/zkill3r/linkedin-jobs-scraper-no-cookies.md
- **Developed by:** [Zee Lex](https://apify.com/zkill3r) (community)
- **Categories:** Jobs
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.25 / 1,000 jobs

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 (No Cookies)

Scrape **public LinkedIn job postings** by keyword and location, **with no login, no cookies and no LinkedIn account**. The scraper reads the same public "guest" job pages LinkedIn shows to signed-out visitors, over plain HTTP with no browser, so it is fast and cheap.

For every job you get the title, company with its link and logo, location, posting date, salary (when shown) and the job link. You can optionally add the **full job details**: description (text and HTML), seniority level, employment type, job function, industries, **applicant count**, the **external apply link**, the recruiter and the salary range as numbers.

### Use cases

- **Job boards and aggregators:** keep a niche job board filled with fresh postings by keyword and location.
- **Recruiting and sourcing:** watch which companies are hiring for which roles, and how many people applied.
- **Lead generation:** companies posting jobs are spending money right now. Get the company names, LinkedIn pages, recruiters and career-site apply links.
- **Market research:** track demand for skills, salary ranges and remote/hybrid share over time.

### Input example

```json
{
  "keywords": ["data engineer", "analytics engineer"],
  "location": "United Kingdom",
  "maxItems": 100,
  "datePosted": "past_week",
  "jobType": ["full_time", "contract"],
  "experienceLevel": ["mid_senior"],
  "workplaceType": ["remote", "hybrid"],
  "easyApplyOnly": false,
  "sortBy": "recent",
  "includeDetails": true
}
```

Or paste searches from your browser, filters included:

```json
{ "startUrls": ["https://www.linkedin.com/jobs/search/?keywords=react%20developer&location=Germany&f_WT=2&f_TPR=r604800"] }
```

An empty input (`{}`) searches "software engineer" in the United States and saves 50 jobs. It finishes in seconds.

### Output example

```json
{
  "type": "job",
  "jobId": "4156770518",
  "title": "Senior Software Engineer",
  "companyName": "Example Co",
  "companyUrl": "https://www.linkedin.com/company/example-co",
  "companyLogo": "https://media.licdn.com/dms/image/v2/…/company-logo_100_100/0/1?…",
  "location": "San Francisco, CA",
  "postedAt": "2026-10-27",
  "postedTimeAgo": "2 days ago",
  "salary": "$117,000.00/yr - $234,000.00/yr",
  "benefits": "Actively Hiring",
  "isEasyApply": false,
  "url": "https://www.linkedin.com/jobs/view/4156770518",
  "keyword": "software engineer",
  "searchLocation": "United States",
  "scrapedAt": "2026-10-29T07:56:46.080Z",

  "descriptionText": "About the role\n\nWe are hiring a Senior Software Engineer to …\n• 5+ years of TypeScript\n• Experience with distributed systems",
  "descriptionHtml": "<strong>About the role</strong><br><br>We are hiring …",
  "seniorityLevel": "Mid-Senior level",
  "employmentType": "Full-time",
  "jobFunction": "Engineering and Information Technology",
  "industries": "Software Development",
  "applicantsCount": 200,
  "applicantsText": "Over 200 applicants",
  "applyUrl": "https://careers.example.com/jobs/4156770518?source=linkedin",
  "recruiterName": "Jordan Example",
  "recruiterTitle": "Technical Recruiter at Example Co",
  "salaryMin": 117000,
  "salaryMax": 234000,
  "salaryCurrency": "USD",
  "salaryPeriod": "year"
}
```

All values above are invented. The fields from `descriptionText` down appear only with `includeDetails: true`.

Details on some fields:

- **`postedAt`** is the posting date LinkedIn gives (`YYYY-MM-DD`).
- **`applicantsCount`** is a number:
  - "Over 200 applicants" gives `200`.
  - "Be among the first 25 applicants" gives `0` and keeps the original text in `applicantsText`.
- **`isEasyApply`** is `true` for LinkedIn Easy Apply jobs and `false` for jobs that apply on the company's own site.
- **`applyUrl`**: LinkedIn hides the external application link from logged-out visitors, so this is usually `null`. Use `url` to open the job.
- **`recruiterName` / `recruiterTitle`** are filled only when LinkedIn shows the "Meet the hiring team" block publicly (roughly 10–15% of jobs).
- **`salary`** is filled only when the employer publishes it; `salaryMin`/`salaryMax`/`salaryCurrency`/`salaryPeriod` are parsed from it (e.g. `SGD 4,000.00/mo - SGD 5,200.00/mo`).
- **`detailsError`** appears when the details could not be read, for example when a job was removed. The job is still saved with its card data and charged as a `job` only.

The run's **summary** is in the key-value store under `OUTPUT`:

```json
{ "keywords": ["software engineer"], "location": "United States", "jobsScraped": 50, "detailsFetched": 0, "blockedRequests": 0, "usedResidential": false, "perSearch": [ … ] }
```

### Pricing

Pay per event:

- **`job`** is charged once per job saved.
- **`job-details`** is charged on top for each job saved with its full details (`includeDetails`).

The run stops cleanly when your maximum cost per run is reached.

| Plan | `job` per 1,000 | `job-details` per 1,000 (extra) |
|---|---|---|
| Free | $0.35 | $0.55 |
| Bronze | $0.32 | $0.50 |
| Silver | $0.28 | $0.45 |
| Gold+ | $0.25 | $0.40 |

### How it works

1. **Search pages.** For each keyword (or start URL) the scraper reads LinkedIn's public guest search results, about 10 jobs per page, and pages on until it has `maxItems` jobs or a page comes back empty. LinkedIn stops after about 1,000 results per search.
2. **Details** (optional). For each job it reads the public job page.
3. **Duplicates** are removed by job ID across all keywords.
4. **Blocking:**
   - A 429, a 999 (LinkedIn's block code), a 401/403, a redirect to the sign-in wall or an empty response counts as blocked. The scraper drops that session, waits a random, growing delay and retries, up to 5 retries.
   - After **3 blocks in a row** on datacenter proxy, the rest of the run uses **RESIDENTIAL** proxy, and the summary says `usedResidential: true`.
   - Each session gets a realistic desktop browser identity and language.
5. **Run timeout.** About 45 seconds before the run's timeout, the scraper stops, saves everything it has (including jobs still waiting for details, with their card data) and exits successfully.

### FAQ

**Do I need a LinkedIn account, login or cookies?**
No. The scraper only reads pages LinkedIn shows publicly to signed-out visitors, and it never sends cookies.

**Why fewer jobs than LinkedIn's result count?**
LinkedIn shows at most about 1,000 jobs per search to signed-out visitors, and its counts are estimates. Use narrower keywords, locations or filters (date posted, job type, experience level, workplace type) to reach more of them.

**Why is the salary often empty?**
Many employers don't publish a salary. When LinkedIn shows one, it is in `salary` (and with details, as `salaryMin`, `salaryMax`, `salaryCurrency` and `salaryPeriod`).

**Can I use my own LinkedIn search?**
Yes. Paste the URL from your browser into `startUrls`. Its filters are kept. If you leave Keywords at the default, only your URLs are scraped.

**What if LinkedIn blocks the run?**
The scraper retries on new sessions and switches to residential proxy by itself. If LinkedIn still blocks every single request, the run fails with a clear message instead of finishing "successfully" with nothing. If you see many `blockedRequests` in the summary, run fewer keywords at once or use residential proxy from the start.

**Is the description complete?**
With `includeDetails`, `descriptionText` and `descriptionHtml` hold the full description LinkedIn shows to signed-out visitors.

### Development

```bash
npm install
npm test                  # unit tests on synthetic fixtures (+ live-capture tests when captured)
npm run mock-server       # offline LinkedIn guest mock on :4596 (scenarios in test/mock-server.ts)
LI_BASE_URL=http://127.0.0.1:4596 npm start    # end-to-end against the mock (input: {"proxyConfiguration":{"useApifyProxy":false}})
npm run capture-fixtures  # save real LinkedIn guest responses to test/fixtures/live/ (needs network)
npm run fixtures          # regenerate the synthetic fixtures
```

`RESIDENTIAL_PROXY_URL` (development only) replaces Apify RESIDENTIAL proxy with your own proxy URL when the run switches.

# Actor input Schema

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

Job search terms, one per line (e.g. "software engineer", "data analyst"). Each keyword is a separate search of up to "Max jobs per keyword". Jobs found by several keywords are saved once.

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

Where to search: a country, region or city as you would type it on LinkedIn ("United States", "London, England, United Kingdom", "Remote"). Leave empty for worldwide.

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

Optional. linkedin.com/jobs/search URLs copied from your browser (with your filters). Their keywords, location, geoId and f\_\* filters are used as-is; the filters below don't apply to them. When you give URLs and leave Keywords at the default ("software engineer"), only the URLs are scraped.

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

Maximum jobs saved per keyword or search URL. LinkedIn shows at most about 1,000 results per search.

## `datePosted` (type: `string`):

Only jobs posted within this period.

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

Only these job types (any of them). Empty = all.

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

Only these experience levels (any of them). Empty = all.

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

On-site, remote and/or hybrid. Empty = all.

## `easyApplyOnly` (type: `boolean`):

Only jobs you can apply to with LinkedIn Easy Apply.

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

Most recent first, or LinkedIn's relevance order.

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

Also open each job for its full description, seniority level, employment type, job function, industries, applicant count, external apply link, recruiter and salary. One extra request per job, charged as an extra "job-details" event.

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

Apify datacenter proxy by default. Blocked requests (429, 999, sign-in wall) are retried on a fresh session; after 3 blocks in a row the rest of the run switches to RESIDENTIAL proxy automatically (if your plan includes it).

## Actor input object example

```json
{
  "keywords": [
    "software engineer"
  ],
  "location": "United States",
  "startUrls": [],
  "maxItems": 20,
  "datePosted": "any",
  "jobType": [],
  "experienceLevel": [],
  "workplaceType": [],
  "easyApplyOnly": false,
  "sortBy": "recent",
  "includeDetails": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

One item per job posting.

## `allFields` (type: `string`):

Every field of every job, including details when they were fetched.

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

keywords, location, jobsScraped, detailsFetched, blockedRequests, usedResidential, plus per-search pages and stop reasons.

# 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 = {
    "keywords": [
        "software engineer"
    ],
    "location": "United States",
    "startUrls": [],
    "maxItems": 20,
    "datePosted": "any",
    "jobType": [],
    "experienceLevel": [],
    "workplaceType": [],
    "easyApplyOnly": false,
    "sortBy": "recent",
    "includeDetails": false,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("zkill3r/linkedin-jobs-scraper-no-cookies").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 = {
    "keywords": ["software engineer"],
    "location": "United States",
    "startUrls": [],
    "maxItems": 20,
    "datePosted": "any",
    "jobType": [],
    "experienceLevel": [],
    "workplaceType": [],
    "easyApplyOnly": False,
    "sortBy": "recent",
    "includeDetails": False,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("zkill3r/linkedin-jobs-scraper-no-cookies").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 '{
  "keywords": [
    "software engineer"
  ],
  "location": "United States",
  "startUrls": [],
  "maxItems": 20,
  "datePosted": "any",
  "jobType": [],
  "experienceLevel": [],
  "workplaceType": [],
  "easyApplyOnly": false,
  "sortBy": "recent",
  "includeDetails": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call zkill3r/linkedin-jobs-scraper-no-cookies --silent --output-dataset

```

## MCP server setup

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

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/FX2gX5aG427NRaigO/builds/ywO8w7CzKm5i6or6m/openapi.json
