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

Scrape LinkedIn jobs by keyword, location, company & filters — no login or cookies needed. Returns title, company, logo, location, salary, seniority, full description, applicants count and more. Fast, reliable, built for scale.

- **URL**: https://apify.com/osamih/linkedin-jobs-scraper.md
- **Developed by:** [Osamih](https://apify.com/osamih) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 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/platform/actors/running/actors-in-store#pay-per-usage

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

### What does LinkedIn Jobs Scraper do?

Scrape **LinkedIn job postings** at scale — by keyword, location, company, industry, salary, seniority and more — **without a LinkedIn account, login, or cookies**. Every run returns clean, structured JSON: job title, company, logo, location, salary, seniority, employment type, applicant count, the full job description, and the direct job URL.

This Actor reads LinkedIn's **public guest job pages**, the same pages any logged-out visitor sees on [linkedin.com/jobs](https://www.linkedin.com/jobs). Nothing is scraped from behind a login, so there is no account to get restricted and no cookies to keep alive. Run it from the Apify Console, call it from your own backend through the [Apify API](https://docs.apify.com/api/v2), schedule it, hook it into Make, Zapier, Google Sheets, or Slack, and export results as JSON, CSV, Excel, HTML, or XML.

### Why use LinkedIn Jobs Scraper?

- **Job boards and aggregators** — keep a fresh index of openings in your niche, refreshed hourly or daily on a schedule.
- **Recruiting and talent intelligence** — track which companies are hiring, for which roles, in which markets, and how fast postings accumulate applicants.
- **Market and salary research** — collect posted salary ranges, seniority mixes, and remote-vs-onsite splits across an industry.
- **Lead generation** — a company that is hiring five sales engineers is a company with budget; surface those signals early.
- **Job-seeker tools** — power alerts for new postings that match a candidate's filters, including LinkedIn's "be an early applicant" (under 10 applicants) filter.

It is built on pure HTTP requests (Crawlee's `CheerioCrawler`, no headless browser), which makes it **fast and inexpensive** — a 60-job run finishes in about 35 seconds.

### How to use LinkedIn Jobs Scraper

1. Click **Try for free** / **Start** to open the Actor.
2. Type a **job title or keywords** (e.g. `Software Engineer`) and a **location** (e.g. `United States`, `London`, `Remote`).
3. Optionally narrow the search — date posted, remote/hybrid/on-site, employment type, experience level, company, industry, or minimum salary.
4. Set **Number of results** to how many jobs you want.
5. Click **Save & Start**, then watch live progress in the run's status message (`Scraped 40/100 jobs`).
6. When the run finishes, open the **Output** tab and export as JSON, CSV, Excel, HTML, or XML.

Keep the default **Apify residential proxy** — LinkedIn blocks datacenter IPs aggressively, so residential is what makes runs reliable.

### Input

The only thing you really need is a job title or a location. Everything else is an optional filter.

| Field | Type | Default | Notes |
|---|---|---|---|
| `title` | string | `""` | Job title / keywords. Quoted phrases supported. |
| `location` | string | `"United States"` | Free-text city, state, or country. |
| `companyName` | string\[] | `[]` | Resolved to LinkedIn company IDs automatically; unresolvable names fall back to quoted keyword search. |
| `companyId` | string\[] | `[]` | Numeric LinkedIn company IDs (`f_C`, e.g. `1441` for Google) for exact targeting when a name is ambiguous. OR-combined and merged with any resolved `companyName`. |
| `publishedAt` | enum | `""` | `""` any time, `r2592000` month, `r604800` week, `r86400` 24 h. |
| `workType` | enum | `""` | `1` on-site, `2` remote, `3` hybrid. |
| `contractType` | enum | `""` | `F`/`P`/`C`/`T`/`I`/`V`/`O` = full-time/part-time/contract/temporary/internship/volunteer/other. |
| `experienceLevel` | enum | `""` | `1` internship … `5` director, `6` executive. |
| `jobFunctions` | string\[] | `[]` | LinkedIn job-function codes (e.g. `eng` engineering, `sale` sales, `it` IT, `mrkt` marketing). OR-combined. |
| `industryIds` | string\[] | `[]` | Numeric LinkedIn industry codes (e.g. `4` software development, `43` financial services). OR-combined. |
| `titleIds` | string\[] | `[]` | Numeric LinkedIn title IDs for exact-title filtering (`f_T`). OR-combined. |
| `minSalary` | enum | `""` | `1`–`9` = $40k+ … $200k+ (LinkedIn salary filter; mostly US). |
| `easyApply` | boolean | `false` | Only Easy Apply jobs (apply directly on LinkedIn). |
| `under10Applicants` | boolean | `false` | Only jobs with fewer than 10 applicants so far ("Be an early applicant"). |
| `distance` | integer | `0` | Search radius in miles around the location; `0` = LinkedIn default. |
| `sortBy` | enum | `""` | `R` relevance (default), `DD` most recent first. Changes *which* jobs LinkedIn returns; not a guarantee about row order. |
| `postedWithinSeconds` | integer | `0` | Custom freshness window (e.g. `3600` = past hour). Overrides `publishedAt`. |
| `proxy` | object | Apify RESIDENTIAL | Keep residential — LinkedIn blocks datacenter IPs. |
| `rows` | integer | `50` | Target number of jobs (max 2000). LinkedIn caps one search at ~1,000; above that the Actor splits the search across facets automatically. |

> At least one of `title`, `location`, `companyName`, or `companyId` must be non-empty, otherwise the run stops immediately with `Input error: provide at least a job title, location, or company.`

#### Example input

```json
{
    "title": "Software Engineer",
    "location": "United States",
    "rows": 50,
    "publishedAt": "r604800",
    "workType": "2",
    "proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}
```

### Output

One dataset item per job. You can download the dataset in various formats such as JSON, HTML, CSV, or Excel.

```json
{
    "id": "4430579227",
    "title": "Data Engineer",
    "jobUrl": "https://www.linkedin.com/jobs/view/data-engineer-at-arlo-4430579227",
    "companyName": "Arlo",
    "companyUrl": "https://www.linkedin.com/company/arlo-health",
    "companyId": "65615916",
    "companyLogo": "https://media.licdn.com/dms/image/v2/.../company-logo_100_100/...",
    "location": "New York, NY",
    "postedTime": "8 hours ago",
    "publishedAt": "2026-07-01",
    "applicationsCount": "Over 200 applicants",
    "description": "…full plain-text job description…",
    "descriptionHtml": "<strong>About Arlo</strong><br>…raw HTML of the description…",
    "contractType": "Full-time",
    "experienceLevel": "Mid-Senior level",
    "workType": "Information Technology",
    "sector": "Insurance",
    "salary": "$150,000.00/yr - $220,000.00/yr",
    "applyUrl": null,
    "applyType": "EXTERNAL",
    "benefits": ["Medical insurance", "401(k)"],
    "posterFullName": "Grace Boyle",
    "posterProfileUrl": "https://www.linkedin.com/in/gracekboyle"
}
```

#### Data fields

| Field | Description |
|---|---|
| `id` | LinkedIn job posting ID |
| `title` | Job title |
| `jobUrl` | Canonical link to the posting |
| `companyName`, `companyUrl`, `companyId`, `companyLogo` | Hiring company details |
| `location` | Job location as shown by LinkedIn |
| `postedTime`, `publishedAt` | Relative age (`8 hours ago`) and absolute date (`YYYY-MM-DD`) |
| `applicationsCount` | Applicants so far, e.g. `Over 200 applicants` |
| `description`, `descriptionHtml` | Full job description as plain text and as HTML |
| `contractType`, `experienceLevel`, `workType`, `sector` | LinkedIn's job criteria |
| `salary` | Posted compensation range, where LinkedIn shows one |
| `applyType` | `EASY_APPLY` (apply on LinkedIn) or `EXTERNAL` (apply on the company site) |
| `benefits` | Listed benefits, where shown |
| `posterFullName`, `posterProfileUrl` | The person who posted the job, where shown |

#### Field availability

LinkedIn does not publish every field for every posting, so some are `null` depending on the job. Measured on live runs: `id`, `title`, `jobUrl`, `companyName`, `companyId`, `location`, `publishedAt`, `description` and `applyType` come back **~100% of the time**; `salary` roughly half of US searches; `posterFullName` / `posterProfileUrl` roughly a third; `benefits` mostly on US postings.

`applyUrl` is always `null` — the external application link is **not** present on LinkedIn's public pages, and reaching it would require a logged-in session, which this Actor deliberately avoids so that no account is ever put at risk. `applyType` still tells you reliably whether a job is Easy Apply or external, and `jobUrl` always takes you to the posting.

**Row order is not guaranteed.** Job detail pages are fetched concurrently and written as they complete, so sort on `publishedAt` client-side if you need ordering.

### How much does it cost to scrape LinkedIn jobs?

Measured on a real 60-job run: **$0.021 total**, which works out to roughly **$0.35 per 1,000 jobs** (about 3.5 cents per 100). Residential proxy bandwidth is the dominant cost at ~68% of the total; the Actor itself uses about 0.01 compute units per 60 jobs and peaks at ~120 MB RAM.

Apify's free plan includes $5 of monthly usage, which is enough for roughly **10,000+ jobs per month at no cost**. Your exact rate depends on your plan's residential proxy pricing and on how long the job descriptions are.

### Using the Actor via API

Small jobs (`rows` ≤ ~100) — synchronous, returns items directly:

```
POST https://api.apify.com/v2/acts/osamih~linkedin-jobs-scraper/run-sync-get-dataset-items?token=<API_TOKEN>
Content-Type: application/json

{ "title": "Software Engineer", "location": "United States", "rows": 25 }
```

Large or bursty jobs — asynchronous run plus a webhook:

```
POST https://api.apify.com/v2/acts/osamih~linkedin-jobs-scraper/runs?token=<API_TOKEN>
{
    "title": "Software Engineer", "location": "United States", "rows": 500,
    "webhooks": [{ "eventTypes": ["ACTOR.RUN.SUCCEEDED", "ACTOR.RUN.FAILED"], "requestUrl": "https://your-app.example.com/apify-callback" }]
}
```

Then fetch results from `GET https://api.apify.com/v2/datasets/{defaultDatasetId}/items`. Keep your API token server-side only.

**Run semantics for integrations:**

- **Success with 0 items** — the search legitimately matched nothing (`Finished: no jobs found for this search`).
- **Success with fewer than `rows` items** — LinkedIn had fewer results. Not an error.
- **Non-zero exit** — input or system error.

### Tips and advanced options

- **Go faster and cheaper** by setting `rows` to what you actually need; the run stops the moment it has them.
- **Monitor new postings** with `postedWithinSeconds` (e.g. `3600` for the past hour) on a schedule, rather than re-scraping everything.
- **Target a specific employer** with `companyName`. If the name is ambiguous, use `companyId` — open the company's LinkedIn page and copy the number from the URL.
- **Scrape more than 1,000 jobs** by simply asking for them: LinkedIn caps a single search at ~1,000 results, so the Actor automatically splits the query across experience levels, work types, or employment types to get past that ceiling.
- **1 GB of memory is plenty** — peak usage is ~120 MB. Compute cost scales linearly with allocated memory, so raising it just multiplies your bill.

### FAQ

**Do I need a LinkedIn account or cookies?** No. The Actor only reads publicly accessible job pages. There is no login, so there is no account to get restricted.

**Is it legal to scrape LinkedIn jobs?** This Actor collects only publicly available job-posting information — the same content any logged-out visitor can see. Scraping public data is generally lawful in many jurisdictions, but how you *use* the data is your responsibility, particularly under GDPR/CCPA where personal data (such as a job poster's name) is involved. Review LinkedIn's Terms of Service and consult your own legal counsel before commercial use. Do not use this Actor to collect personal data without a lawful basis.

**Why did I get fewer jobs than I asked for?** LinkedIn simply had fewer matching results. The run still succeeds and its status message says so. Broadening the location or removing filters usually helps.

**Why is `applyUrl` empty?** See *Field availability* above — that link is not exposed on LinkedIn's public pages.

**Can it scrape LinkedIn profiles, companies, or posts?** No. This Actor is scoped to job postings.

### Support

Found a bug or need a field that isn't here? Open an issue on the Actor's **Issues** tab and it will be looked at. Feature requests and custom-scraper enquiries are welcome.

### Development

```bash
npm install
npm test                 # unit tests (parsers, query planning)
apify run --purge        # local run, reads storage/key_value_stores/default/INPUT.json
apify push               # build & deploy
```

Architecture (see `src/`): `main.ts` wires input → proxy → crawler; `routes.ts` holds the two handlers (SEARCH pages enqueue DETAIL pages with a rolling 5-page lookahead; DETAIL pushes exactly `rows` items and stops the crawl); `parsers.ts` extracts data (schema.org `ld+json` first, CSS selectors as fallback); `company.ts` resolves company names via the guest typeahead endpoint; `queryPlan.ts` splits >1,000-row requests across search facets.

> `apify push` does **not** update `defaultRunOptions` on an Actor that already exists — it only applies them at creation. If the Console shows a different memory than `.actor/actor.json` declares, every run silently costs the wrong amount. Check with `apify api acts/<actorId>` and correct it with:
>
> ```bash
> apify api PUT acts/<actorId> -d '{"defaultRunOptions":{"build":"latest","timeoutSecs":3600,"memoryMbytes":1024}}'
> ```

# Actor input Schema

## `title` (type: `string`):

Keywords to search for (e.g. Web developer). Quoted phrases are supported. Can be left empty when filtering by company.

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

Free-text location of the job (e.g. Paris, New York, United States).

## `companyName` (type: `array`):

Names of companies to filter by (e.g. Google, NASA). Names are resolved to LinkedIn company IDs automatically; names that cannot be resolved fall back to a quoted keyword search.

## `companyId` (type: `array`):

Numeric LinkedIn company IDs to filter by (LinkedIn's f\_C parameter, e.g. 1441 for Google). Use this for exact company targeting when a name is ambiguous. To find an ID, open the company page and copy the number from the URL, or apply a company filter on linkedin.com/jobs and read f\_C from the URL. Multiple IDs are OR-combined and merged with any resolved company names.

## `publishedAt` (type: `string`):

Only return jobs published within this time range.

## `workType` (type: `string`):

Filter by workplace type.

## `contractType` (type: `string`):

Filter by employment type.

## `experienceLevel` (type: `string`):

Filter by required experience level.

## `jobFunctions` (type: `array`):

Filter by LinkedIn job function (the "Job function" filter on linkedin.com/jobs). Multiple selections are OR-combined.

## `industryIds` (type: `array`):

Numeric LinkedIn industry codes to filter by (e.g. 4 = Software Development, 43 = Financial Services, 14 = Hospitals and Health Care). Multiple IDs are OR-combined. Full list of codes: https://learn.microsoft.com/en-us/linkedin/shared/references/reference-tables/industry-codes-v2

## `titleIds` (type: `array`):

Numeric LinkedIn title IDs for exact-title filtering (LinkedIn's f\_T parameter). To find an ID, apply a "Title" filter on linkedin.com/jobs and copy the f\_T value from the URL. Multiple IDs are OR-combined.

## `minSalary` (type: `string`):

Only jobs above this annual salary. Uses LinkedIn's salary filter — applies only where LinkedIn has salary data (mostly US searches).

## `easyApply` (type: `boolean`):

Only return jobs that can be applied to directly on LinkedIn (Easy Apply).

## `under10Applicants` (type: `boolean`):

Only return jobs with fewer than 10 applicants so far (LinkedIn's "Be an early applicant" filter). Useful for applying before a posting gets crowded.

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

Search radius around the location in miles (e.g. 25). 0 = LinkedIn's default radius. Only meaningful for city-level locations.

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

Order of search results. "Most recent first" is useful for monitoring new postings.

## `postedWithinSeconds` (type: `integer`):

Custom freshness window in seconds, finer than "Date posted" (e.g. 3600 = past hour). Overrides "Date posted" when set. 0 = disabled.

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

Proxies used for scraping. Residential proxies are strongly recommended — LinkedIn blocks datacenter IPs aggressively.

## `rows` (type: `integer`):

How many jobs to scrape. LinkedIn caps a single search at ~1,000 results; above 1,000 the actor automatically splits the search into multiple sub-queries.

## Actor input object example

```json
{
  "title": "Software Engineer",
  "location": "United States",
  "publishedAt": "",
  "workType": "",
  "contractType": "",
  "experienceLevel": "",
  "minSalary": "",
  "easyApply": false,
  "under10Applicants": false,
  "distance": 0,
  "sortBy": "",
  "postedWithinSeconds": 0,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "rows": 50
}
```

# Actor output Schema

## `results` (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 = {
    "title": "Software Engineer",
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("osamih/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 = {
    "title": "Software Engineer",
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

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

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

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

```

## CLI example

```bash
echo '{
  "title": "Software Engineer",
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call osamih/linkedin-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=osamih/linkedin-jobs-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/3JkoqU3dxb6heYX5s/builds/nfOYOD4W4expLDOag/openapi.json
