# LinkedIn Jobs Scraper — No Login, No Cookies (`dottti/linkedin-jobs-scraper`) Actor

Scrape LinkedIn job postings by keyword and location: title, company, location, posting date and apply link. Filter by date posted, experience, job type and remote. No login, no cookies.

- **URL**: https://apify.com/dottti/linkedin-jobs-scraper.md
- **Developed by:** [Mohanad Alshaka](https://apify.com/dottti) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 job scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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 Login, No Cookies

Scrape LinkedIn job postings by keyword and location: title, company, location, posting date and apply link. Filter by date posted, experience level, job type and remote status.

No login. No cookies. No session credentials to supply or keep alive.

### Output

```json
{
  "jobId": "4467038602",
  "title": "Senior Data Engineer",
  "company": "Haystack",
  "companyUrl": "https://uk.linkedin.com/company/wearehaystack",
  "location": "London, England, United Kingdom",
  "postedDate": "2026-09-14",
  "postedText": "7 hours ago",
  "salary": null,
  "companyLogo": "https://media.licdn.com/...",
  "isEasyApply": false,
  "url": "https://uk.linkedin.com/jobs/view/senior-data-engineer-at-haystack-4467038602",
  "searchKeywords": "data engineer",
  "searchLocation": "London"
}
```

`postedDate` is a real ISO date taken from LinkedIn's own `datetime` attribute, not derived from "7 hours ago". Both are returned.

Every row records the search that produced it, so one run can cover many keyword and location pairs and stay sortable afterwards.

### What this does and does not cover

This reads LinkedIn's **public guest job feed**. That surface answers without authentication and is the only one that does.

Profiles, company pages and connection data are **not** included and are not attempted. Those require authentication and heavy proxying, and an Actor that claimed to scrape them from a plain request would be selling something it cannot deliver.

### It reports its own breakage

This is HTML, not an API, so LinkedIn can change the markup at any time. The failure mode that matters is subtle: cards keep arriving, fields stop matching, and a scraper that only counts its own output reports a successful run that returned nothing.

Every page therefore compares **cards seen** against **cards parsed**. If cards arrive but stop parsing, the run logs it and `RUN_SUMMARY` counts it. A test injects exactly that defect — ten cards with changed markup — and asserts the run is flagged unhealthy rather than passing silently.

An empty response is treated as the end of results, not as breakage.

### Input

| Field | What it does |
| --- | --- |
| `searches` | Objects with `keywords` and/or `location`. Several can run in one job. |
| `maxJobsPerSearch` | Cap per search, and therefore on cost. LinkedIn returns 10 per request. |
| `datePosted` | Any time, past month, past week, past 24 hours. |
| `experience` | Internship through Executive, using LinkedIn's own levels. |
| `jobType` | Full-time, part-time, contract, temporary, internship, volunteer. |
| `workplace` | On-site, remote or hybrid. |

#### Example: remote senior roles posted this week

```json
{
  "searches": [
    { "keywords": "data engineer", "location": "London" },
    { "keywords": "product manager", "location": "Dubai" }
  ],
  "maxJobsPerSearch": 200,
  "datePosted": "week",
  "experience": "mid_senior",
  "workplace": "remote"
}
```

### Notes and limits

- Public job postings only, exactly as an unauthenticated visitor sees them.
- Salary appears only where the employer published it, which is a minority of postings. It is returned as `null` rather than estimated.
- Duplicate job IDs within one search are removed automatically.
- LinkedIn rate-limits aggressively. The default delay is deliberately conservative; lower it only if the log shows no throttling.
- The guest feed exposes a finite result window per search. Narrow the keywords or location rather than raising `maxJobsPerSearch` to reach deeper.

### Development

```bash
npm install
npm test
node src/main.js
```

# Actor input Schema

## `searches` (type: `array`):

One or more searches, each an object with keywords and/or location. Example: \[{"keywords": "data engineer", "location": "London"}]

## `maxJobsPerSearch` (type: `integer`):

Hard cap per search, and therefore on cost. LinkedIn returns 10 jobs per request.

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

Restrict to recently posted jobs.

## `experience` (type: `string`):

LinkedIn's own experience-level filter.

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

Employment type.

## `workplace` (type: `string`):

On-site, remote or hybrid.

## `requestDelayMs` (type: `integer`):

LinkedIn throttles bursts hard. Lower this only if the log shows no 429s.

## `maxRetries` (type: `integer`):

Retries with exponential backoff on 403, 429 and 5xx.

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

LinkedIn rate-limits aggressively. A proxy is recommended for large runs, though the guest endpoint answers without one at modest volume.

## Actor input object example

```json
{
  "searches": [
    {
      "keywords": "data engineer",
      "location": "London"
    }
  ],
  "maxJobsPerSearch": 100,
  "datePosted": "any",
  "experience": "any",
  "jobType": "any",
  "workplace": "any",
  "requestDelayMs": 1200,
  "maxRetries": 4,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Job ID and apply URL, title, company and company LinkedIn URL, location, posting date as ISO and as displayed, salary where shown, company logo, easy-apply flag, plus the keywords and location of the search that produced the row.

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

Per-search outcome: jobs delivered, pages fetched, any search that stopped early, and a count of pages where cards arrived but failed to parse.

# 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 = {
    "searches": [
        {
            "keywords": "data engineer",
            "location": "London"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("dottti/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 = { "searches": [{
            "keywords": "data engineer",
            "location": "London",
        }] }

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

```

## MCP server setup

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