LinkedIn Jobs Scraper — LinkedIn Job Postings, Salary, No Login
Pricing
from $1.50 / 1,000 job listings
LinkedIn Jobs Scraper — LinkedIn Job Postings, Salary, No Login
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.
Pricing
from $1.50 / 1,000 job listings
Rating
0.0
(0)
Developer
Steadyfetch Team
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
a day ago
Last modified
Categories
Share
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" }—queriesis the one field you have to set. - Cap the bill: set
maxTotalChargeUsdon 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.
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-scraperRequired: queries (array of job titles/keywords, or full LinkedIn jobs-search URLs)Optional: jobUrls (array of LinkedIn job links you already hold — each isfetched whole: description, salary block, criteria, for ONEjob-listing charge and no description charge)location (string, plain text: "London", "United States", "Dubai")postedWithinDays (1, 7 or 30 — anything else, 90 included, earns anuncharged 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 alreadyin it is skipped and never charged again)includeSeen (boolean, default false — OFF: a posting already delivered toyour account is skipped, uncharged. ON: it comes back markedrepeat:true with firstSeenRunId, still uncharged)Charges: job-listing once per delivered job listingjob-description once per DELIVERED full description — withincludeDescription: true on the search door, and always on apasted jobUrls link, which IS a request for the whole posting.A description that does not come back is never chargedNote: 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
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
{"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.
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 |
|---|---|
| this actor | |
| Indeed | Indeed Jobs Scraper — Indeed API Alternative, Salary, Vacancies |
| A company's own career site (paste a domain) | Career Page Scraper — Company Jobs by Domain, 9 ATS Boards |
| Glassdoor, with the employer rating | Glassdoor Jobs Scraper — Glassdoor API Alternative + Ratings |
| Several boards at once, de-duplicated | Multi Job Board Scraper — Career Pages, Listings Aggregator |
| Google Jobs, with the full panel and apply links | Google Jobs Scraper — Google for Jobs Listings, Apply Links |
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 · Google Ads video · TikTok · LinkedIn · Google Ads text & OCR |
| Trends & keywords | Google Trends · Trends Now · Breakout keywords · Autocomplete keywords · Keyword volume & CPC · Social trends |
| YouTube transcripts | YouTube videos · YouTube channels |
| Reel transcripts · Profile posts | |
| Jobs | LinkedIn · Indeed · Career sites by domain · Glassdoor · Multi-board · Google Jobs |
| Amazon | Products · Search · Bestsellers · Sellers |
| Any media file | Speech to Text · any link or file |
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.