# LinkedIn Jobs Scraper - No Cookies, Fast & Cheap (`andrew_babo/linkedin-jobs-scraper`) Actor

Scrape LinkedIn job postings without a login or cookies: title, company, location, salary, posted date, full description, seniority, employment type, industries and apply links. HTTP-first engine, no browser, from $0.30 per 1,000 jobs. Export CSV, Excel, JSON.

- **URL**: https://apify.com/andrew\_babo/linkedin-jobs-scraper.md
- **Developed by:** [Andrew Babo](https://apify.com/andrew_babo) (community)
- **Categories:** Jobs, Lead generation, Business
- **Stats:** 19 total users, 5 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## 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, Fast & Cheap

Scrape LinkedIn job postings **without a login, account or `li_at` cookie**. The actor talks to LinkedIn's public guest job surface over plain HTTP — no browser, no Playwright — so it is fast, cheap and easy to run at scale.

### What you get

Every job record contains:

| Field | Notes |
|---|---|
| `jobId`, `link` | Canonical LinkedIn job URL |
| `title`, `companyName`, `companyLinkedinUrl`, `companyLogo` | From the search card |
| `location`, `postedAt`, `postedAtText` | ISO date + original relative text |
| `descriptionText`, `descriptionHtml` | Full posting body (detail mode) |
| `seniorityLevel`, `employmentType`, `jobFunction`, `industries` | LinkedIn job criteria |
| `applyUrl`, `applyMethod` | Direct apply target, incl. off-site employer URLs |
| `applicantsCount`, `salaryInfo` | When LinkedIn exposes them publicly |
| `query` | The search term / location that produced the row |

### Two modes, two prices

- **List only** (`scrapeJobDetails: false`) — one request returns 10 job cards, so a thousand jobs cost a fraction of a cent of compute. Best for monitoring, dedup pipelines and building a job-ID universe.
- **Full detail** (`scrapeJobDetails: true`, default) — one extra request per job adds description, criteria, apply URL and salary.

### Measured head-to-head (real runs, not estimates)

`data analyst` in Singapore, 20 jobs, sequential runs on the same day:

| | This actor | curious\_coder/linkedin-jobs-scraper |
|---|---|---|
| Time | **24 s** | 69 s |
| Cost per 1,000 jobs | **$0.38** | $2.00 |
| Coverage: title, company, location, posted date, full description, seniority, employment type, function, industries, logo, applicants | 100% | 100% |
| Blocked requests | 0 | 0 |

**2.8x faster and about 5x cheaper** for the same fields. Fields LinkedIn hides from logged-out visitors (employer apply URL, salary, job poster name) are unavailable to both.

### Input

```json
{
  "searchTerms": ["software engineer"],
  "location": "Singapore",
  "datePosted": "pastWeek",
  "workplaceType": ["2"],
  "maxItems": 500,
  "scrapeJobDetails": true,
  "speedMode": "turbo"
}
```

You can also pass `startUrls` (any LinkedIn jobs search URL), `geoId`, `distance`, `experienceLevel`, `jobType`, `companyIds`, `easyApplyOnly` and `sortBy`.

### Honest limits without a cookie

LinkedIn's public guest pages simply do not contain these, so no cookie-free scraper can return them:

- the **name of the job poster / hiring manager**
- the **exact applicant count** (guest pages show buckets such as "Over 200 applicants")
- **LinkedIn salary insights** (only salary written into the posting itself is returned)
- each query is capped by LinkedIn at **1,000 results** — split by city, date range or industry to go beyond it

`liAtCookie` is an optional input. It is never required, and we recommend leaving it empty.

### Proxy

Use Apify **residential** proxies for best results. LinkedIn rate-limits per IP; the actor runs many slow, warmed-up sessions in parallel instead of one fast one, and rotates any IP that gets a `999`.

### Output

Export to CSV, Excel, JSON or JSONL, or pull the dataset via API. Run statistics — pages fetched, blocked requests, per-field coverage — are stored in the key-value store under `STATS`.

### Company details on every job (new)

Set `scrapeCompanyDetails: true` and each job also carries its employer's real data:
`companyWebsite`, `companyEmployeesCount`, `companyEmployeeCountRange`, `companyIndustry`,
`companyAddress`, `companyCountry`, `companyDescription`, `companyFoundedOn`.

Employers repeat across a search, so each company page is fetched **once per unique employer**
and cached for 30 days — 500 jobs from 80 employers costs 80 extra requests, not 500.
The deduplicated employer list (with websites) is written to the key-value store under
`COMPANIES`, and `pushCompanies: true` also appends it to the dataset — ready as the input
for a company-enrichment pipeline.

Measured run (25 jobs, Singapore, residential proxy): coverage `companyEmployeesCount` 100%,
`companyIndustry` 100%, `companyWebsite` 96%, `companyAddress` / `companyCountry` 96%,
0 blocked requests, ≈ $0.73 per 1,000 jobs with company enrichment on.

`datePosted` accepts both spellings: `pastWeek`/`pastMonth` and the shorter `week`/`month`.
`includeRunSummary` is now **off** by default, so the dataset contains job rows only; the
summary (counts, blocked requests, `stopReason`, `estimatedCostUsd`, `costPer1000Usd`) is
always written to `STATS`.

### For AI agents (MCP-ready)

This actor is built to be called by AI agents. It works out of the box with the
[Apify MCP Server](https://mcp.apify.com) — add it to Claude Desktop, Cursor or any MCP
client: the agent can monitor job postings on its own.

```json
{
  "mcpServers": {
    "linkedin-jobs-scraper": {
      "url": "https://mcp.apify.com/?actors=andrew_babo/linkedin-jobs-scraper",
      "headers": { "Authorization": "Bearer <YOUR_APIFY_TOKEN>" }
    }
  }
}
```

#### Agent skill (paste into your agent's instructions)

```text
Use the "linkedin-jobs-scraper" tool to collect public LinkedIn job posts
for a role, location or company: title, company, location, posted date,
seniority, employment type, description and apply URL. No cookies needed.
Good for hiring-signal research, job alerts and talent-market analysis.

HOW TO CALL
- { "searchTerms": ["software engineer"], "location": "Singapore",
    "datePosted": "past_week", "maxItems": 100 }
- Filter with "experienceLevel", "jobType", "workplaceType",
  "companyIds", "easyApplyOnly".
- Hiring signals for specific accounts: pass "companyIds" and set
  "scrapeCompanyDetails" only when company data is really needed.
- "scrapeJobDetails": false is much cheaper when the user only wants the
  listing rows.

OUTPUT CONTRACT
- One row per job posting with the fields above.
- Empty results mean no posting matched the filters in the scraped window —
  say so and suggest widening "datePosted" or terms; never invent postings,
  salaries or apply links.
```

# Actor input Schema

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

Job keywords to search, e.g. \["software engineer", "data analyst"].

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

Free-form location, e.g. "Singapore" or "London, United Kingdom". Resolved by LinkedIn server-side.

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

Run the same search terms across several locations. Also the way to break past LinkedIn's 1,000-results-per-query ceiling.

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

Optional LinkedIn numeric geo ID. Only needed when combined with a radius.

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

Search radius around the location. Requires a geo ID, which is resolved automatically for well-known places.

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

Both spellings are accepted: pastWeek/pastMonth and the shorter week/month used by other LinkedIn actors.

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

1 internship, 2 entry, 3 associate, 4 mid-senior, 5 director, 6 executive.

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

F full-time, P part-time, C contract, T temporary, I internship, V volunteer, O other.

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

1 on-site, 2 remote, 3 hybrid.

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

Restrict to specific LinkedIn company IDs (numeric, not slugs).

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

Easy Apply only.

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

Sort by.

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

Paste LinkedIn job search URLs directly instead of using the filters above.

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

Maximum jobs to extract. LinkedIn caps any single query at 1,000 — use multiple locations or date windows to go beyond.

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

On: full description, seniority, employment type, job function, industries, apply URL. Off: listing fields only, roughly 25x cheaper because one request covers 25 jobs.

## `speedMode` (type: `string`):

Trade-off between wall-clock speed and retry depth.

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

Upper bound on parallel requests. Leave empty to use the speed mode default.

## `liAtCookie` (type: `string`):

Leave empty. Only needed for fields LinkedIn hides from logged-out visitors (exact applicant count, job poster identity). Using your own account carries a restriction risk.

## `failOnZeroResults` (type: `boolean`):

Fail run when nothing is extracted.

## `includeRunSummary` (type: `boolean`):

Append one final item with coverage, request counts, stop reason and estimated cost. Off by default so the dataset holds only job rows; the same summary is always written to the key-value store under STATS.

## `maxRunTimeSecs` (type: `integer`):

Max run time (seconds).

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

Residential proxies are strongly recommended; datacenter IPs get blocked by LinkedIn within minutes.

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

Opens each employer's LinkedIn company page once (deduplicated, cached 30 days) and adds companyWebsite, companyEmployeesCount, companyIndustry, companyAddress and companyCountry to every job.

## `pushCompanies` (type: `boolean`):

Adds one extra row per unique employer (slug, name, website, headcount, industry, country). Always stored in the key-value store under COMPANIES.

## `companyCacheTtlDays` (type: `integer`):

Reuse company profiles scraped within this many days.

## Actor input object example

```json
{
  "searchTerms": [
    "software engineer"
  ],
  "location": "Singapore",
  "locations": [],
  "datePosted": "any",
  "experienceLevel": [],
  "jobType": [],
  "workplaceType": [],
  "companyIds": [],
  "easyApplyOnly": false,
  "sortBy": "relevance",
  "startUrls": [],
  "maxItems": 100,
  "scrapeJobDetails": true,
  "speedMode": "turbo",
  "failOnZeroResults": true,
  "includeRunSummary": false,
  "maxRunTimeSecs": 3600,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "scrapeCompanyDetails": false,
  "pushCompanies": false,
  "companyCacheTtlDays": 30
}
```

# Actor output Schema

## `STATS` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

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

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "searchTerms": [
        "software engineer"
    ],
    "location": "Singapore"
};

// Run the Actor and wait for it to finish
const run = await client.actor("andrew_babo/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 = {
    "searchTerms": ["software engineer"],
    "location": "Singapore",
}

# Run the Actor and wait for it to finish
run = client.actor("andrew_babo/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 '{
  "searchTerms": [
    "software engineer"
  ],
  "location": "Singapore"
}' |
apify call andrew_babo/linkedin-jobs-scraper --silent --output-dataset

```

## MCP server setup

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