LinkedIn Jobs Search Lookup: jobs scraper from $1.50/1k
Pricing
from $1.14 / 1,000 job posting returneds
LinkedIn Jobs Search Lookup: jobs scraper from $1.50/1k
LinkedIn jobs scraper by keyword and location — run a search on LinkedIn's public job board and get one row per posting: title, company, location, posted date, apply link, and the full description on request. No login, no cookies. Pay per posting returned; empty searches are free.
Pricing
from $1.14 / 1,000 job posting returneds
Rating
0.0
(0)
Developer
Adrian Voss
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
10 hours 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.
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.
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 | jobs | jobId | title | company | companyUrl | companyLogoUrl | location | remote | postedAt | postedAgo | jobUrl | activelyHiring | earlyApplicant | benefitsText | descriptionFetched | description | salaryText | seniorityLevel | employmentType | jobFunction | industries | applicantsText | scrapedAt |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| marketing manager @ United States | true | OK | marketing manager | United States | 100 | true | <all postings found (full list)> | 4447045308 | Marketing Manager | Amcor | https://ch.linkedin.com/company/amcor | https://media.licdn.com/dms/image/v2/C4D0BAQF0sed-lbowMg/company-logo_100_100/company-logo_100_100/0/1630499181382/amcor_logo | Evansville, IN | false | 2026-08-20 | 1 week ago | https://www.linkedin.com/jobs/view/marketing-manager-at-amcor-4447045308 | true | false | false | 2026-08-31T06:50:20.789Z |
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.
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 by Keyword | Apify" — the agent will find and run this actor.