LinkedIn Jobs Search Scraper: No Login, Only New Postings
Pricing
from $1.14 / 1,000 job posting returneds
LinkedIn Jobs Search Scraper: No Login, Only New Postings
LinkedIn jobs scraper without login or cookies: search by keyword and location, posted in the last 24 hours or week, remote and Easy Apply filters, only new postings since the last run, and hiring signals per company for sales leads. One row per posting, full description on request. Pay per posting.
Pricing
from $1.14 / 1,000 job posting returneds
Rating
0.0
(0)
Developer
Adrian Voss
Maintained by CommunityActor stats
1
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
LinkedIn Jobs Search Lookup: LinkedIn Jobs Scraper by Keyword & Location
You write job searches the way you'd type them into LinkedIn — data engineer @ Berlin, Germany —
and this actor runs each one against LinkedIn's public, logged-out job board and returns one
clean row per posting: title, company, location, posted date, and a permanent link. Turn on the
description option and each row also carries the full posting text, the pay range when the posting
states one, seniority, employment type, job function, industry and applicant count.
No login. No cookies. No LinkedIn account or session token of any kind — see the legal note at the bottom of this page.
Who it's for
The accountable_eel catalogue sells company and hiring intelligence columns for outbound and recruiting. Each actor takes a list of identifiers — domains, company slugs, and here, job searches — and returns one flat, stably-named row per result: the shape a Clay table, an n8n workflow, or an AI agent can consume without post-processing. Pricing is pay-per-event: a fraction of a cent for a row you actually got, and nothing at all for a search that finds nothing. No seat licence, no monthly minimum, no credit system to decode.
This actor is the wide-net end of that catalogue. The ATS lookups in the same family
(greenhouse-jobs-lookup, lever-jobs-lookup, ashby-jobs-lookup and friends) answer "what is
this company hiring for" — you bring the company list. This one answers "who is hiring for this
role, here" when you don't have the list yet.
Why this one
- Every filter it offers is a filter that works. LinkedIn's logged-out search endpoint accepts
a great many
f_*parameters and quietly ignores most of them. Measured on 2026-08-24 by running each one 4–6 times and comparing the returned job IDs against an unfiltered search: date-posted, Easy Apply, location, geo ID and company ID are real; job type, experience level, remote, salary band, industry and sort order return the identical result set to no filter at all. So keywords, location and posted-date go to LinkedIn, and role/company/location/remote narrowing happens here on the postings after they arrive — and the page tells you which is which. Paste a LinkedIn search URL that carried filters LinkedIn drops, and the row'signoredSearchFilterscolumn names them instead of pretending they applied. - You are never billed for the same posting twice. LinkedIn hands out ten results at a time, reshuffles between requests, and repeats postings across neighbouring pages — two requests at offsets ten apart shared half their results on one measurement. Every posting is deduplicated by its LinkedIn job ID across the pages of a search and, by default, across every search in the run. On a per-posting price, that is a billing guarantee, not a tidiness feature.
- The description hop is opt-in and separately priced. Opening every posting's own page is one extra visit per posting and roughly twenty times the bandwidth. It's off by default, and when it's on you're charged for it only on postings whose page actually came back.
- Partial results survive a rate limit. If LinkedIn throttles the run halfway through a
100-posting search, you get the postings collected so far, flagged with
blockedWhilePaging, instead of losing the whole search to a retry. - Three input shapes.
keywords @ location, bare keywords with a location set once for the whole run, or a LinkedIn jobs search URL pasted straight out of your browser's address bar.
What you get
One row per job posting by default. (Turn off "One row per job posting" in the Input tab to get one
row per search instead, with the whole posting list nested in jobs.) Every row carries these
fields, whether or not you turned on filters or descriptions — the columns never move:
| Field | Type / format | Description |
|---|---|---|
query | text | The search line you passed in, unchanged. |
found | boolean | true if the search returned at least one posting. false rows are never charged. |
status | text | OK, NOT_FOUND (no public postings for that search), BAD_FORMAT (the line wasn't a search or a LinkedIn URL), or BLOCKED. |
searchKeywords | text | The keywords actually sent to LinkedIn. |
searchLocation | text | The location actually sent to LinkedIn. |
jobCount | number | How many postings this search returned after your filters — this is exactly what you're charged for. |
truncated | boolean | true if more postings were available than you asked for, or if paging stopped early. |
ignoredSearchFilters | array | Filters present on a pasted LinkedIn URL that the public endpoint does not honour. Empty for a normal search line. |
jobs | array | The full posting list. Present in every row; it's what gets expanded into separate rows in "one row per posting" mode. |
jobId | text | LinkedIn's own numeric posting ID — stable, and what deduplication keys on. |
title | text | Job title. |
company | text | Hiring company as LinkedIn names it. |
companyUrl | link | The company's LinkedIn page, with tracking parameters stripped. |
companyLogoUrl | image | Company logo image URL. |
location | text | Location as LinkedIn prints it on the posting (e.g. "Fort Wayne, IN", "United States"). |
remote | boolean | true if the location or title reads as remote. |
postedAt | date (ISO) | The posting date, read from LinkedIn's own datetime attribute — a real calendar date, not a guess back-derived from "4 days ago". |
postedAgo | text | The same thing in LinkedIn's words ("4 days ago", "20 hours ago"). |
jobUrl | link | Permanent public link to the posting, with this run's tracking parameters removed. |
activelyHiring | boolean | true if LinkedIn shows the "Actively Hiring" badge. |
earlyApplicant | boolean | true if LinkedIn says you'd be among the first applicants. |
benefitsText | text | The benefits teaser LinkedIn shows on some cards ("Medical insurance +6 benefits"). Most postings don't have one. |
descriptionFetched | boolean | true if the posting's own page was opened for the fields below. |
description | text | The full posting text, as plain text. |
salaryText | text | The pay range, when the posting states one in its description. |
seniorityLevel | text | e.g. "Associate", "Mid-Senior level". |
employmentType | text | e.g. "Full-time", "Contract". |
jobFunction | text | e.g. "Engineering and Information Technology". |
industries | text | e.g. "Software Development". |
applicantsText | text | e.g. "Over 200 applicants", "Be among the first 25 applicants". Shown on roughly half of postings. |
scrapedAt | date (ISO) | When this actor fetched the row. |
A search that returns no public postings comes back as a single found: false row with a
status/message explaining why, and is never charged. So does a line that isn't a usable search.
Two honest limits, both measured rather than assumed:
- There is no
applyUrl. The Apply button on a logged-out posting opens a LinkedIn sign-up dialog, not an offsite application link — so no such field is offered rather than filled with the sign-up URL.jobUrlis the permanent public link. salaryTextcomes out of the description. There is no structured compensation field on a logged-out posting, so pay is read from the posting text and is null unless you turn descriptions on and the posting actually states a range. A perk written like pay (a learning budget, a signing bonus) is deliberately not reported as salary.
Run it on a schedule
Turn on deltaMode: true (alongside your searches) and this actor returns only postings that are
new since the last run — nothing you've already seen. Name your watchlist with deltaName if
you're running the same searches on more than one schedule and want to keep their memory apart;
leave it blank and one gets derived automatically. Schedule it in Apify (Schedules > Create) or
trigger it from n8n/Make; the delta state lives in a named key-value store, so every schedule run
bills only new rows. See "Monitoring: only new results" below for how the memory works.
Pricing
- Job posting returned: $1.5 per 1,000 job postings
- Full description added: $1 per 1,000 job postings
Plus a $0.00005 start fee per run. Each event above is billed independently, only when it actually returns data — misses (found:false) are never charged.
You're charged per posting returned, not per search — a search that returns 4 postings costs
four, a search that returns none costs nothing, and a BAD_FORMAT line costs nothing. The
description option adds a second, separate charge on top, and only on the postings whose page
actually loaded.
Because you pay per posting, "Most postings to return per search" is your budget control: leave it at 100 and a five-search run costs at most 500 postings' worth. LinkedIn stops serving results at roughly 1,000 per search regardless, so that is the real ceiling per line.
How to use
- In the Apify Console. Open the actor page and click Start — the
searchesfield is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found. - Via the API. Call it directly with a POST request — no Console needed once you have an API token:
curl "https://api.apify.com/v2/acts/accountable_eel~linkedin-jobs-search-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \-X POST \-H "Content-Type: application/json" \-d '{"searches":["marketing manager @ United States"]}'
- On a schedule. Save this actor as an Apify Task with the input you want, then add a Schedule (hourly, daily, weekly) so it runs on its own — no server of your own required.
Paste one search per line. All three of these work, and you can mix them in one run:
data engineer @ Berlin, Germanymarketing managerhttps://www.linkedin.com/jobs/search?keywords=data%20engineer&location=Berlin%2C%20Germany
A line with no @ location uses the location you set once in 🔍 Search settings. A pasted
LinkedIn URL brings its own keywords, location, geo ID, date-posted, Easy Apply and company filters
with it; anything else it carries is dropped and named in ignoredSearchFilters.
🔍 Search settings — these are sent to LinkedIn, so they narrow the search before results are returned, which makes runs cheaper as well as more relevant:
| Input | What it does |
|---|---|
defaultLocation | Location for any line that doesn't name one. Write it as LinkedIn does — "Berlin, Germany", "United States". |
maxJobsPerQuery | Most postings to return per search. Default 100, capped at what LinkedIn will serve (about 1,000). |
postedWithin | Past 24 hours / week / month. |
easyApplyOnly | Only roles you can apply to without leaving LinkedIn. |
fetchDescription | Open each posting for its full text, pay range, seniority, employment type, function, industry and applicant count. |
🎯 Narrow the results — these are applied here, to the postings after they arrive, because LinkedIn's logged-out search ignores the equivalent parameters. They combine with AND across fields and OR within a field:
| Input | What it does |
|---|---|
titleKeywords | Keep only titles containing one of these — ["engineer","designer"]. |
excludeTitleKeywords | Drop titles containing one of these — ["intern","senior"]. Applied after the include list. |
companies | Keep only these companies — partial names match. |
excludeCompanies | Drop these companies — useful for filtering out staffing agencies you already know. |
locations | Narrow a wide search to particular cities or regions. |
remoteOnly | Keep only roles whose location or title reads as remote. |
skipDuplicateJobs | On by default. Each posting is returned, and billed, once per run even if two searches overlap. |
If LinkedIn throttles a run, lower Max concurrency (in ⚙️ Advanced) to 1 and keep the Residential proxy setting on. Rate limits are retried with backoff automatically, and a run that gets limited mid-search returns what it collected rather than failing.
A bulk search example — 26 role/location pairs across common roles and hiring hubs, the kind of list you'd run on a weekly schedule:
{"searches": ["software engineer @ Berlin, Germany","software engineer @ London, United Kingdom","software engineer @ New York, United States","product manager @ San Francisco, United States","product manager @ Amsterdam, Netherlands","data engineer @ Paris, France","data scientist @ Toronto, Canada","marketing manager @ Chicago, United States","marketing manager @ Dublin, Ireland","sales development representative @ Austin, United States","account executive @ Boston, United States","customer success manager @ Madrid, Spain","devops engineer @ Warsaw, Poland","backend engineer @ Stockholm, Sweden","frontend engineer @ Barcelona, Spain","full stack developer @ Lisbon, Portugal","site reliability engineer @ Zurich, Switzerland","recruiter @ Denver, United States","operations manager @ Seattle, United States","financial analyst @ Singapore","business development manager @ Sydney, Australia","UX designer @ Copenhagen, Denmark","content marketing manager @ Vancouver, Canada","solutions engineer @ Munich, Germany","HR business partner @ Milan, Italy","enterprise account executive @ Miami, United States"],"maxJobsPerQuery": 50}
At $1.50 per 1,000 postings returned (this actor's headline rate — see "Pricing" above), this list's worst case is 26 searches × 50 postings = 1,300 postings, under $2 for the whole run — and a quiet week across all 26 searches costs less, because a search that finds nothing is never billed.
Input
{"searches": ["marketing manager @ United States"]}
One search per line. Write it as "keywords @ location", or just the keywords and set a location below, or paste a LinkedIn jobs search URL straight from your browser. Accepted formats: data engineer @ Berlin, Germany, marketing manager, https://www.linkedin.com/jobs/search?keywords=data%20engineer&location=Berlin%2C%20Germany.
Output
| query | found | status | searchKeywords | searchLocation | jobCount | truncated | ignoredSearchFilters | hiringSignals | jobs | jobId | title | company | companyUrl | companyLogoUrl | location | remote | postedAt | postedAgo | jobUrl | activelyHiring | earlyApplicant | benefitsText | descriptionFetched | description | salaryText | seniorityLevel | employmentType | jobFunction | industries | applicantsText | isNew | firstSeenAt | scrapedAt |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| marketing manager @ United States | true | OK | marketing manager | United States | 100 | true | <all postings found (full list)> | 4460494450 | Marketing - Brand Marketing Manager | GOODAI GLOBAL USA INC. | https://www.linkedin.com/company/goodaiglobalus | https://media.licdn.com/dms/image/v2/D560BAQF32eHj1r0-PQ/company-logo_100_100/B56Z77zmlZGUAI-/0/1782341069374/hansung_usa_logo | Irvine, CA | false | 2026-09-03 | 2 weeks ago | https://www.linkedin.com/jobs/view/marketing-brand-marketing-manager-at-goodai-global-usa-inc-4460494450 | false | false | false | 2026-09-20T06:10:12.757Z |
Use it from Clay, n8n, Make, or an AI agent
This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.
curl "https://api.apify.com/v2/acts/accountable_eel~linkedin-jobs-search-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \-X POST \-H "Content-Type: application/json" \-d '{"searches":["marketing manager @ United States"]}'
n8n. Add an HTTP Request node: Method POST, URL https://api.apify.com/v2/acts/accountable_eel~linkedin-jobs-search-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>, Body Content Type JSON, JSON Body {"searches":["marketing manager @ United States"]} (swap in an expression from an earlier node for a real value).
Clay. Add an "HTTP API" column: Method POST, URL https://api.apify.com/v2/acts/accountable_eel~linkedin-jobs-search-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>, Body {"searches":["{{search}}"]}, mapping the row's search into the searches array.
MCP. In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "LinkedIn Jobs Scraper Without Login | Apify" — the agent will find and run this actor.
Monitoring: only new results
Turn on Only return results that are new since the last run and this actor becomes a job monitor. Every posting LinkedIn returns is checked against the job IDs your previous run already delivered, and anything you have seen is dropped before you are billed. A run where nothing was posted returns no rows and costs only the run fee. Each run logs a count like 3 new of 100 fetched.
The first run has nothing to compare against, so it returns everything it finds and remembers it. From the second run on you get only what LinkedIn added since.
To turn that into an alert:
- Save your search as a task with the checkbox on.
- Add a Schedule to that task, hourly or daily.
- Add a webhook on Run succeeded, pointing at Slack, n8n, Make or Zapier.
The memory lives in a named key-value store, linkedin-jobs-search-lookup-delta, one record per search line, holding the last 5,000 postings per watchlist. The same memory is what powers Add hiring signals, below. Two schedules with different filters keep separate memories. Leave the checkbox off but fill in Watchlist name and every posting comes back stamped with isNew and firstSeenAt, so you can filter them yourself in Sheets or n8n.
Hiring signals for sales and investors
A job search tells you who is hiring right now. What a sales team or an
investor actually wants is the derivative: which companies moved since last
week, and in which direction. Turn on Add hiring signals alongside the
monitoring checkbox and the search row gains a hiringSignals list, one entry
per company the search found at least two postings for.
{"query": "enterprise account executive @ United States","jobCount": 6,"hiringSignals": [{"company": "Acme Robotics","openRoles": 14,"signal": "gtm-push","signalReason": "4 of the 5 new roles (80%) are sales or marketing, which usually means a go-to-market push rather than general growth.","newRolesSinceLastRun": 5,"closedRolesSinceLastRun": 1,"growthPct": 40,"roleClusters": { "engineering": 2, "sales": 9, "marketing": 2, "ops": 1, "finance": 0, "other": 0 },"newRoleClusters": { "engineering": 1, "sales": 3, "marketing": 1, "ops": 0, "finance": 0, "other": 0 },"topDepartments": [],"seniorityMix": { "junior": 0, "mid": 9, "senior": 3, "lead": 2 },"remoteShare": 0.29,"comparedToRunAt": "2026-09-03T06:00:00.000Z"}]}
| Field | Type | What it tells you |
|---|---|---|
company | string | The employer name as LinkedIn shows it. |
openRoles | integer | Postings this search returned for them. Read the note below before treating it as headcount. |
signal | string | One of baseline, expanding, gtm-push, contracting, steady. The one field to sort a watchlist on. |
signalReason | string | One sentence naming the numbers behind the verdict. |
newRolesSinceLastRun | integer | Postings that were not there at your previous run. |
closedRolesSinceLastRun | integer | Postings that were there and are gone. Filled or withdrawn, LinkedIn does not say which. |
growthPct | number | null | Percent change in openRoles since the previous run. null on the baseline run. |
roleClusters | object | Every posting bucketed into engineering, sales, marketing, ops, finance, other. |
newRoleClusters | object | The same six buckets over only the newly opened postings. This is what gtm-push is read from. |
topDepartments | array | { department, count } from jobFunction, so empty unless Fetch full description is on. A guest search card carries no department. |
seniorityMix | object | Postings by junior, mid, senior, lead, read from the title. |
remoteShare | number | null | Share of postings flagged remote, 0 to 1. |
comparedToRunAt | ISO datetime | null | The run this entry was measured against. |
How the verdict is decided, in this order:
baselineif this is the first run of the watchlist. There is nothing to compare against yet, and calling every company "expanding" on day one would put a false spike at the front of your time series.expandingif growth is at least 20% or at least 5 postings opened, and the company is no smaller than it was.gtm-pushif sales plus marketing account for 40% or more of the newly opened postings.contractingif more postings closed than opened.steadyotherwise.
The schedule recipe
- Write the search you want to track, for example
account executive @ United States. - Put your 5 to 15 competitors or target accounts in Only these companies, so the run measures them and not the whole market.
- Turn on Only return results that are new since the last run, and Add hiring signals.
- Turn One row per job posting off. Signals sit on the search row, so grouped mode gives you one clean row per run instead of the same list repeated on every posting.
- Set Max job postings per search high enough that
truncatedcomes backfalse, and then leave it alone. A moving cap makes every company look like it grew. See the sampling caveat below. - Save it as a task, add a Schedule, and pick weekly.
- Add a webhook on Run succeeded to push the dataset into Slack, Sheets, n8n or your CRM. The Hiring signals dataset view is already trimmed for this.
Your first scheduled run is the baseline. Read from run two on.
{"searches": ["account executive @ United States"],"companies": ["Ramp", "Brex", "Mercury", "Navan", "Airbase"],"maxJobsPerQuery": 100,"deltaMode": true,"signals": true,"deltaName": "competitors-weekly","expandRows": false}
What openRoles here does and does not mean
openRoles is postings this search returned for that company, not the size
of their careers page. A search for account executive @ United States capped
at 100 measures their US sales hiring, which is exactly the number you want for
a GTM watchlist, and is not the number a careers-page scraper would give you.
Two consequences:
- Keep
maxJobsPerQuerysteady across runs. Raising it makes every company look like it grew. - Changing the search or its filters changes what is being measured. The watchlist name is derived from your filters for that reason, so a filter edit re-baselines cleanly instead of silently comparing two different questions. If you pinned your own Watchlist name, edit the search and the name together.
For whole-board numbers on a named company, use ATS Jobs Unified Lookup in hiring-signal mode instead, which reads each company's own ATS.
The sampling caveat, which you should read before you act on one run
LinkedIn's logged-out job search does not serve a stable result set. The same
offset requested twice comes back with an overlapping but different list, as
the endpoint alternates between two differently ranked populations. That is
documented at the top of src/target.js and it is why this actor deduplicates
by job ID in the first place.
It has a direct consequence for signals: a single run's
newRolesSinceLastRun, closedRolesSinceLastRun and growthPct include
sampling noise, not only real hiring movement. Two verification runs two
minutes apart on 2026-09-10, with nothing on earth having changed in between,
reported Salesforce at +23.1%, expanding and Cisco at -60%, contracting.
Both were the endpoint reshuffling, not hiring.
What to do about it:
- Read the trend, not the run. The watchlist memory is cumulative, so the
set of postings it knows about saturates over the first few runs and the
noise falls away. A company that reads
expandingfour weeks running is expanding. One that readsexpandingonce is a coin flip. - Set
maxJobsPerQueryhigh enough that your search is exhausted, and keep it fixed.truncated: falseon the row means the actor reached the end of what LinkedIn would serve. That reduces the noise but does not remove it, because the population itself moves. - Treat
roleClusters,seniorityMixandremoteShareas the sturdy fields. They are proportions of a sample, so they are far more stable than the counts, and they answer "what kind of hiring is this" without depending on run-over-run identity at all. - Ignore companies with only a handful of postings. Entries below two postings are already left out; below about five, a swing of one or two postings dominates the percentage.
- If you need per-company numbers you can act on individually, read the company's own job board instead. ATS Jobs Unified Lookup in hiring-signal mode returns the whole board from the company's own ATS API, where a posting that is gone is genuinely gone.
vs. TheirStack and PredictLeads
TheirStack and PredictLeads both sell hiring signals as a data product. TheirStack lists at roughly $0.005 to $0.03 per company; PredictLeads is sold as an annual contract, around $24,000 a year at the tiers most teams land on. Both are genuinely broader: they cover many job boards and ATS platforms at once, they keep years of back history, and they resolve a company from its domain rather than from how its name is spelled on a job card. If you need whole-market coverage with history, buy one of them.
What this gives you instead is the same weekly question answered off LinkedIn's
own public job search, at $1.50 per 1,000 postings and nothing else. A weekly
watchlist that returns 100 postings a run is about $8 a year, with no contract
and no minimum. The trade-offs are worth naming: history starts the day you
start running it, closedRolesSinceLastRun cannot tell a filled role from a
withdrawn one, companies are matched on their display name rather than on a
resolved entity, and the numbers describe one search rather than a whole
company.
Legal note
This actor reads public data only. It requests the same two logged-out URLs your browser requests when you open a LinkedIn job search or a job posting while signed out — the ones LinkedIn serves to search engines and to visitors without an account.
It does not log in, does not accept or store a LinkedIn account, password, session cookie
or li_at token, and has no input field in which you could give it one. It reads no private
profiles, no connection graphs and no personal data beyond what a hiring company chose to publish
in its own job advertisement: the role, the company, the location and the posting text. It does not
collect names, email addresses or contact details of individuals.
You are responsible for how you use the results. Public-data collection of this kind has been held lawful in the United States (hiQ Labs v. LinkedIn), but that is not the whole picture: LinkedIn's User Agreement prohibits automated collection regardless, and if you are in or handling data from the EU/UK, the GDPR applies to job-posting data that identifies a person (a named hiring manager in the posting text, for instance) even though it is public. Check your own obligations before putting this on a schedule, and don't use it to build profiles of individuals.
Related actors
- ATS Jobs Unified Lookup — cross-check hiring signals for a named company against its own ATS, across Greenhouse, Lever, Ashby and more.
- LinkedIn Profile Lookup — look up who posted a role, or the hiring manager named in a posting, from their LinkedIn profile URL.
- Email Pattern Finder + Verifier — once you have a company from a posting, work out its email address pattern to reach the hiring team.
If this saved you a scrape, a rating on the Store page helps other buyers find it.