LinkedIn Jobs Scraper — LinkedIn Job Postings, Salary, No Login avatar

LinkedIn Jobs Scraper — LinkedIn Job Postings, Salary, No Login

Pricing

from $1.50 / 1,000 job listings

Go to Apify Store
LinkedIn Jobs Scraper — LinkedIn Job Postings, Salary, No Login

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

Steadyfetch Team

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

0

Monthly active users

a day ago

Last modified

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" }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.

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
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 termquery, 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

situationmissReasonrun status
LinkedIn rate-limited us, and was still rate-limiting after we waitedTHROTTLEDSUCCEEDED — re-runnable, and the row says so
LinkedIn redirected the request to its sign-in pageCHALLENGEDSUCCEEDED — re-runnable
LinkedIn returned a page we could not read as resultsSOURCE_UNAVAILABLESUCCEEDED — re-runnable
The result list ran outCLEAN_EMPTYSUCCEEDED — a real answer
A posting you linked to has been taken downGONESUCCEEDED — a real answer, and re-running will not change it
A posting your account already has from an earlier runskipped, never charged twice (needs key-value store permission on a scoped token)
A search term or link we could not readUSER_INPUTSUCCEEDED — 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 capSTOPPED_AT_LIMITSUCCEEDED — 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.

SourceActor
LinkedInthis actor
IndeedIndeed 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 ratingGlassdoor Jobs Scraper — Glassdoor API Alternative + Ratings
Several boards at once, de-duplicatedMulti Job Board Scraper — Career Pages, Listings Aggregator
Google Jobs, with the full panel and apply linksGoogle 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.

FamilyActors
Ad creative intelligenceFacebook · Google Ads video · TikTok · LinkedIn · Google Ads text & OCR
Trends & keywordsGoogle Trends · Trends Now · Breakout keywords · Autocomplete keywords · Keyword volume & CPC · Social trends
YouTube transcriptsYouTube videos · YouTube channels
InstagramReel transcripts · Profile posts
JobsLinkedIn · Indeed · Career sites by domain · Glassdoor · Multi-board · Google Jobs
AmazonProducts · Search · Bestsellers · Sellers
Any media fileSpeech 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.