# LinkedIn Jobs Scraper - Full Descriptions + Company, No Login (`lanternlab318/linkedin-jobs`) Actor

Search LinkedIn's public job listings by keyword and location, no login or cookies. Each job comes with its full description, company, location, posting date, applicant count, seniority, employment type, industries and salary when shown. Partial rows and duplicates are free.

- **URL**: https://apify.com/lanternlab318/linkedin-jobs.md
- **Developed by:** [Lantern Lab](https://apify.com/lanternlab318) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.49 / 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.

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 - Full Descriptions + Company, No Login

Search LinkedIn's public job listings by keyword and location and get every posting back with its **full description, company name and company page, location, posting date, applicant count, seniority level, employment type and industries**, plus salary where LinkedIn shows one. No LinkedIn login, no cookies, no account of your own. **$1.49 per 1,000 complete job postings, no start fee.** Postings LinkedIn would not open for us are still written, marked partial, and not charged. See [Pricing](#pricing).

Input:

```json
{
  "keywords": ["software engineer", "data analyst"],
  "location": "United States",
  "datePosted": "week",
  "jobType": ["fulltime"],
  "experienceLevel": ["mid-senior"],
  "includeDescription": true,
  "maxItems": 200
}
```

Output - one row per posting (`recordType: "job"`); per-term status rows and the run summary are free. Sample from a real run, description shortened:

```json
{
  "recordType": "job",
  "jobId": "4479196690",
  "url": "https://www.linkedin.com/jobs/view/4479196690/",
  "title": "Software Engineer I, Fullstack (Search)",
  "companyName": "Affirm",
  "companyUrl": "https://www.linkedin.com/company/affirm",
  "location": "Madison, WI",
  "postedAt": "2026-10-16",
  "publishedAt": "2026-10-16",
  "postedAtText": "1 week ago",
  "applicantsText": "41 applicants",
  "benefitsText": "Actively Hiring",
  "seniorityLevel": "Mid-Senior level",
  "employmentType": "Volunteer",
  "jobFunction": null,
  "industries": "Financial Services",
  "salaryText": null,
  "descriptionText": "At Affirm, we exist for the moments that matter—giving people a clear, predictable way to pay over time…",
  "applyMethod": "external",
  "applyUrl": null,
  "query": "software engineer",
  "searchLocation": "United States",
  "rank": 3,
  "partial": false,
  "partialReason": null,
  "collectedAt": "2026-10-24T09:43:15.750Z"
}
```

Values are what LinkedIn shows on the posting. In the sample above the employer itself set the employment type to "Volunteer"; the Actor does not correct it.

Limits:

- **Public data only.** The guest job search and the guest job page that LinkedIn serves to logged-out visitors. Nothing behind sign-in: no "who's hiring" names, no recruiter or poster profiles, no Easy Apply forms, no "see how you compare" insights.
- **`applyUrl` is usually null.** LinkedIn shows the employer's apply link to logged-out visitors on only some postings; elsewhere it sits behind sign-in. `applyMethod` still says whether the posting is applied to on the employer's site (`external`) or on LinkedIn (`linkedin`).
- **No on-site / remote / hybrid filter.** LinkedIn neither applies that filter nor shows a posting's workplace type to logged-out visitors, so `workplaceType` is refused with an error rather than silently ignored. Put "remote" in your location or search terms instead.
- **Job type and experience level are filtered by the Actor, not by LinkedIn.** LinkedIn ignores those two filters for logged-out visitors, so the Actor opens each posting and keeps it only if LinkedIn's own Employment type / Seniority level on the page matches. Dropped postings are free, but a narrow filter makes a run slower. Many employers set seniority to "Not Applicable", which matches no level. `datePosted` is applied by LinkedIn's search itself.
- **At most 1,000 postings per search.** That is LinkedIn's own ceiling for one keyword + location + filter combination. Use more terms, locations or filters for more.
- **Fields LinkedIn leaves out are null, never guessed.** `salaryText` is filled only where LinkedIn shows a salary field (about one posting in ten in our tests; a range written inside the description stays in `descriptionText`). `jobFunction` is currently not shown to logged-out visitors at all, so it is null on every row; the field is kept so your schema does not change if LinkedIn shows it again.

### What it does

- Searches each term separately, in the location you give, filtered by date posted, job type and experience level.
- Opens each posting's public page for the full description (as plain text in `descriptionText` and as LinkedIn's HTML in `descriptionHtml`), the applicant count and the job criteria. Turn `includeDescription` off for a faster, search-card-only list.
- Deduplicates across the whole run: a posting that matches several of your terms is written once and charged once.
- Keeps LinkedIn's posting ID (`jobId`), LinkedIn's posting date (`postedAt` / `publishedAt`, a calendar date, plus `postedAtText` as shown) and our collection time (`collectedAt`) as separate fields.
- Says what happened per search term in a free `query_status` row: how many postings it returned, how many repeats were skipped, how many pages were read and why paging ended. A term LinkedIn answered with nothing is an empty result; a term cut short by a limit, refusal or error is `partial: true` with the reason. The two are never confused.
- Retries every request LinkedIn refuses (HTTP 429) from a different IP. A posting whose page still cannot be read, or that was closed after it was listed (HTTP 404, `partialReason: "job_closed"`), is written from its search card, marked partial and not charged.
- Caps itself: `maxUpstreamRequests` is a hard ceiling on requests to LinkedIn for the whole run, retries included, derived from your input when you leave it empty, so a run is never unbounded.

### Input

| Field | What it does |
| --- | --- |
| `keywords` | Search terms, one per line. Required. |
| `location` | City, region or country as you would type it into LinkedIn, e.g. `United States`, `London, England, United Kingdom`, `Bengaluru`. Empty = worldwide. |
| `datePosted` | `any`, `24h`, `week` or `month`. |
| `jobType` | Any of `fulltime`, `parttime`, `contract`, `temporary`, `internship`, `volunteer`, `other`. Matched against the posting's own Employment type. |
| `experienceLevel` | Any of `internship`, `entry`, `associate`, `mid-senior`, `director`, `executive`. Matched against the posting's own Seniority level. |
| `includeDescription` | Default `true`: open each posting for its description and criteria. |
| `maxItems` | Maximum job rows for the whole run (default 100), filled term by term in the order given. |
| `maxUpstreamRequests` | Optional hard ceiling on requests to LinkedIn. |

### Output

- `recordType: "job"` - one per posting. Charged only when `partial` is false.
- `recordType: "query_status"` - one per search term. Free.
- `recordType: "RUN_SUMMARY"` - the last row, also stored under the key `RUN_SUMMARY`: postings collected and charged, partial postings by reason, duplicates, empty and incomplete terms, requests and retries. Free.

`partialReason` values: `blocked` (LinkedIn kept refusing after every retry), `job_closed` (posting removed after it was listed), `upstream_request_cap`, `charge_limit_reached`, `upstream_error`, `not_collected` (the run stopped before this term).

### Pricing

Pay per result: **$1.49 per 1,000 complete job postings**, no start fee. Partial rows, status rows and the run summary are free. Set a maximum charge per run in Apify and the Actor stops at it, keeping what it already collected and marking the term `charge_limit_reached`.

### Is it legal?

It reads only pages LinkedIn publishes to logged-out visitors, and it collects no personal profiles: no recruiter, poster or employee names. Company names and company pages are public business information. You are responsible for how you use the data; check LinkedIn's terms and the data-protection rules that apply to you.

# Actor input Schema

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

One job search term per line, for example "data analyst" or "python developer". Each term is searched separately and its rows are labelled with it. A posting that matches several terms is returned and charged once.

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

City, region or country as you would type it into LinkedIn's location box, for example "United States", "London, England, United Kingdom", "Berlin" or "Bengaluru". Leave empty to search worldwide.

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

Only postings published within this window. Applied by LinkedIn's search.

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

Only postings whose own Employment type, as LinkedIn prints it on the posting, is one of these. LinkedIn does not apply this filter for logged-out visitors, so the Actor opens each posting and drops the ones that do not match; dropped postings are not written and not charged. Leave empty for all.

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

Only postings whose own Seniority level, as LinkedIn prints it on the posting, is one of these. Applied by the Actor in the same way as Job type; postings marked "Not Applicable" by the employer do not match any level. Leave empty for all.

## `includeDescription` (type: `boolean`):

Open each posting's public page for its full description, applicant count, seniority level, employment type and industries. Turn off for a faster list of search-card fields only (title, company, location, date).

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

Upper bound on job rows for the whole run, across all search terms, in the order given: once it is reached, later terms are not searched. LinkedIn's public search shows at most 1,000 postings per search; use more terms, locations or filters for more.

## `maxUpstreamRequests` (type: `integer`):

Hard ceiling on the HTTP requests this run sends to LinkedIn, retries included. At the ceiling collection stops, the rest is marked partial with partialReason "upstream_request_cap", and the run ends normally. Leave empty and the Actor works out a ceiling from your input, so a run is never unbounded.

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

The proxy requests to LinkedIn go through. The default Apify datacenter proxy works; each request LinkedIn refuses is retried from another IP.

## Actor input object example

```json
{
  "keywords": [
    "data analyst"
  ],
  "location": "United States",
  "datePosted": "any",
  "includeDescription": true,
  "maxItems": 100,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `runSummary` (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 = {
    "keywords": [
        "data analyst"
    ],
    "location": "United States"
};

// Run the Actor and wait for it to finish
const run = await client.actor("lanternlab318/linkedin-jobs").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": ["data analyst"],
    "location": "United States",
}

# Run the Actor and wait for it to finish
run = client.actor("lanternlab318/linkedin-jobs").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": [
    "data analyst"
  ],
  "location": "United States"
}' |
apify call lanternlab318/linkedin-jobs --silent --output-dataset

```

## MCP server setup

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

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/aTxnK9nI5h7Qa433P/builds/wRRsNAVz5XmGA05WW/openapi.json
