# LinkedIn Jobs Scraper - Full Descriptions & Job Feeds (`benthepythondev/linkedin-jobs-scraper`) Actor

Search public LinkedIn jobs with full descriptions, company data, source filters and persistent only-new monitoring. No LinkedIn login or third-party API key.

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

## Pricing

from $0.90 / 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?

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

## LinkedIn Jobs Scraper: full descriptions and new-job monitoring

Search public LinkedIn job postings and export a consistent dataset with the full public description, employer, location, posting date and employment criteria. Use keywords and a location, or provide exact job URLs. Enable `onlyNew` to remember exported job IDs and omit them from later runs of the same saved search.

This Actor retrieves LinkedIn's public job-search cards and public job-detail pages directly. It does not require a LinkedIn login, session cookie or separate data-provider API key. Default runs use HTTP without a browser or proxy. Optional proxy configuration is available for accounts that need a different source route.

**For recurring hiring feeds:** collect full public job descriptions and retain job IDs between runs with `onlyNew`. Result events cost **$1.80 per 1,000 jobs on Free** or **$0.90 on paid plans**, plus the start event. This Actor returns jobs, not personal profiles or candidate contact details.

### Quick start

Start with a tested example for a Berlin Python job feed:

- [Python jobs in Berlin with full descriptions](https://apify.com/benthepythondev/linkedin-jobs-scraper/examples/berlin-python-jobs-full-descriptions)

Open the example and choose **Try for free** to copy its input into your account. Check the input and set a maximum run charge before starting; Actor fees still follow the pricing below. When the run finishes, open the run's dataset and select the **Overview** view and download JSON for nested fields or CSV/Excel for a table.

| Your goal | Input to use |
|---|---|
| Find recent jobs | `keywords`, `location` and `timeRange` |
| Read known postings | `jobUrls`; this bypasses search filters |
| Receive only new IDs | `onlyNew` with a separate `monitorId` per search |

The [real output example](#example-output) shows the record shape. The [Python workflow](#python-api-workflow) saves your own export to a local JSON file with a $0.05 maximum Actor charge.

### Start with a small search

```json
{
  "keywords": "python",
  "location": "Berlin, Germany",
  "maxResults": 3,
  "maxPages": 2,
  "timeRange": "week",
  "sortBy": "date",
  "strictKeywords": true,
  "onlyNew": false
}
```

The Actor asks LinkedIn for recent Python jobs in Berlin, reads the public descriptions, checks the keywords, and returns up to three matching jobs. It examines no more than two search pages. Fewer results can be correct: the source may have fewer matches, repeat an earlier page, or return recommendations that do not satisfy the strict keyword check.

Inspect the first dataset before increasing the cap. The overview table shows title, employer, location, posting date, employment type, salary text and the source link. JSON contains the full description and additional fields. Export CSV, Excel or JSON from Apify, or consume the default dataset through its API.

### Why check the keywords again?

LinkedIn's guest endpoint can return unrelated recommendations even for a nonsense keyword. A technically successful HTTP request does not establish that those jobs match your search. With `strictKeywords: true`, every whitespace-separated keyword word must appear in the title or public description, ignoring case and respecting word boundaries. For example, `Java` does not match a record solely because it mentions `JavaScript`.

This is a literal text check, not a relevance score. Use simple words such as `python` or `software engineer`. For LinkedIn-specific Boolean syntax or a broader source-defined search, turn strict matching off and inspect the resulting relevance yourself. The Actor does not replace missing matches with invented listings.

### Example output

One record from a verified September 9, 2026 cloud run. Only the description is shortened here; the Actor exports the complete public description, including paragraph breaks. The dated vacancy may no longer be open.

```json
{
  "job_id": "4462232588",
  "title": "Python Developer",
  "company": "Russell Tobin",
  "company_url": "https://www.linkedin.com/company/russell-tobin-&-associates-llc",
  "location": "Berlin, Germany",
  "posted_date": "2026-09-07",
  "posted_text": "1 day ago",
  "salary": null,
  "url": "https://www.linkedin.com/jobs/view/4462232588",
  "description": "Role: Software Engineer Employment Type: Contract – 12 Months minimum Location: Germany - Berlin, Cologne, Frankfurt, Dusseldorf, Munich, Hamburg, Stuttgart Travel & Mode of work: Travel required at client’s locations & Hybrid mode of work Experience required – 5-12 years Max NOTE – No architect, Manager, Full stack, Frontend required Required- have experience using tools like Python, Kedro, AWS, Azure, Databricks, Docker, Kubernetes, Django, Flask, and MySQL / Postgres advocate using the right tech for the right task—you’ll be up to date with the latest [description shortened in this documentation example]",
  "applicants_text": "100 applicants",
  "apply_url": null,
  "seniority": "Mid-Senior level",
  "employment_type": "Contract",
  "job_function": null,
  "industries": "Technology, Information and Media",
  "scraped_at": "2026-09-09T17:50:26.850503+00:00",
  "is_new": false,
  "monitor_id": null
}
```

### Returned fields

| Field | Meaning |
|---|---|
| `job_id` | LinkedIn's numeric job ID, returned as a string |
| `title` | Title from the job-detail page |
| `company`, `company_url` | Employer name and public LinkedIn company URL |
| `location` | Location text supplied by LinkedIn |
| `posted_date` | Calendar date from the search card, when available |
| `posted_text` | Source wording such as “4 days ago” |
| `description` | Complete public description as plain text, retaining paragraph and list breaks |
| `seniority`, `employment_type` | Explicit job criteria from the detail page |
| `job_function`, `industries` | Source job criteria, when exposed |
| `salary` | Source salary text, without guessing a currency or annualizing a range |
| `applicants_text` | Source applicant wording; it may be a range rather than an exact count |
| `apply_url` | External application URL only when the public page exposes it |
| `url` | Stable LinkedIn job URL without search tracking parameters |
| `scraped_at` | UTC collection timestamp |
| `is_new`, `monitor_id` | Optional monitor metadata |

Missing source fields are not inferred. A direct job URL may have relative posting text without a machine-readable calendar date. Salary and external application URLs are not guaranteed. Company information describes the employer named on the posting; the Actor does not identify employees or infer their contact addresses.

### Source filters

`location` is passed to LinkedIn as entered. `timeRange` accepts `all`, `day`, `week` or `month`; `sortBy` accepts `date` or `relevance`. Available posting dates are also checked against the selected recency window at calendar-day precision. Date-only source data cannot establish an exact elapsed-hour boundary.

Optional array filters use LinkedIn's source values:

| Input | Accepted values |
|---|---|
| `workTypes` | `1` onsite, `2` remote, `3` hybrid |
| `jobTypes` | `F` full time, `P` part time, `C` contract, `T` temporary, `I` internship, `V` volunteer, `O` other |
| `experienceLevels` | `1` internship, `2` entry, `3` associate, `4` mid senior, `5` director, `6` executive |

These filters are submitted to the source. The Actor preserves the returned criteria instead of claiming that every employer has categorized its listing correctly. An empty filter array means any value. Keep the query and all filter settings stable when comparing recurring snapshots.

### Exact job URLs

```json
{
  "jobUrls": ["https://www.linkedin.com/jobs/view/4462980082"],
  "maxResults": 1
}
```

That ID came from a real September 9, 2026 local source check for a Software Engineer (Python) role. It is a dated example, not a promise that the vacancy remains open. Replace it with a current public job URL. Numeric IDs are also accepted. Direct mode bypasses search, keyword and recency filters, deduplicates repeated IDs, and verifies that each detail response belongs to the requested job.

### Recurring only-new feed

```json
{
  "keywords": "python",
  "location": "Berlin, Germany",
  "timeRange": "week",
  "maxResults": 25,
  "maxPages": 5,
  "onlyNew": true,
  "monitorId": "berlin-python-jobs",
  "firstRunMode": "seedOnly"
}
```

Save this input as an Apify Task and schedule it after inspecting a small run. A `seedOnly` first run records its snapshot without returning the existing backlog. Later runs compare their bounded snapshots against the saved IDs. Use `emitAll` to export the initial snapshot instead.

Monitor state lives in a named key-value store in the account running the Actor. Different customers do not share IDs. Use one explicit `monitorId` per search, avoid overlapping runs, and create a new ID when changing filters. `resetState: true` clears only the selected baseline; turn it off again before scheduling.

The monitor retains at most 5,000 IDs for 90 days. An ID that ages out can appear as new if the source returns it again. `maxResults` bounds the collected snapshot before only-new filtering, so an unchanged run can correctly produce zero rows. It does not continue indefinitely until it finds that many unseen jobs. Increase the snapshot/page limits when the current window is too small for your use case.

Only successfully exported IDs are marked seen after a spending-limit stop. Unexported jobs remain eligible for a later run. A source failure stops collection before the monitor baseline is updated. This is a new-job feed; it does not detect every description edit or treat a missing search result as proof that a job closed.

### Pricing and limits

Free-plan pricing is **$0.0018 per exported job**, or **$1.80 per 1,000 jobs**. Bronze, Silver, Gold, Platinum and Diamond pay **$0.0009 per job**, or **$0.90 per 1,000 jobs**. Full public descriptions and monitor mode are included; there is no separate description or monitoring event.

At 512 MB, the start event is $0.00005 on Free, $0.000045 on Bronze, $0.00004 on Silver and $0.000035 on Gold, Platinum or Diamond. A ten-job export costs **$0.01805 on Free** or **$0.009035 on Gold**. A seed-only run or an unchanged repeat has no result charge, but still has the start charge. Larger memory settings use one start event per GB, with a minimum of one. These are pricing examples, not forecasts of output or future bills; inspect Apify's current pricing panel and set a maximum run charge.

The result cap is 500 and the page cap is 50. A page currently contains about ten search cards, but the source can change that. Requests run sequentially with a delay. Default memory is 512 MB and the default timeout is five minutes; large searches or slow routes can require a larger timeout. Optional proxy availability and usage depend on your account. No paid external data provider is required by the default engine.

### Python API workflow

Copy the first input JSON above into `input.json` and save this code as `export.py`. In a Python virtual environment, install `apify-client` with `python -m pip install apify-client`. Set `APIFY_TOKEN` to your own Apify token in your environment, then run `python export.py`. Keep the token out of source files and shared screenshots.

```python
import json
import os
from decimal import Decimal
from pathlib import Path

from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("benthepythondev/linkedin-jobs-scraper").call(
    run_input=json.loads(Path("input.json").read_text(encoding="utf-8")),
    max_total_charge_usd=Decimal("0.05"),
    memory_mbytes=512,
    timeout_secs=300,
)
if not run or run["status"] != "SUCCEEDED":
    raise RuntimeError(f"Inspect the run before retrying: {run and run['id']}")

rows = list(client.dataset(run["defaultDatasetId"]).iterate_items())
Path("berlin-python-jobs.json").write_text(
    json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8"
)
print(f"Saved {len(rows)} records from run {run['id']}")
```

This starts a run in your account, waits for completion and saves `berlin-python-jobs.json`. The $0.05 limit bounds Actor charges; it is not a promised bill or result count. Check the printed run ID and server log if the result is shorter than expected. If your client loses its connection, inspect the existing run in Console before starting another one. See the [official Python client](https://docs.apify.com/api/client/python/) for other run options.

Use `job_id` as the destination key and keep the complete `description`. The example exports a snapshot. To create a recurring feed, first test the documented only-new input, then save it as your own Task. An unchanged monitor can return zero rows. A job absent from one bounded search is not confirmed closed.

For recurring runs without writing a scheduler, copy a tested public Task into your account and attach an Apify Schedule to that copy. Keep its maximum-charge limit and avoid overlapping runs when using monitor state. The Python example above is also suitable for an existing scheduler; run it from the directory containing `input.json` and provide the token through that scheduler's secret settings.

### Failures, support and related feeds

A blocked, rate-limited or malformed source page is surfaced as a failed run rather than a successful empty export. Temporary rate limits and server errors receive bounded retries. Persistent restrictions, removed direct jobs and missing descriptions still need attention; reduce the scope or inspect the failed run before retrying. No scraper can promise uninterrupted source access.

For a reproducible issue, open an Actor issue with the run URL, relevant input and expected result. Do not include credentials. An honest review after a useful export helps other users evaluate the Actor.

For other job sources, see the [Remote Jobs feed](https://apify.com/benthepythondev/remote-jobs-aggregator) and [Greenhouse jobs](https://apify.com/benthepythondev/greenhouse-jobs-scraper). They have separate source coverage and pricing. This Actor covers public job advertisements; it does not scrape private LinkedIn profiles, employee lists or messages.

Keywords: LinkedIn jobs scraper, LinkedIn job search API, public job postings, full job descriptions, hiring data, job monitoring, new job alerts, recruitment data, LinkedIn jobs CSV, Berlin Python jobs.

# Actor input Schema

## `keywords` (type: `string`):

Plain-text source search. Strict matching requires every word in the title or full description; disable it for source-defined query syntax.

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

Location passed to LinkedIn.

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

Public /jobs/view/ URLs or numeric IDs. Overrides search; keyword and recency filters do not apply.

## `maxResults` (type: `integer`):

Maximum jobs collected in this snapshot, before only-new filtering.

## `maxPages` (type: `integer`):

Scan at most this many guest pages even when fewer jobs match; currently around ten cards per page.

## `timeRange` (type: `string`):

Source recency filter, also checked against available calendar dates. Not an exact-hour timestamp guarantee.

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

Ordering requested from LinkedIn; the source may rank or recommend results.

## `strictKeywords` (type: `boolean`):

Reject unrelated source recommendations: every keyword word must occur in the title or description, ignoring case.

## `workTypes` (type: `array`):

Pass these filters to LinkedIn. Leave empty for any.

## `jobTypes` (type: `array`):

Pass these filters to LinkedIn. Leave empty for any.

## `experienceLevels` (type: `array`):

Pass these filters to LinkedIn. Leave empty for any.

## `onlyNew` (type: `boolean`):

Persist IDs in your account and omit previously exported jobs from later snapshots.

## `monitorId` (type: `string`):

Use one name per search and separate names for different filters. Avoid overlapping runs.

## `firstRunMode` (type: `string`):

Return initial jobs or seed the baseline without exporting the existing backlog.

## `resetState` (type: `boolean`):

Discard this monitor baseline only. Turn this off again before scheduling.

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

Uses direct HTTP by default. Supply your own proxy configuration if needed. Availability and proxy charges depend on your account.

## Actor input object example

```json
{
  "keywords": "python",
  "location": "Berlin, Germany",
  "maxResults": 10,
  "maxPages": 5,
  "timeRange": "week",
  "sortBy": "date",
  "strictKeywords": true,
  "onlyNew": false,
  "firstRunMode": "emitAll",
  "resetState": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `jobs` (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": "python",
    "location": "Berlin, Germany",
    "maxResults": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("benthepythondev/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 = {
    "keywords": "python",
    "location": "Berlin, Germany",
    "maxResults": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("benthepythondev/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 '{
  "keywords": "python",
  "location": "Berlin, Germany",
  "maxResults": 10
}' |
apify call benthepythondev/linkedin-jobs-scraper --silent --output-dataset

```

## MCP server setup

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