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

Scrape current public LinkedIn job search cards with job IDs, titles, companies, locations, posted dates, and source links without login.

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

## Pricing

from $0.70 / 1,000 public job delivereds

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

Find current public LinkedIn job listings by keyword and location and receive clean, source-linked rows ready for recruiting research, market analysis, and alerts. Enter one to five searches such as `software engineer` in `United States`, choose up to three observed result pages per search, and set a total row limit. Each useful Dataset row contains the numeric LinkedIn job ID, title, company, location, displayed posting age, source-provided calendar date, canonical job URL, exact search URL, query attribution, page offset, observation timestamp, and a source reference.

The Actor reads LinkedIn's public guest job-search HTML directly. It does not require LinkedIn login credentials, cookies, an upstream Actor, or a proxy. It parses search cards only: it does not claim to collect job descriptions, applicant counts, recruiter details, or a complete historical archive. Source failures and incomplete coverage are reported separately from a valid empty search.

Pricing is pay per result: **$0.001 per delivered job** at the base tier, with plan-tier prices of $0.0009 for Bronze, $0.0008 for Silver, and $0.0007 for Gold, Platinum, and Diamond. There is no Actor-start fee and no Dataset-item fee. Only a confirmed useful row triggers `result-found`; diagnostics and zero-result runs carry no Actor result charge. Repeated current listings are billable again because each run is a new observation. Set a positive Max total charge. The Apify API treats a literal zero as the platform default rather than a zero-dollar cap; use a small positive value below one result price when you need a no-source budget check.

![LinkedIn Jobs Scraper workflow](https://api.apify.com/v2/key-value-stores/IwI7VoxszQjT6v69X/records/linkedin-public-jobs-scraper-ffb3bfee6cbbed71-readme-hero.webp?signature=1nIR1EawzxfuTX3feC3k3)

### Input

```json
{
  "searches": [
    { "keywords": "software engineer", "location": "United States" }
  ],
  "maxItems": 30,
  "maxPagesPerSearch": 3
}
```

| Field | Contract |
|---|---|
| `searches` | Required array of 1–5 unique `{ keywords, location }` objects. Both values are required, trimmed, and limited to 100 characters. |
| `maxItems` | Total useful rows across all searches, 1–100. Default: 20. Collection is reduced to the number of rows the verified run budget can afford. |
| `maxPagesPerSearch` | 1–3 guest-search pages. Observed offsets are 0, 10, and 20. Default: 1. |

Unknown input fields, duplicate searches, invalid limits, and control characters are rejected before collection.

### Output

One Dataset item represents one current public job card. This compact row was parsed from the captured `software engineer` / `United States` source page on September 12, 2026:

```json
{
  "schemaVersion": "1.0",
  "recordType": "linkedin_public_job",
  "jobId": "4419969671",
  "jobURL": "https://www.linkedin.com/jobs/view/senior-software-engineer-%E2%80%93-go-golang-at-general-motors-4419969671",
  "title": "Senior Software Engineer – Go (Golang)",
  "company": "General Motors",
  "location": "Warren, MI",
  "postedTimeText": "2 weeks ago",
  "postedDate": "2026-08-22",
  "keywords": "software engineer",
  "searchLocation": "United States",
  "pageStart": 0
}
```

`postedDate` is copied from the card's `datetime` attribute. The Actor does not infer a timestamp from phrases such as “2 weeks ago.” `jobURL` is accepted only when the source anchor uses LinkedIn HTTPS and its path ends in the same numeric `jobId`.

### Coverage and run summary

`OUTPUT` in the default key-value store records the outcome, requested scope, number of delivered jobs, confirmed event count, per-search page observations, failure details, and a bounded snapshot of delivered rows. `coverage.complete` remains `false`: the source is a bounded view of current guest-search cards, not an assertion that LinkedIn has no other matching jobs.

Outcomes have distinct meanings:

- `completed`: one or more rows were delivered within the chosen bounds.
- `empty`: the source responded normally but exposed no qualifying cards.
- `partial`: at least one source request failed after some useful data was collected.
- `source_failed`: every attempted search failed and no job row was delivered.
- `budget_stopped`: the verified charge limit could not fund all requested output.

A missing job from a later observation is not classified as deleted or closed.

### Source and pagination boundaries

The source endpoint is `https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search` with exact `keywords`, `location`, and `start` parameters. On the captured source, offsets 0, 10, and 20 returned 10 cards each and 30 distinct numeric job IDs. This supports the three-page release limit. LinkedIn can change availability, markup, ranking, and returned counts at any time.

Responses must remain on the exact requested URL, be HTML, fit within 2 MiB, and arrive within the shared 110-second run deadline. Requests do not redirect or retry. A challenge page, unexpected content type, HTTP error, timeout, or malformed card set is reported as a source failure. The error summary retains only a safe endpoint path, attempt count, and status when present.

### Deduplication and repeat runs

Within one run, `jobId` is the identity key across pages and searches. If the same job appears more than once, it is emitted once. The first matching search remains its attribution. Separate runs do not share storage or silently suppress output. This makes scheduled repeat observations transparent: current rows are delivered and charged again, and you can compare snapshots in your own database.

### Pricing and budgets

| Tier | Price per confirmed `result-found` |
|---|---:|
| Free | $0.0010 |
| Bronze | $0.0009 |
| Silver | $0.0008 |
| Gold, Platinum, Diamond | $0.0007 |

The Actor verifies the live pricing map and raw run charge limit before requesting LinkedIn. Nonzero start, Dataset, or unknown event prices are rejected. A one-row budget limits source collection to one potential row. Storage and charge acknowledgement are treated as one delivery operation; uncertain delivery stops without an automatic retry.

### Task recipes

**Quick current search** uses one page and up to 10 rows:

```json
{"searches":[{"keywords":"software engineer","location":"United States"}],"maxItems":10,"maxPagesPerSearch":1}
```

**Three observed pages** uses the source-proven offsets and a 30-row bound:

```json
{"searches":[{"keywords":"software engineer","location":"United States"}],"maxItems":30,"maxPagesPerSearch":3}
```

**Repeat observation** uses the same three-page input later. Compare `jobId`, `postedDate`, and your observation time; do not interpret absence as a closure signal.

### API integration

Start the Actor with your Apify API token in the Authorization header, wait for at most 60 seconds in the POST request, then poll the returned run ID with bounded GET requests if it is still running. Never retry a timed-out POST automatically because the server may have accepted it. Read clean Dataset items from the `defaultDatasetId` in the terminal run object and read `OUTPUT` from `defaultKeyValueStoreId`.

```js
const actorId = 'ACTOR_ID_AFTER_PUBLICATION';
const response = await fetch(`https://api.apify.com/v2/acts/${actorId}/runs?waitForFinish=60`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.APIFY_TOKEN}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ searches: [{ keywords: 'software engineer', location: 'United States' }], maxItems: 10, maxPagesPerSearch: 1 }),
  signal: AbortSignal.timeout(75_000)
});
if (!response.ok) throw new Error(`Run start failed: ${response.status}`);
const run = (await response.json()).data;
```

Keep the token outside source code and logs. Check every HTTP status and terminal run status before treating data as complete.

### Field dictionary

| Field | Meaning |
|---|---|
| `jobId` | Numeric ID from `urn:li:jobPosting` on the card. |
| `jobURL` | Canonical source anchor after query parameters are removed. |
| `title`, `company`, `location` | Visible card text, whitespace normalized. |
| `postedTimeText` | Source-visible relative age such as `23 hours ago`. |
| `postedDate` | Exact `YYYY-MM-DD` card attribute; no inferred time of day. |
| `keywords`, `searchLocation` | Query that first produced the job in this run. |
| `pageStart` | Guest-search offset: 0, 10, or 20. |
| `sourceURL` | Exact requested public guest-search URL. |
| `observedAt` | UTC time when the page was parsed. |
| `source.htmlSha256` | SHA-256 fingerprint of the bounded HTML response. |

### Practical uses

Recruiting teams can monitor newly visible roles for a fixed set of competitors. Labor-market analysts can compare current title and location mixes. Sales teams can use company/title signals as research inputs while keeping the LinkedIn source URL for verification. Each use should retain observation timestamps and treat the feed as a bounded sample of current public search cards.

### Limits and responsible use

The Actor works only with public guest-search pages. It does not log in, bypass access controls, collect profiles, contact people, or enrich personal data. Source availability is outside the Actor's control. Use results in line with LinkedIn's terms and applicable law, and avoid decisions about individuals based only on a search card.

### FAQ

**Does it fetch full job descriptions?** No. This release parses search cards only.

**Does three pages mean all matching jobs?** No. It means up to three observed offsets. Ranking and availability can change.

**Why did a run return fewer than 10 rows?** The source may expose fewer cards, duplicate a job, provide a malformed card, hit the row limit, or encounter a source failure. Inspect `OUTPUT.coverage`.

**Are repeated jobs free?** No. Every useful row delivered in a new observation is billable.

**Does an absent job mean it closed?** No. Absence from a bounded search result is not closure evidence.

![From query to verified job rows](https://api.apify.com/v2/key-value-stores/IwI7VoxszQjT6v69X/records/linkedin-public-jobs-scraper-c1ad1001a7c8ad86-readme-workflow.webp?signature=1RdndOnPMGFlYKZz08ygA)

# Actor input Schema

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

One to five public LinkedIn job searches. Each search needs keywords and a location.

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

Maximum useful job rows delivered across all searches.

## `maxPagesPerSearch` (type: `integer`):

Read one to three observed guest-search pages per search. Each page normally exposes up to ten cards.

## Actor input object example

```json
{
  "searches": [
    {
      "keywords": "software engineer",
      "location": "United States"
    }
  ],
  "maxItems": 20,
  "maxPagesPerSearch": 1
}
```

# Actor output Schema

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

One billable linkedin\_public\_job row per useful delivered search card.

## `summary` (type: `string`):

Bounded source coverage, failures, delivery state, and a snapshot of delivered rows only.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("zinin/linkedin-public-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("zinin/linkedin-public-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 '{}' |
apify call zinin/linkedin-public-jobs-scraper --silent --output-dataset

```

## MCP server setup

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