# LinkedIn Job Details Scraper - By URL or Job ID (`automation_craft/linkedin-job-details-scraper`) Actor

Scrape LinkedIn job details from job URLs or job IDs, no login: one row per input in your order. Get title, company, exact posted date, full description as text, HTML and Markdown, salary, applicants, Easy Apply, closed status, optional job poster. Not found is free. Pay per job, JSON or CSV.

- **URL**: https://apify.com/automation_craft/linkedin-job-details-scraper.md
- **Developed by:** [Automation Craft](https://apify.com/automation_craft) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 5 total users, 5 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.16 / 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 Job Details Scraper - By URL or Job ID

**LinkedIn Job Details Scraper** turns a list of LinkedIn job URLs or job IDs into complete public job records, with no LinkedIn login, no cookies and no API key. Paste 1 or 20,000 jobs: every value gets exactly one row, in the order you gave, and anything LinkedIn cannot serve gets a free status row that says why. You pay only for jobs delivered.

Each job row carries the title, company (with LinkedIn company ID), location, the **exact posted date** read from LinkedIn's own search card, the full description as plain text, HTML and Markdown, seniority, employment type, job function, industries, the employer's pay range when shown, applicant count, Easy Apply or company site apply, open or closed state, and on request (two switches, off by default) the job poster card and any e-mail addresses written in the description; URLs in the description are always listed. Company details (website, size, employees on LinkedIn, headquarters, founded, specialties, followers) are one switch away.

#### Why this one

- **Every input gets a row, in order.** Not found, invalid, duplicate and blocked values get a free status row in their place. Nothing is dropped silently and nothing missing is billed.
- **Real posted dates.** LinkedIn's job page only says "2 days ago". This Actor reads the posting's date from LinkedIn's public search card, matched on the job ID, instead of guessing it from the relative text.
- **Closed jobs are marked.** Postings that show "No longer accepting applications" come back with `jobState: "closed"`, or can be skipped for free.
- **Pay once per job across runs** with an optional memory name, ideal for scheduled runs over a growing list.

### Quick start

1. Paste job URLs or IDs into **LinkedIn job URLs or job IDs** (one per line). Any of these work: `https://www.linkedin.com/jobs/view/4454862094`, a URL with the title slug and tracking, a regional URL such as `uk.linkedin.com`, a search URL with `currentJobId=...`, `urn:li:jobPosting:4454862094`, or the bare number.
2. Leave **Exact posted date** on. Turn on **Company details** if you want the company block.
3. Click **Start**. Rows appear in the Dataset tab in input order; export JSON, CSV or Excel, or read them through the API.

### What you get

Measured on 320 live open postings across 16 countries on 2026-09-29 (fill rates are what LinkedIn publishes, not what the Actor chooses to return):

| Field | What it is | Filled |
|---|---|---|
| `jobId`, `jobUrl`, `jobSlugUrl` | LinkedIn job ID and canonical URLs | 100% |
| `title`, `companyName`, `location` | As shown on the posting | 100% |
| `companyId`, `companySlug`, `companyUrl`, `companyLogoUrl` | LinkedIn's company identity | 99.7% |
| `postedDate` | Exact posting date, `YYYY-MM-DD`, from LinkedIn's search card, `postedDateSource` says how | 97% of open jobs |
| `postedTimeAgo`, `postedWindowStart`, `postedWindowEnd` | The relative text and the time window it implies | 100% |
| `jobState`, `isClosed` | `open` or `closed` (No longer accepting applications) | 100% |
| `applicantsText`, `applicantsMin`, `applicantsMax` | "Over 200 applicants" and the bounds that text states | 100% |
| `applyMethod`, `isEasyApply`, `applyButton` | `easy_apply` or `external`, from LinkedIn's own Apply button | 100% of open jobs |
| `seniorityLevel`, `employmentType`, `industries`, `industriesList` | LinkedIn job criteria | 100% |
| `jobFunction`, `jobFunctions` | Job function, also split into a list | 93% |
| `descriptionText`, `descriptionHtml`, `descriptionMarkdown`, `descriptionLength` | Full description in three formats | 100% |
| `salaryText`, `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod`, `salarySource` | Employer provided pay range, parsed | 9% (shown only when the employer provides it) |
| `jobPoster` (opt in, `includeJobPoster`) | Name, headline and profile URL from "Direct message the job poster" | 16% |
| `descriptionUrls`, `descriptionEmails` (e-mails opt in, `includeContactEmails`) | URLs and e-mail addresses written in the description | 11% each |
| `company` (option) | website, industry, companySize, employeesOnLinkedIn, headquarters, organizationType, foundedYear, specialties, followers, description, tagline | 95% of jobs (school pages are not public) |

A status row (`type: "status"`) carries `position`, `input`, `status` (`not_found`, `invalid`, `duplicate`, `already_known`, `filtered`, `blocked`, `failed`, `skipped`) and a `message`. The last row of every run is a free `summary` with counters: delivered, charged, not found, exact dates, company details, requests and retries.

Example job row (shortened):

```json
{
  "type": "job",
  "status": "ok",
  "position": 0,
  "jobId": "4451937327",
  "jobUrl": "https://www.linkedin.com/jobs/view/4451937327",
  "title": "Sales Associate - Northland Volkswagen",
  "companyName": "AutoCanada",
  "companyId": "1869302",
  "location": "Calgary, Alberta, Canada",
  "jobState": "open",
  "postedDate": "2026-09-23",
  "postedDateSource": "search_card",
  "postedTimeAgo": "5 days ago",
  "applicantsText": "36 applicants",
  "applicantsMin": 36,
  "applicantsMax": 36,
  "applyMethod": "external",
  "isEasyApply": false,
  "salaryText": "CA$90,000.00/yr - CA$170,000.00/yr",
  "salaryMin": 90000,
  "salaryMax": 170000,
  "salaryCurrency": "CAD",
  "salaryPeriod": "YEAR",
  "salarySource": "employer",
  "seniorityLevel": "Associate",
  "employmentType": "Full-time",
  "jobFunctions": ["Sales", "Business Development"],
  "industries": "Motor Vehicle Manufacturing",
  "descriptionMarkdown": "**Join the ride!**\n\n**Position**: Sales Associate ..."
}
```

### How much does it cost to scrape LinkedIn job details?

You pay per delivered job. Status rows (not found, invalid, duplicate, blocked, skipped) and the run summary are free.

| Event | FREE | BRONZE | SILVER | GOLD |
|---|---|---|---|---|
| Job | $0.20 / 1,000 | $0.20 / 1,000 | $0.18 / 1,000 | $0.16 / 1,000 |
| Company (option, once per company per run) | $0.50 / 1,000 | $0.50 / 1,000 | $0.45 / 1,000 | $0.40 / 1,000 |
| Actor start (per GB of memory, once per run) | $0.001 | $0.001 | $0.001 | $0.001 |

Worked example on the FREE tier: 1,000 job URLs with company details for 300 distinct employers cost $0.20 + $0.15 + $0.001 start (the default 512 MB run counts as one start event) = about $0.35. The exact posted date is included in the job price. The Store pricing card shows the same prices per 1,000 events.

### Input

| Field | Default | What it does |
|---|---|---|
| `jobUrls` | (sample jobs) | Job URLs, URNs or IDs, one per line. `jobIds`, `job_id`, `jobUrlsOrIds`, `startUrls` and `urls` are accepted too. |
| `maxJobs` | 0 (no limit) | Stop after this many delivered jobs; later values get a free skipped row. |
| `exactPostedDate` | true | One public search per open job to read the posting date. |
| `includeCompanyDetails` | false | Company block from the public company page, billed once per company per run. |
| `includeJobPoster` | false | The job poster card when the page shows one (personal data, off by default). |
| `includeContactEmails` | false | E-mail addresses written in the description (off by default). |
| `includeDescriptionHtml`, `includeDescriptionMarkdown` | true | Keep the HTML and Markdown versions of the description. |
| `skipClosedJobs` | false | Closed postings become free filtered rows. |
| `memoryName`, `resetMemory` | empty | Pay once per job across runs that share the name. |
| `maxConcurrency` | 5 | Jobs fetched in parallel (1 to 10). |
| `proxyConfiguration` | Apify Proxy | The datacenter pool is included in the price. |

API example:

```bash
curl -X POST "https://api.apify.com/v2/acts/automation_craft~linkedin-job-details-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jobUrls":["https://www.linkedin.com/jobs/view/4454862094","4011051212"],"includeCompanyDetails":true}'
```

### FAQ

#### Do I need a LinkedIn account, cookies or an API key?

No. The Actor reads the public job pages that LinkedIn serves to signed out visitors. Nothing in the input asks for a credential, and it never touches personal profiles beyond the job poster card the job page itself shows.

#### How do I get the job description from a LinkedIn job URL?

Paste the URL (or just the number in it) and run. `descriptionText` holds the full description with line breaks, `descriptionHtml` the markup LinkedIn serves and `descriptionMarkdown` a clean Markdown version for LLM prompts and CMS imports.

#### Does it return closed or expired jobs?

Yes. A posting that shows "No longer accepting applications" is still a complete public record with its description, returned with `jobState: "closed"`. Turn on **Skip closed jobs** to get a free filtered row for those instead.

#### Why is postedDate empty for some jobs?

LinkedIn's job page shows only relative time. The exact date comes from the posting's own card in LinkedIn's public search, which exists for open postings that LinkedIn still lists (97 percent in our measurements). Closed postings have no card, so `postedDate` is null and `postedDateSource` says `not_available_closed`; the relative time and its window are always there.

#### Does it return the external apply URL?

No. LinkedIn shows the company site apply link only to signed in members. The row says whether the job uses Easy Apply or the company site (`applyMethod`), and `descriptionUrls` lists any application or careers URL the employer wrote into the description.

#### Can I run it on a schedule and only pay for new jobs?

Yes. Set **Memory name** (for example `weekly-alerts`) and schedule the run: jobs delivered under that name come back as free `already_known` rows on later runs. Do not start two runs with the same memory name at the same time; each run saves the memory as it goes, and two runs writing one memory at once can each bill a job the other already delivered.

#### Why does this Actor run with limited permissions?

It runs with Apify's limited permissions, the least privilege level: it can only read its input and write to its own dataset and key value stores. The optional cross run memory is a named key value store the Actor creates for itself under your account.

### What this Actor does NOT do

- It does not search LinkedIn by keyword: it reads the jobs you give it. To find job IDs first, use a LinkedIn jobs search actor and pass its IDs here.
- It does not log in, so it cannot return views, the number of applies, the external apply URL, workplace type or recruiter contact details beyond the public poster card.
- It does not follow `lnkd.in` short links; paste the full job URL.
- It does not read LinkedIn school pages for company details (LinkedIn does not show them to signed out visitors).

### More data tools by Automation Craft

- [LinkedIn Jobs Scraper - No Login, Real Dates](https://apify.com/automation_craft/linkedin-jobs-scraper)
- [LinkedIn Ad Library Scraper: Ads by Company](https://apify.com/automation_craft/linkedin-ad-library-scraper)
- [Workday Jobs Scraper & API - Whole Career Site](https://apify.com/automation_craft/workday-jobs-scraper)
- [Greenhouse Jobs Scraper & API - Pay Once Per Job](https://apify.com/automation_craft/greenhouse-jobs-scraper)
- [ATS Jobs Scraper & API - Greenhouse, Lever, Ashby](https://apify.com/automation_craft/ats-jobs-scraper)
- [SEEK Job Scraper: Australia & New Zealand Jobs](https://apify.com/automation_craft/seek-job-scraper)
- [Google News Scraper: Search, Topics, Decoded URLs](https://apify.com/automation_craft/google-news-scraper)

This Actor is an independent tool and is not affiliated with or endorsed by LinkedIn. It reads only publicly available job postings.

# Changelog

This Actor's version history is a separate document: https://apify.com/automation_craft/linkedin-job-details-scraper/changelog.md

# Actor input Schema

## `jobUrls` (type: `array`):

One job per line. Accepted: job view URLs (https://www.linkedin.com/jobs/view/4454862094, with or without the title slug and tracking), regional URLs (uk.linkedin.com), search or collection URLs that carry currentJobId=..., URNs (urn:li:jobPosting:4454862094) and bare numeric IDs. Up to 20,000 values per run. An input with no job list at all (a literal {}) runs these three sample jobs so the Actor can be tried with one click; an explicitly empty list returns a free status row.

## `maxJobs` (type: `integer`):

Stop after this many delivered jobs. 0 means no limit (every value in the list). Values after the limit get a free skipped row.

## `exactPostedDate` (type: `boolean`):

LinkedIn's job page only says "2 days ago". When on, one extra public search per open job reads the posting's own date from LinkedIn's search card (matched on the job ID) into postedDate. Included in the job price. Closed jobs have no card, so their postedDate stays null.

## `includeCompanyDetails` (type: `boolean`):

Adds a company object from the public LinkedIn company page: website, industry, company size, employees on LinkedIn, headquarters, type, founded year, specialties, followers, description. Billed with the Company event once per company per run, however many of its jobs you pass.

## `includeJobPoster` (type: `boolean`):

Off by default: the poster card stays out of the dataset. Turn on to add the poster's name, headline and public profile URL as jobPoster when the public page shows "Direct message the job poster".

## `includeContactEmails` (type: `boolean`):

Off by default. Turn on to add descriptionEmails: e-mail addresses the employer wrote into the job description (about 1 posting in 10, often a careers or recruiter address).

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

Keep descriptionHtml (the job description markup as LinkedIn serves it). descriptionText is always included.

## `includeDescriptionMarkdown` (type: `boolean`):

Keep descriptionMarkdown (headings, bold and lists preserved), ready for LLM prompts and CMS imports.

## `skipClosedJobs` (type: `boolean`):

Postings that show "No longer accepting applications" get a free filtered row instead of a paid job row. Off by default: a closed posting is still a complete record with its description.

## `memoryName` (type: `string`):

Optional. Give a name, for example weekly-alerts, and jobs delivered under it are remembered: later runs with the same name return a free already_known row for them instead of a paid row. Leave empty to deliver every job every run.

## `resetMemory` (type: `boolean`):

Forget every job remembered under memoryName before this run starts.

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

How many jobs are fetched at the same time (1 to 10). The default 5 is fast and polite.

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

Apify Proxy (the automatic datacenter pool) is used by default and is part of the price. A fresh exit is used for every retry.

## Actor input object example

```json
{
  "jobUrls": [
    "https://www.linkedin.com/jobs/view/4454862094",
    "4011051212",
    "https://www.linkedin.com/jobs/view/3044370495"
  ],
  "maxJobs": 0,
  "exactPostedDate": true,
  "includeCompanyDetails": false,
  "includeJobPoster": false,
  "includeContactEmails": false,
  "includeDescriptionHtml": true,
  "includeDescriptionMarkdown": true,
  "skipClosedJobs": false,
  "resetMemory": false,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (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 = {
    "jobUrls": [
        "https://www.linkedin.com/jobs/view/4454862094",
        "4011051212",
        "https://www.linkedin.com/jobs/view/3044370495"
    ],
    "maxJobs": 0,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation_craft/linkedin-job-details-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 = {
    "jobUrls": [
        "https://www.linkedin.com/jobs/view/4454862094",
        "4011051212",
        "https://www.linkedin.com/jobs/view/3044370495",
    ],
    "maxJobs": 0,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("automation_craft/linkedin-job-details-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 '{
  "jobUrls": [
    "https://www.linkedin.com/jobs/view/4454862094",
    "4011051212",
    "https://www.linkedin.com/jobs/view/3044370495"
  ],
  "maxJobs": 0,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call automation_craft/linkedin-job-details-scraper --silent --output-dataset

```

## MCP server setup

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