# LinkedIn Jobs Scraper — LinkedIn Job Postings, Salary, No Login (`steadyfetch/linkedin-jobs-scraper`) Actor

LinkedIn jobs by keyword and location, no login and no cookies. Every row: title, company, location, posted date, the public job link. From $1.50 per 1,000 job listings, never for a job we did not deliver. The full description, salary and seniority are opt-in, and charged as a second event per job.

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

## Pricing

from $1.50 / 1,000 job listings

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 — LinkedIn Job Postings, Salary, No Login

**No login, no cookies, no session to keep alive.** This actor reads LinkedIn's public job
pages — the ones an ordinary browser sees without an account — and hands you one row per
posting: title, company, location, posted date and the public job link. Ask for the full
description and you also get the description text, the salary range LinkedIn shows on about a
third of postings, and its four criteria (seniority, employment type, job function, industries)
— charged as a **second event per job, a flat $0.003 each**, and only when the description
actually arrives. It is off by default, so a run you start without it is the per-listing price and
nothing else.

**Never pay for a job we didn't deliver.** Set a row cap and a deadline and the run stops
exactly there — no runaway bills.

**Using an AI agent?** Pin this actor in Apify's MCP server with one link: `https://mcp.apify.com?tools=steadyfetch/linkedin-jobs-scraper`

- **Actor id:** `steadyfetch/linkedin-jobs-scraper`
- **Input:** `{ "queries": ["software engineer"], "location": "United States" }` — `queries` is the one field you have to set.
- **Cap the bill:** set `maxTotalChargeUsd` on the run (a run option, not Actor input), e.g. `0.50` — the run stops when it reaches it.

**From $1.50 per 1,000 job listings** — all-inclusive pay per event, nothing charged for
starting a run, charged only on delivery.

**Just want to see it work?** Run it with no input at all (`{}` from the API or an AI agent)
and you get a real 5-posting sample — "software engineer" in the United States — charged like
any run, a few cents at most. Set only the filters — a location, a date window, a row cap —
and no search terms, and that same sample runs under them, charged like any run; a limit
larger than the sample's own is capped at it, and one extra uncharged row names which
settings were yours.

**Running the same search again?** A posting already delivered to your account is never
charged a second time — see [Never pay for the same posting twice](#never-pay-for-the-same-posting-twice).

> **Unofficial.** This actor is not affiliated with, endorsed by, or sponsored by LinkedIn.
> "LinkedIn" and all related marks are trademarks of LinkedIn Corporation. It reads only the
> public job pages an ordinary browser sees, with no account and no cookie.

Every row carries `charged` and `missReason`, so you can reconcile the invoice from the
dataset itself without opening the console. **Only rows with `charged: true` were billed.**

***

### Agent / API paste-block

```
Actor:      steadyfetch/linkedin-jobs-scraper
Required:   queries        (array of job titles/keywords, or full LinkedIn jobs-search URLs)
Optional:   jobUrls            (array of LinkedIn job links you already hold — each is
                                fetched whole: description, salary block, criteria, for ONE
                                job-listing charge and no description charge)
            location           (string, plain text: "London", "United States", "Dubai")
            postedWithinDays   (1, 7 or 30 — anything else, 90 included, earns an
                                uncharged row and is never rounded)
            jobType            (full-time | part-time | contract | temporary | internship)
            sortBy             (relevance | date)
            includeDescription (boolean, default false — fetch every posting's full text)
            maxItems           (integer, default 50 — hard cap for the whole run;
                                up to 10,000 per run)
            maxRunSeconds      (integer, default 900 — clean stop, never a timeout kill;
                                30 to 3,600 seconds)
            resumeFromDatasetId (string — an earlier run's dataset ID; every posting already
                                in it is skipped and never charged again)
            includeSeen        (boolean, default false — OFF: a posting already delivered to
                                your account is skipped, uncharged. ON: it comes back marked
                                repeat:true with firstSeenRunId, still uncharged)
Charges:    job-listing      once per delivered job listing
            job-description  once per DELIVERED full description — with
                             includeDescription: true on the search door, and always on a
                             pasted jobUrls link, which IS a request for the whole posting.
                             A description that does not come back is never charged
Note:       sending null (or an empty value) for a field is the same as leaving it out —
            the default is used, so templated callers need no special-casing.
Token:      https://console.apify.com/settings/integrations
```

```bash
curl -X POST "https://api.apify.com/v2/acts/steadyfetch~linkedin-jobs-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"queries":["software engineer","data analyst"],"location":"United Kingdom","maxItems":30}'
```

***

### What a delivered row looks like

```json
{
  "source": "linkedin",
  "sourceJobId": "4447220072",
  "jobUrl": "https://www.linkedin.com/jobs/view/4447220072",
  "title": "Software Engineer",
  "company": "Blossom",
  "companyUrl": "https://www.linkedin.com/company/join-blossom-health",
  "location": { "raw": "New York, NY", "city": "New York", "region": "NY", "postcode": null, "country": "US" },
  "postedAt": "2026-09-08T00:00:00.000Z",
  "postedRelative": "2 weeks ago",
  "employmentType": "Full-time",
  "seniority": "Not Applicable",
  "jobFunction": "Engineering and Information Technology",
  "industries": "Hospitals and Health Care",
  "salaryText": "$150,000.00/yr - $220,000.00/yr",
  "applicants": 200,
  "applicantsText": "Over 200 applicants",
  "activelyHiring": true,
  "query": "software engineer",
  "searchLocation": "United States",
  "dedupeKey": "linkedin:4447220072",
  "charged": true,
  "chargedDescription": true,
  "missReason": null
}
```

Plus **one search row per search term** — `query`, `searchLocation`, `delivered`,
`pagesWalked`, `outcome` — and **one summary row** with `delivered`, `stoppedBy`,
`resumeCursor`, `datasetId` (paste it into `resumeFromDatasetId` to continue this run later),
`carriedOver` and `chargedEvents`.

`jobUrl` is the canonical LinkedIn link built from the posting's own id: identical across
runs, with no `refId`, `trackingId` or position token in it, so it is safe to use as a join
key.

**About `location`.** LinkedIn prints one location line per posting, and we parse it:
`location.raw` is exactly what LinkedIn printed, and `location.city`, `location.region`,
`location.postcode` and `location.country` are what it means. **`location.country` is a real
ISO-2 country code or `null` — never a state and never a postcode.** When the line names no
country we can prove, `country` is `null` rather than a guess — the full line is always in
`location.raw`, so nothing is lost. The row's `searchLocation` is the text *you* searched in,
kept separate from what LinkedIn printed on the posting.

***

### How many postings one search returns

**Up to 1,000, ten at a time.** LinkedIn serves ten postings per result page and stops at
1,000 results for any one search — that ceiling is LinkedIn's, not ours. So one search term
can fill a large `maxItems` on its own, and the search row tells you how many result pages it
walked. Past 1,000 for a single term, more rows come from more search terms or a narrower
location, not from a bigger row cap. The summary row says which limit bound the run.

**One thing worth knowing before you schedule a new search term:** LinkedIn does not answer
"no matches". Give it a keyword nothing matches and it broadens the search and still returns
ten postings — so check the first rows of a new term before you put it on a schedule. An
*empty* answer from this actor means the result list ran out, never "your keyword matched
nothing".

### Descriptions: what the option adds

Every row from a search carries what LinkedIn puts on the result card: title, company,
location, posted date, the public link and whether the company is actively hiring. **Salary is
not on the card** — LinkedIn only shows it on the posting's own page.

**"Fetch the full description for every job"** (`includeDescription`, off by default) fetches
each posting's own public page and adds the description text, the four criteria and the salary
block when LinkedIn shows one (measured on 20 harvested postings: all 20 carried the
description and the four criteria, 7 carried a salary block). The honesty rules are the same
as everywhere else here:

- a description is charged **only when it actually comes back** — every row carries
  `chargedDescription`, so the invoice reconciles from the dataset;
- we read the description out of the posting's own page before charging: a page that answers
  without one is an uncharged miss, never a billed row;
- a description we could not fetch is reported **on its own row**, uncharged, and the listing
  itself is unaffected;
- if the option is ever not active on a run, that run tells you once in its log and charges
  nothing extra.

The per-description price is on the Pricing tab, as the **Full description** charge.

**Already hold the links?** Put them in `jobUrls` and each one is fetched whole — description,
salary block, criteria. **A pasted link is billed like a searched one: listing + description.**
A pasted link is a request for the whole posting, so it is priced the same way whichever door it
came through, and `includeDescription` does not change it either way. If LinkedIn serves the
posting page without a description on it, you are charged for the listing only, the description
column stays empty rather than holding something invented, and the row says so.

### Postings change daily

LinkedIn's public job lists move every day. **Schedule it with `postedWithinDays: 1`** and each
run reads only what appeared since yesterday; the repeat memory below then keeps the second run
cheap, because anything you already have is skipped before it takes a row slot or a charge. A
scheduled run started with a scoped API token needs key-value store permission for that — see
[Never pay for the same posting twice](#never-pay-for-the-same-posting-twice).

### What you are never charged for

| situation | `missReason` | run status |
|---|---|---|
| LinkedIn rate-limited us, and was still rate-limiting after we waited | `THROTTLED` | SUCCEEDED — re-runnable, and the row says so |
| LinkedIn redirected the request to its sign-in page | `CHALLENGED` | SUCCEEDED — re-runnable |
| LinkedIn returned a page we could not read as results | `SOURCE_UNAVAILABLE` | SUCCEEDED — re-runnable |
| The result list ran out | `CLEAN_EMPTY` | SUCCEEDED — a real answer |
| A posting you linked to has been taken down | `GONE` | SUCCEEDED — a real answer, and re-running will not change it |
| A posting your account already has from an earlier run | — | skipped, never charged twice (needs key-value store permission on a scoped token) |
| A search term or link we could not read | `USER_INPUT` | SUCCEEDED — the row names the fix |
| A full description that could not be fetched | *(reported on its job row)* | SUCCEEDED — the listing still ships; no Full description charge |
| Your own `maxItems` / `maxRunSeconds` / cost cap | `STOPPED_AT_LIMIT` | SUCCEEDED — summary row with `resumeCursor` and the `datasetId` to continue from |

We never guess. A rate limit is reported as a rate limit, not as "no jobs found" — a temporary
problem is never dressed up as a permanent verdict, and it is never charged. Only a 404 from
LinkedIn's own posting endpoint is ever read as "this posting is gone".

**This actor may fail when LinkedIn changes things.** The one you are most likely to see is
`THROTTLED`. Before it hands that row back, a run that still has real time left waits a minute
and a half to three and tries the search again on fresh routes — up to three passes — so a
limit that lifts within minutes delivers postings instead of a re-run request; the row then
says how long the run waited and how many minutes were left. A short run makes its one pass.

### Never pay for the same posting twice

Every run remembers the postings it delivered for your account, in a key-value store called
`linkedin-watch-account` in your own Apify account. Run the same searches again and what you
already have is **skipped before it takes a row slot**: no row, no charge, and no bite out of
your row cap. The store lives in your own Apify account, so a run started with a scoped API
token in restricted-access mode needs key-value store **Read, Write and Create** permission (or
Actor runs set to **Full access**) — without it the run says so on its status line and on an
uncharged first row, and it cannot skip what you already have (Write is what lets the run
remember what it delivered; without it every run pays again). The status line says how many
(*18 already in your account, skipped, not charged*) and `OUTPUT.skippedSeen` holds the number. That also means no second **Full
description** fetch and no second description charge for a posting you already own the text of.

Want one complete dataset per run instead of only what changed? Turn on **Include listings you
already have** (`includeSeen: true`) and those rows come back anyway — marked `repeat: true`
with `firstSeenAt` and `firstSeenRunId` naming the run that first delivered them,
`charged: false`.

- **The store lives in your account**, one record (`SEEN`). Open it in *Storage → Key-value
  stores* to see everything the actor has delivered you, or delete the record to start over.
  This actor runs with limited permissions and touches nothing else in your account.
- **Entries older than 90 days count as new again**, so a posting you collected last quarter is
  collected (and charged) again if it is still live.
- **Only delivered rows are remembered.** A rate-limited page or a search that ran out leaves
  nothing behind, so the next run tries it again.
- **If the store cannot be read on some run**, the run still runs: it delivers and charges as
  usual and the status line says the repeat check was unavailable, so you know a repeat may
  have been charged that once. If the reason is the token's own permissions, the status line
  says that instead and names the permission to grant — set it under *Settings → API &
  Integrations* on the token, or start the run from the console.

### Limits are hard limits

`maxItems` and `maxRunSeconds` stop the run exactly where you set them. **Ask for more than
this actor can do and the run still starts.** One run delivers up to 10,000 listings and runs
30 to 3,600 seconds. A larger (or smaller) number is not an error: the run continues at the
nearest limit and writes one extra uncharged row saying what you asked for and what bound it
— you are never left with no run at all.
The run still finishes
**successfully**, the summary row names which limit bound, and `resumeCursor` lists the
searches that were not reached and the result-page offset the walk stopped at. A time limit
ends the *collecting*, never the delivering: a result page already fetched when the clock runs
out is still delivered in full, so a run never pays for postings it does not hand you.

To collect the rest without paying again for what you already have, re-run the same input with
a higher cap and `resumeFromDatasetId` set to that run's dataset ID (it is on the summary row
as `datasetId`): every posting already in that dataset is skipped — never charged — and the new
summary's `carriedOver` says how many. A dataset ID that cannot be read stops the run before it
fetches anything, uncharged.

They are limits on the **run**, not on a container. If Apify moves your run to another server
part-way through, or you resurrect a finished run, it picks up from what is already in your
dataset: the same posting is never delivered twice and never charged twice, and `maxItems`
still means what you typed.

### FAQ

**Do I need a LinkedIn account or a cookie?**
No. This actor reads only the public job pages, with no account, no cookie and no session.

**Can I paste a LinkedIn search URL?**
Yes, in `queries`. A pasted search keeps its own keywords, location and filters. A link to a
single posting belongs in `jobUrls` — paste one into `queries` and you get a guidance row
pointing you at the right field, uncharged.

**What does a pasted job link cost?**
The same as finding that posting through a search: one job listing plus one full description,
because a link is a request for the whole posting. `includeDescription` does not change it —
that setting governs the search door. If the page comes back without a description, you pay for
the listing only.

**Why is `salaryText` empty on so many rows?**
Because LinkedIn did not show one. Salary is never on the result card, and on the posting page
it appears on roughly a third of postings. When there is none, the field stays `null` rather
than being estimated.

**Why is `applicants` sometimes null when `applicantsText` is not?**
Because LinkedIn writes three different things there. "193 applicants" and "Over 200
applicants" are counts and we report them; "Be among the first 25 applicants" is a threshold
LinkedIn is advertising, not a count, so `applicants` stays `null` and the verbatim caption
ships in `applicantsText`. We would rather give you the sentence than invent a number.

**Does `{}` charge anything?**
Yes — like any run, and a few cents at most. With no input at all the run is a real 5-posting
sample ("software engineer" in the United States) and you pay for the postings it delivers. Set
only filters and no search terms and the same sample runs under them, charged the same way. If
LinkedIn cannot be read at that moment you get one uncharged row saying so, never an empty
result.

***

### Run it from an AI agent (MCP)

Works out of the box from any MCP client (Claude, Cursor, …) via Apify's MCP server — ask for
"the newest data engineer jobs posted on LinkedIn in Berlin this week" and point it at this
actor.

The one-link pin is at the top of this page.

### Steadyfetch jobs suite

One actor per source, one job each, the same row shape — so a pipeline built on one source
takes the next without a rewrite. All-inclusive pay per event, nothing charged for starting a
run, charged only on delivery.

| Source | Actor |
|---|---|
| LinkedIn | **this actor** |
| Indeed | [Indeed Jobs Scraper — Indeed API Alternative, Salary, Vacancies](https://apify.com/steadyfetch/indeed-jobs-scraper) |
| A company's own career site (paste a domain) | [Career Page Scraper — Company Jobs by Domain, 9 ATS Boards](https://apify.com/steadyfetch/company-jobs-by-domain) |
| Glassdoor, with the employer rating | [Glassdoor Jobs Scraper — Glassdoor API Alternative + Ratings](https://apify.com/steadyfetch/glassdoor-jobs-scraper) |
| Several boards at once, de-duplicated | [Multi Job Board Scraper — Career Pages, Listings Aggregator](https://apify.com/steadyfetch/multi-job-board-scraper) |
| Google Jobs, with the full panel and apply links | [Google Jobs Scraper — Google for Jobs Listings, Apply Links](https://apify.com/steadyfetch/google-jobs-scraper) |

**The rest of the steadyfetch shelf** — same contract everywhere: all-inclusive pay per event,
nothing charged for starting a run, charged only on delivery.

| Family | Actors |
|---|---|
| Ad creative intelligence | [Facebook](https://apify.com/steadyfetch/facebook-ads-transcript-scraper) · [Google Ads video](https://apify.com/steadyfetch/google-ads-video-transcript-scraper) · [TikTok](https://apify.com/steadyfetch/tiktok-ads-transcript-scraper) · [LinkedIn](https://apify.com/steadyfetch/linkedin-ads-transcript-scraper) · [Google Ads text & OCR](https://apify.com/steadyfetch/google-ads-creative-text-scraper) |
| Trends & keywords | [Google Trends](https://apify.com/steadyfetch/google-trends-scraper) · [Trends Now](https://apify.com/steadyfetch/google-trends-now-scraper) · [Breakout keywords](https://apify.com/steadyfetch/breakout-keywords-scraper) · [Autocomplete keywords](https://apify.com/steadyfetch/google-keyword-suggest-scraper) · [Keyword volume & CPC](https://apify.com/steadyfetch/keyword-search-volume-scraper) · [Social trends](https://apify.com/steadyfetch/social-trends-scraper) |
| YouTube transcripts | [YouTube videos](https://apify.com/steadyfetch/youtube-transcript-scraper) · [YouTube channels](https://apify.com/steadyfetch/youtube-channel-transcripts) |
| Instagram | [Reel transcripts](https://apify.com/steadyfetch/instagram-reel-transcript-scraper) · [Profile posts](https://apify.com/steadyfetch/instagram-profile-posts) |
| **Jobs** | [LinkedIn](https://apify.com/steadyfetch/linkedin-jobs-scraper) · [Indeed](https://apify.com/steadyfetch/indeed-jobs-scraper) · [Career sites by domain](https://apify.com/steadyfetch/company-jobs-by-domain) · [Glassdoor](https://apify.com/steadyfetch/glassdoor-jobs-scraper) · [Multi-board](https://apify.com/steadyfetch/multi-job-board-scraper) · [Google Jobs](https://apify.com/steadyfetch/google-jobs-scraper) |
| Amazon | [Products](https://apify.com/steadyfetch/amazon-product-scraper) · [Search](https://apify.com/steadyfetch/amazon-search-scraper) · [Bestsellers](https://apify.com/steadyfetch/amazon-bestsellers-scraper) · [Sellers](https://apify.com/steadyfetch/amazon-seller-scraper) |
| Any media file | [Speech to Text · any link or file](https://apify.com/steadyfetch/media-transcriber) |

### Feedback & support

Found an issue? Open it on the **Issues tab** — we usually reply within a couple of hours,
always within a day. Feature requests welcome.

If it earned its keep, a rating helps other buyers find it, and saving the actor keeps it one click away.

# Actor input Schema

## `queries` (type: `array`):

Job titles or keywords (for example "software engineer"). You can also paste a full LinkedIn jobs search URL such as https://www.linkedin.com/jobs/search?keywords=data%20analyst\&location=London — a pasted search keeps its own keywords, place and filters. No result fee for a removed or blocked posting, a filtered row, or a posting this account already has. Note that LinkedIn never answers a keyword with nothing: a term it cannot match is BROADENED and still returns jobs, so read the titles that come back before trusting a rare keyword. An Indeed search is steadyfetch/indeed-jobs-scraper; a company's own careers page is steadyfetch/company-jobs-by-domain. Called with no input at all (an empty {} from the API or an agent), the run is a 5-listing sample — "software engineer" across the United States — charged like any run, and setting only the filters below leaves that sample running under them.

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

LinkedIn job links you already have, fetched as postings directly instead of being searched for — for example https://www.linkedin.com/jobs/view/data-analyst-at-acme-3912345678 or https://www.linkedin.com/jobs/search?currentJobId=3912345678. One row per link, and a link always brings the WHOLE posting back: description, salary block where the employer published one, seniority and employment type. Because a link is a request for the whole posting, it is billed exactly like a posting found through a search — one job listing plus one Full description — and "Fetch the full description" below does not change that either way. If LinkedIn serves the page without a description on it, you are charged for the listing only and the row says so. A link that is not a LinkedIn job posting is reported on its own row and never charged.

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

Where to look, exactly as you would type it on LinkedIn — a city ("London"), a country ("United States"), a region ("Greater Dubai Area"). LinkedIn resolves the place itself, so any wording it understands works. Leave empty to search everywhere.

## `postedWithinDays` (type: `integer`):

Only jobs posted in the last 1, 7 or 30 days — the three windows LinkedIn's public job search offers. Any other number, 90 included, is reported on an uncharged row and the search is not run, because rounding it to the nearest window would answer a different question. Leave empty for every posting date.

## `jobType` (type: `string`):

Keep only one employment type. The value or the name both work — "full-time", "Full-time" and "F" are the same filter. Leave empty for every job type.

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

Ask LinkedIn to order results by date instead of relevance. The value or the name of the order both work — date and "Date posted" are the same sort.

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

Off by default. When on, each delivered full description costs one extra Full description charge of $0.003 per job, on top of the per-listing price. It governs jobs found by SEARCH only — a link pasted into "Job links" already asks for the whole posting and is billed for it whatever this is set to. The extra charge is taken only when a description actually comes back; a description that cannot be fetched is reported on its row and never charged. If this option is not yet active on this listing, the run says so and charges nothing extra.

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

Hard cap on delivered job listings across every search in this run. This actor delivers up to 10,000 listings in one run; ask for more and the run continues at 10,000, with one uncharged row saying so.

## `maxRunSeconds` (type: `integer`):

The run stops cleanly before this many seconds and reports what is left, instead of being killed by a timeout. A time limit ends the collecting, never the delivering: jobs already in hand are still handed back. A run is given between 30 and 3,600 seconds; ask for more or less and the run continues at whichever of the two is nearer, with one uncharged row saying so.

## `includeSeen` (type: `boolean`):

OFF (default): a job already delivered to your account is skipped before it takes a row slot — not delivered again, not charged, not counted against your row cap. ON: those rows come back anyway from the search page, marked `repeat: true` with `firstSeenAt` and `firstSeenRunId` naming the run that first delivered them, and still not charged. A handed-back row carries no full description: that text was delivered with it in the earlier run and is not fetched (or charged) a second time, and the row says so.

## `resumeFromDatasetId` (type: `string`):

The default dataset ID of an earlier run of this actor. Leave empty to start fresh. If the dataset cannot be read, the run stops before fetching anything and charges nothing, rather than risk billing you twice.

## Actor input object example

```json
{
  "queries": [
    "software engineer"
  ],
  "sortBy": "relevance",
  "includeDescription": false,
  "maxItems": 25,
  "maxRunSeconds": 900,
  "includeSeen": false
}
```

# Actor output Schema

## `jobs` (type: `string`):

One row per delivered job listing: title, company, location, posted date and the public LinkedIn job link. With "Fetch the full description" on, the row also carries the description, the salary block LinkedIn shows on about a third of postings, and its four criteria (seniority, employment type, job function, industries). `location` is LinkedIn's own location line parsed into city, region, postcode and `location.country`, which is a real ISO-2 country code or null and never a state or a postcode. Every row carries `charged` and `missReason`, so the invoice reconciles from the dataset itself. Only rows with charged = true were billed. A row whose `chargedDescription` is false was NOT billed a Full description charge, and its `statusReason` says why.

## `searches` (type: `string`):

One row per search term: how many jobs it delivered, how many result pages it walked, or why it delivered none. Searches that were refused or ran out of postings are never charged, and neither is a posting LinkedIn has taken down.

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

Delivered count, searches run, result pages walked, uncharged misses, what stopped the run, and the charged-event totals.

## `errors` (type: `string`):

Present only when LinkedIn answered a request with a rate limit, a sign-in redirect or an unreadable page: the search and the reason. These are re-runnable, not permanent.

# 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 = {
    "queries": [
        "software engineer"
    ],
    "maxItems": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadyfetch/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 = {
    "queries": ["software engineer"],
    "maxItems": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("steadyfetch/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 '{
  "queries": [
    "software engineer"
  ],
  "maxItems": 25
}' |
apify call steadyfetch/linkedin-jobs-scraper --silent --output-dataset

```

## MCP server setup

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