LinkedIn Posts Scraper
Pricing
from $1.75 / 1,000 post scrapeds
LinkedIn Posts Scraper
Searches public LinkedIn posts by keyword and returns post text, author, exact publication time, engagement counts, media and comments. No login, no cookies, no session tokens.
Pricing
from $1.75 / 1,000 post scrapeds
Rating
5.0
(1)
Developer
coding laporte
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Unofficial LinkedIn Posts Search Scraper
⚠️ This is an UNOFFICIAL tool
It is not affiliated with, endorsed by, sponsored by, or connected to LinkedIn Corporation in any way. "LinkedIn" is a trademark of LinkedIn Corporation, used here solely to describe what this tool reads. This Actor is not a LinkedIn product and provides no LinkedIn-backed guarantee of accuracy, availability, or continuity.
Find public LinkedIn posts that mention your keywords and export them as structured JSON, CSV or Excel — post text, author, exact publication time, reaction and comment counts, media, and the comments themselves.
Give it a keyword, optionally a list of authors, and get back a clean dataset of matching posts.
Runs on Apify as renovative_basilisk/linkedin-posts-scraper — that is the Actor ID to pass to the API and the CLI.
What it does
- Searches for public LinkedIn posts by keyword, with LinkedIn's Boolean syntax (
AND,OR,NOT,"quoted phrases", parentheses). - Reads the public page of any author you name — a member profile or a company page — for their recent posts.
- Evaluates your query against every post's actual text, so a returned post really does mention what you asked for.
- Recovers each post's exact publication timestamp, and filters on recency before fetching anything.
- Optionally collects the comments LinkedIn shows to a signed-out visitor, with author, timestamp and per-comment like count.
What it does not do
This matters, so it is stated plainly:
| ❌ No login. | It never signs in, never asks for your LinkedIn credentials, and never accepts session cookies or an li_at token. |
| ❌ No private data. | It reads only what LinkedIn serves to an anonymous visitor. |
| ❌ No contact details. | It does not collect emails, phone numbers or connection lists. |
| ❌ No CAPTCHA solving, no protection defeated. | It solves no challenges and reverse-engineers no anti-bot system. If a server throttles, it backs off and retries on a rotated proxy session. |
Post authors and commenters are named individuals, so the output contains personal data. See Legal and compliance.
How keyword search works — read this first
LinkedIn's own post search (/search/results/content/) is behind the login wall: signed out, it redirects to a sign-in page. There is no public, LinkedIn-native way to search posts by keyword. This Actor does not have a hidden way around that, and neither does anything else that does not log in.
So discovery uses two routes, and their results are merged and de-duplicated:
| Route | How it works | Reliability |
|---|---|---|
| Author pages | The public /in/{identifier} or /company/{identifier} page of an author you name lists their recent posts. | High. LinkedIn-native, no third party involved. Covers roughly the author's last 10–25 posts. |
| Direct URLs | Any post URL in targetUrls is scraped as given. | Exact. |
| Web search | DuckDuckGo, Yahoo and Bing, restricted to site:linkedin.com/posts and to your postedLimit window. Google and Mojeek are selectable but off by default: Google serves a JavaScript-only page to anything that is not a full browser, and Mojeek refuses proxied traffic, so both currently return nothing. | Variable. Depends on what each engine has indexed and on whether it serves your IP a challenge page instead of results. A challenge is recognised and retried through a fresh proxy session, but the fix is residential proxies. |
Practical consequence: if you know whose posts you want, fill in authorPublicIdentifiers or authorsCompanyPublicIdentifiers. That path does not depend on a search engine and is the one to rely on. An open keyword search across all of LinkedIn works, but its coverage is whatever the engines have indexed — it is not a complete view of LinkedIn, and no tool that stays signed out can offer one.
The keyword query is applied to every post either way, so a run mixing both routes still returns only posts that match.
Input
The minimum viable input is a single search query:
{"searchQueries": ["artificial intelligence"]}
A fuller example — everything Bill Gates and Microsoft published about climate or AI in the past month, newest first, with comments:
{"searchQueries": ["\"artificial intelligence\" OR climate"],"authorPublicIdentifiers": ["williamhgates"],"authorsCompanyPublicIdentifiers": ["microsoft"],"postedLimit": "month","sortBy": "date","maxPosts": 100,"scrapeComments": true,"maxComments": 10,"proxyConfiguration": {"useApifyProxy": true,"apifyProxyGroups": ["RESIDENTIAL"]}}
Input reference
| Field | Type | Default | Description |
|---|---|---|---|
searchQueries (required) | array of strings | — | Keyword phrases, up to 10. Supports AND, OR, NOT, "quoted phrases" and parentheses. Matching ignores case and accents, so hopital americain finds Hôpital Américain. Each is matched against every post; a post is kept if it matches any of them, and the matching query is reported as searchQuery. LinkedIn caps a query at 85 characters. |
authorPublicIdentifiers | array of strings | — | Member public identifiers — williamhgates from linkedin.com/in/williamhgates. Their public page is read directly. Up to 50. |
authorsCompanyPublicIdentifiers | array of strings | — | Company public identifiers — microsoft from linkedin.com/company/microsoft. The company that published the post. Up to 50. |
targetUrls | array of strings | — | Profile, company or individual post URLs. Profile/company URLs are crawled for recent posts; a post URL is scraped directly. Localised hosts (uk.linkedin.com) are accepted. |
postedLimit | enum | any | any, 1h, 24h, week, month, 3months, 6months, year. Applied before fetching, so a narrow window makes a run cheaper as well as smaller. The window is also passed to each engine's own date filter, so discovery returns recent posts rather than the engine's all-time favourites — which, for a niche topic, were all older than the window and left nothing to fetch. |
postedLimitDate | string | — | Keep posts from this moment onwards. 2026-01-31, 2026-01-31T14:48:00.000+09:00, or epoch milliseconds. Combined with postedLimit, the stricter wins. |
sortBy | enum | relevance | relevance streams results in discovery order. date writes the dataset strictly newest-first — and, because the ordering is decided before anything is fetched, a capped run also spends its budget on the newest posts rather than on whatever a search engine ranked first. |
scrapeComments | boolean | false | Include the comments on each post's public page. The total count is reported either way. |
maxComments | integer | 10 | Comments kept per post. LinkedIn shows roughly ten to a signed-out visitor, so higher values have no effect. |
maxPosts | integer | 100 | Stop after this many unique posts. The main dial on run cost: it caps returned posts, search pages read, and posts checked and rejected — a run fetches at most five post pages per post you asked for. 0 is accepted and means "no limit", which here resolves to 1,000 posts — deliberately not the 10,000 maximum, because nobody typing 0 has costed the run out. That still authorises up to 5,000 charged fetches, so set a real number unless you mean it. The highest value you can type is 10,000. Discovery is capped separately and never exceeds 200 pages whatever maxPosts is — see Pricing. |
scrapePages | integer | 2 | Pages of search-engine results per query. Each yields up to ~30 post links. |
startPage | integer | 1 | First search-engine result page. Use it to continue a previous run. |
searchEngines | array of enums | duckduckgo, yahoo, bing | Also accepts google and mojeek, both off by default: Google serves a JavaScript-only page to non-browser clients and Mojeek refuses proxied traffic, so their pages cost a fetch and return nothing. Yahoo serves Bing's index in a form that works. Clear the list to disable web search entirely and scrape only the authors and URLs you supplied. |
maxConcurrency | integer | 5 | Requests in flight, capped at 10. Raising it mostly increases 429s and the retries they cause, not throughput. |
proxyConfiguration | object | Apify Proxy, RESIDENTIAL | Strongly recommended — see Proxies. |
respectRobotsTxt | boolean | false | Obey robots.txt. Enabling this returns zero results — see Legal and compliance. |
Output
One dataset item per post. Real output from a live run, truncated for readability:
{"type": "post","id": "7489894675928027136","urn": "urn:li:activity:7489894675928027136","attributedUrn": "urn:li:share:7489894675324166145","linkedinUrl": "https://www.linkedin.com/posts/williamhgates_heineken-is-trying-a-heat-battery-to-generate-activity-7489894675928027136-g8C-","content": "I'll raise a glass for lower emissions.","author": {"name": "Bill Gates","type": "profile","publicIdentifier": "williamhgates","linkedinUrl": "https://www.linkedin.com/in/williamhgates","avatar": "https://media.licdn.com/dms/image/v2/D5603AQF-RYZP55jmXA/...","followers": 40566873,"info": null},"postedAt": {"timestamp": 1785730046255,"date": "2026-08-03T04:07:26.255Z","postedAgoText": "3d"},"postImages": [{ "url": "https://media.licdn.com/dms/image/sync/v2/...", "alt": "Heineken Is Trying a Heat Battery..." }],"linkedArticle": {"url": "https://www.wsj.com/pro/sustainable-business/heineken-is-trying-a-heat-battery...","title": "Heineken Is Trying a Heat Battery to Generate Steam for Its Brewing","subtitle": "wsj.com"},"engagement": {"likes": 1152,"comments": 159,"shares": null,"reactionTypes": ["LIKE", "PRAISE", "INTEREST"]},"comments": [{"text": "I like that this example makes climate innovation tangible...","createdAt": "2026-08-03T04:25:04.052Z","postedAgoText": "3d","likes": 7,"author": {"name": "Justin W.","linkedinUrl": "https://www.linkedin.com/in/justin-w-91133667","publicIdentifier": "justin-w-91133667","avatar": "https://static.licdn.com/aero-v1/sc/h/..."}}],"searchQuery": "\"artificial intelligence\" OR climate","discoveredVia": "author-page"}
Output reference
| Field | Type | Notes |
|---|---|---|
type | string | Always post. |
id | string | LinkedIn's numeric activity ID. Stable, and the de-duplication key. |
urn | string | urn:li:activity:{id}. |
attributedUrn | string | null | The post's ugcPost/share URN, which is a different number from the activity ID. Both are reported because conflating them produces URLs that resolve to nothing. |
linkedinUrl | string | The public permalink. For a post discovered without one, the canonical /feed/update/… URL — LinkedIn 404s a permalink whose slug and hash were not the original ones, so it is never synthesised. |
content | string | null | Post body, with the author's line breaks preserved. |
author.name | string | null | Member or company name. |
author.type | string | null | profile or company. |
author.publicIdentifier | string | null | e.g. williamhgates, microsoft. |
author.linkedinUrl | string | null | Author page, tracking parameters stripped. |
author.avatar | string | null | Profile picture or company logo. |
author.followers | integer | null | Follower count as rendered. |
author.info | string | null | The line under the author's name — a member's headline, or a company's follower line. |
postedAt.date | string | ISO-8601 UTC with milliseconds. |
postedAt.timestamp | integer | Epoch milliseconds. |
postedAt.postedAgoText | string | null | LinkedIn's relative phrasing, e.g. 3d. |
postImages | array | {url, alt} for each image in the post. Avatars and reaction glyphs are excluded. |
linkedArticle | object | null | {url, title, subtitle} for a linked article or external page. LinkedIn's /redir/redirect wrapper is resolved to the real destination. |
engagement.likes | integer | null | Total reactions. |
engagement.comments | integer | null | Total comments — LinkedIn's own figure, not the number returned in comments. |
engagement.shares | null | Always null. LinkedIn does not render a repost count to signed-out visitors; it is reported as unknown rather than guessed. |
engagement.reactionTypes | array | Which reaction types are displayed, e.g. ["LIKE","EMPATHY","APPRECIATION"]. Per-type counts are not public. |
comments | array | Empty unless scrapeComments is on. Each: {text, createdAt, postedAgoText, likes, author:{name, linkedinUrl, publicIdentifier, avatar}}. |
searchQuery | string | Which of your queries this post matched. |
discoveredVia | string | author-page, input-url, or search:{engine}. |
Fields are always present; unavailable values are null rather than omitted, so the dataset has a stable shape for CSV and Excel export.
Pricing
This Actor uses pay per event. You are charged for the work a run does on your behalf — the posts it returns, and the posts it had to fetch and read to find them:
| Event | Price | When it fires |
|---|---|---|
actor-start | $0.005 | Once per run. Covers the discovery phase — the search-engine result pages that find the posts, which are fetched before any post is. |
post-scraped | $0.00175 — $1.75 per 1,000 posts | Per post saved without comments. |
post-scraped-with-comments | $0.003 — $3.00 per 1,000 posts | Per post that actually carried at least one comment. |
post-filtered | $0.001 — $1.00 per 1,000 posts | Per post fetched but not returned: its text did not match your query, LinkedIn served a sign-in page instead of the post, or the fetch was blocked and gave up after its retries. Charged once per post, whatever the number of attempts. |
Why a checked-but-unreturned post is charged. Fetching a post through a residential proxy is the expensive part of a run, and it costs the same whether or not the post turns out to match your query — the text can only be checked after the page has been fetched. A run whose keyword matches nothing is therefore the most expensive kind to serve, not the cheapest, because every rejected post frees its slot and pulls another candidate in behind it. Charging for the check at well under half the price of a delivered post is what keeps that from being free, and it is why this Actor can afford to price delivered posts below the market.
How to pay less of it. The ratio of checked to returned posts is entirely under your control: name authors in authorPublicIdentifiers or authorsCompanyPublicIdentifiers and use a query that matches how those authors actually write. A broad keyword across all of LinkedIn examines many posts per result; a named author with a fitting query examines barely more than it returns. A run reads at most five times maxPosts posts in total, delivered and rejected together — so post-filtered can add at most 5 × maxPosts × $0.001. The run stops there and logs why. (At maxPosts: 0 that multiplier applies to the 1,000 posts 0 resolves to — 5,000 fetches, up to $7.005 — which is why 0 is not a setting to leave on by accident.) Each post is charged once however many times we have to retry it against a throttling LinkedIn; those retries are our cost, not yours.
How that compares. Against the most-used LinkedIn post-search Actor on this Store, which charges $0.002 per post, the list price here is 12.5% lower — but compare honestly: that Actor does not bill for posts it examines and discards, so on a broad keyword your effective cost per delivered post here can exceed its flat rate. On the "one post in five" row above it works out at $0.0058 per post kept. Where this Actor wins is a run that knows what it is looking for; where it loses is a wide trawl. The larger difference is comments: that Actor bills each comment as its own $0.002 event, so one post with ten comments costs $0.022 there. Comments arrive in the same request as the post here — they cost no extra fetch — so a post with all its comments is $0.003 flat, roughly seven times cheaper. That Actor also bills $0.001 for a query that returns nothing at all, on the same reasoning as post-filtered here: a search that finds nothing still costs the machine that ran it.
Posts outside postedLimit are not charged at all: their publication time is read from the activity ID before anything is fetched, so an out-of-window post costs nothing. A post that is fetched and then rejected does not consume your maxPosts either — it releases its slot, so the run still works toward the number of posts you asked for — but it is charged at the post-filtered rate, because the fetch already happened. (One exception, stated for completeness: if writing a post to the dataset fails partway, that post's slot is treated as spent rather than reused, because the row may already exist and reusing it could deliver one more post than you capped. Such a run returns one fewer post, never one more.)
Setting maxPosts caps your spend, and it caps discovery and checking as well as results: the number of posts read scales with maxPosts (at five times it), and the number of search-result pages scales with it too — half of maxPosts, never fewer than 20 and never more than 200, whatever maxPosts says. So a run cannot quietly spend more on looking for posts than it can return. Each of those 5 × maxPosts fetches is charged once and once only, at one rate or the other, so the arithmetic ceiling on a run is $0.005 + $0.007 × maxPosts — $0.705 at the default, reached only by a run that delivers every post it is allowed to and rejects four times as many and finds comments on all of them. Worked figures at the default maxPosts: 100, where the run reads at most 500 posts:
| What the query matches | Delivered | Read but rejected | You pay |
|---|---|---|---|
| Everything it reads | 100 | 0 | $0.18 |
| One post in five | 100 | 400 | $0.58 |
One in five, scrapeComments: true | 100 | 400 | $0.705 |
| Nothing at all | 0 | 500 | $0.505 |
Naming authors and matching how they actually write is what moves you up that table. The dearest row is not the one that fails — it is the one that trawls widely and succeeds. Each row is the arithmetic for that split, not a promise that the run will get that far: a run stops as soon as any of its limits binds, and you are charged only for what it actually did.
When a run stops early
Fetching a page through a residential proxy costs this Actor real money before it earns anything, and discovery — the search-engine and author pages that find the posts — earns nothing at all. Two separate mechanisms stop a run whose spending has outrun what it can charge for. They are not the same thing and they do not look the same to you.
1. Refused before it starts — the run FAILS and nothing is crawled or charged. Two checks run ahead of the actor-start charge:
-
The Actor's pay-per-event pricing is missing or incomplete on the platform. That is a publisher-side misconfiguration; the run fails rather than delivering paid work for nothing.
-
Your
maxTotalChargeUsdis below what the run's own discovery phase costs. Discovery is fetched before a single post can be charged for, so a cap under that price buys a crawl that can never pay for itself, and a fast failure is the honest answer. The threshold depends on how many discovery pages your input asks for — roughly $0.0086 plus $0.00036 per discovery page:Run Discovery pages Cap must be at least Default: one query, four engines, two pages each 8 $0.0115 Saturating discovery at maxPosts: 10050 $0.0266 Any run at the 200-page discovery ceiling 200 $0.0805 The failure message names the exact figure for your run. Note that the $0.01 platform minimum is below the threshold for almost every run, so it is not a usable cap for real work. $0.10 clears any run this Actor can construct, since discovery can never exceed 200 pages.
2. Stopped part-way — the run ends normally and keeps what it collected. While the run is going it compares what it has been charged for against what its proxied fetches have cost, and stops when the fetches it is still allowed to make could not close the gap. Concretely:
- It waits before judging anything: 40 discovery attempts (while no post has been fetched yet) or 10 post-page attempts. Every run is legitimately behind when discovery ends, because discovery earns nothing.
- It never stops a run that is ahead. If what the run has been charged already covers what it has cost, it continues, whatever the projection says about the fetches to come.
- A discovery still in progress is judged on the discovery it is buying, not on the part that has come back. At attempt 40 a 50-page discovery has ten pages queued and already paid for; the pages still to come are credited with the yield this run is measuring, so a crawl finding one new post link per page is judged on the fifty it is on course to find rather than the thirty it has counted. A crawl finding none is still credited with none — that case is the one this exists to catch.
- What it counts as still earnable is bounded by whichever is smallest: the posts left under your
maxPosts, what is left of the 5 ×maxPostsfetch ceiling, and the candidates discovery has found or is on course to find. Each is valued at what this run is actually earning per fetch — the mix ofpost-scraped,post-scraped-with-commentsandpost-filteredit has measured — not at the cheapest outcome it might have had. Before any post has been fetched, a candidate is valued at thepost-scrapedprice; while fetches are coming back with nothing in them, at thepost-filteredfloor. - A fetch only earns more than it costs while it is not being retried much, and how much is too much depends on what the run earns: a run delivering posts at $0.00175 stops covering its fetches at about 2.4 proxied attempts each, one billing everything at the
post-filteredfloor at about 1.4. Past that no number of remaining fetches can repay anything — which is what a blocked run looks like from the inside.
What you see. The run finishes in its normal successful terminal state. The terminal status message says it was stopped early, quotes the estimated platform cost against the net revenue, and names what was actually observed — no candidates found, post pages fetched but never returning, fetches being retried, posts fetched but rejected by the keyword filter, or discovery simply costing more than the posts it found can repay. The same explanation, with the full numbers, is in the log. The dataset holds everything collected up to that point; it does not quietly return less and call it a complete run. You are billed for the events already charged and nothing more.
What to change depends on which of those it says:
| What the message says | What to change |
|---|---|
| Discovery found no post links at all | Enable Apify Proxy on RESIDENTIAL — search engines serve challenge pages to datacenter IPs — or name authors so discovery does not depend on a search engine at all. |
| Discovery cost more than the candidates it found can repay | Narrow it: fewer searchQueries, fewer searchEngines, a smaller scrapePages. Raising maxPosts is the one thing that cannot help here — the fetches the run has left are bounded by the candidates discovery found, not by maxPosts, and a larger maxPosts buys more of the search pages that caused the stop. |
| Post pages are being fetched and none come back | LinkedIn is throttling or blocking. Residential proxy, and lower maxConcurrency. |
| Fetches are being retried — N proxied attempts per fetch, against 1.0 on a healthy run | Same: residential proxy, lower maxConcurrency. |
| Pages were fetched but few produced a post | The keyword filter is rejecting them. Broaden or correct searchQueries, and narrow discovery with authorPublicIdentifiers so fewer irrelevant posts are fetched at all. |
| Posts are delivering, but discovery cost more than they earn | Narrow discovery, or raise maxPosts so one discovery phase is spread over more paid posts. |
What this is not. It is not a guarantee that a run never costs more than it returns, and it is not a refund mechanism. It stops the spending at the point it notices: requests already in flight still finish and are still charged, so the run can overshoot by up to maxConcurrency fetches, while everything merely queued behind them is never fetched. It is off entirely for runs that are not charging at all, such as a local run.
Proxies
Search engines serve challenge pages to datacenter and shared IPs, and LinkedIn rate-limits anonymous traffic aggressively.
- Recommended: Apify Proxy with the
RESIDENTIALgroup (the default). - Workable for author-only runs: Apify Proxy datacenter — LinkedIn's public author pages tolerate it better than the search engines do.
- Not recommended: no proxy. Keyword discovery will find little or nothing; the Actor logs a warning saying so.
429, 401 and 403 responses are treated as blocks, so the request is retried on a rotated proxy session rather than parsed as an empty result. Those rotations are bounded at one per request — so a blocked request costs at most two proxied attempts in all: the original, and one retry on a fresh proxy session. Past that the candidate is given up on. Measured across real runs, a request that failed once and succeeded on its second try is common, while nothing was ever recovered by a third through ninth attempt: the requests that went past two attempts went all the way to Crawlee's default of ten and still failed. (A failure that is not a block — a timeout, say — is retried up to three times instead, so four attempts.) Those attempts are our cost, not yours — the post is charged once, as post-filtered.
Performance and cost
| Setting | Requests per post | Notes |
|---|---|---|
Author pages only (searchEngines: []) | ~1.1 | One page per author, then one per post. Fastest and most reliable. |
| Web search + authors (default) | ~1.3 | Adds one request per query × engine × page. |
scrapeComments: true | unchanged | Comments come from the same page as the post — they cost no extra request. |
Rough guide: 100 posts takes a few minutes at the default concurrency of 5. Raising maxConcurrency usually produces more retries rather than more throughput, which is why it is capped at 10.
Discovery is capped, in proportion to maxPosts and absolutely. Search fans out as queries × authors × engines × pages, and that product can dwarf what a run can return — so the number of discovery requests is limited to half of maxPosts, never fewer than 20 and never more than 200. That second cap binds from maxPosts: 400 upwards and does not lift however large maxPosts is, because discovery earns nothing: 200 uncharged pages is already about $0.05 of proxy spend before a single post is fetched, and it takes over 250 charged post fetches to earn that back. Author pages are filled from that budget first — they are LinkedIn-native and do not depend on a search engine — and whatever they leave goes to keyword search. When the cap bites, the run logs how many pages it dropped, and the requests it keeps are spread evenly across every dimension rather than being spent entirely on the first of each: author scope first, then engine, then query. One detail worth knowing: keyword search is narrowed to individual authors only while you have named 10 or fewer of them. Past that the cross-product would dominate the run, so search runs unscoped and those authors are covered by their own public pages instead. Raise maxPosts to widen discovery up to the 200-page ceiling; narrow searchQueries, scrapePages or the author list to spend the same budget more deeply.
Usage from code
from apify_client import ApifyClientclient = ApifyClient('YOUR_APIFY_TOKEN')run = client.actor('renovative_basilisk/linkedin-posts-scraper').call(run_input={'searchQueries': ['"artificial intelligence" OR climate'],'authorPublicIdentifiers': ['williamhgates'],'postedLimit': 'month','sortBy': 'date','maxPosts': 50,})for post in client.dataset(run['defaultDatasetId']).iterate_items():print(post['postedAt']['date'], post['author']['name'], '—', post['engagement']['likes'])
Legal and compliance
Read this before running the Actor. It is stated in full rather than summarised because the honest position is more complicated than "it only reads public data".
robots.txt
https://www.linkedin.com/robots.txt sends Disallow: / to all non-whitelisted user agents, and explicitly disallows /feed/update/ and /embed/feed/update/ even for the search engines it names.
robots.txt is a crawler-directive convention, not a contract or a statute, and its legal weight varies by jurisdiction. What it unambiguously is: a clear signal about the access LinkedIn intends to permit. You should factor that into your decision.
There is no configuration of this Actor that both respects robots.txt and returns results. The respectRobotsTxt input is exposed so that:
- the behaviour is verifiable rather than hidden;
- enabling it demonstrably returns zero items, which documents the conflict;
- the decision to proceed is a deliberate, recorded choice by the operator.
The default is false, because true makes the Actor non-functional. That default is a statement about what the Actor does, not a claim that it is permitted.
Terms of service
Automated collection of LinkedIn data is contrary to the LinkedIn User Agreement. Scraping public data has been treated as lawful in some jurisdictions (notably hiQ Labs v. LinkedIn in the US Ninth Circuit, on CFAA grounds), but that ruling did not make it contract-compliant, does not bind other jurisdictions, and does not settle claims LinkedIn may bring in contract or under other statutes.
The web search engines used for keyword discovery have their own terms, which also address automated querying. The same reasoning applies.
Data protection
This Actor collects personal data. Post authors and commenters are identified individuals: name, public profile URL, profile picture, headline and follower count, plus whatever they wrote. Under GDPR and CCPA that is personal data regardless of the fact that they published it themselves.
If you process these records for EU or California data subjects you need a lawful basis, you must be able to honour access and deletion requests, and "it was public" is not by itself a lawful basis. Turning scrapeComments off substantially reduces the number of individuals a run touches — comments bring in people who commented on a post, not just the author.
This Actor deliberately collects no contact details and does not touch profile pages beyond the post list they publish.
Your responsibility
Apify's Store Publishing Terms and Actor Terms place compliance with third-party terms and applicable law on the party running the Actor. You are responsible for determining whether your use is lawful in your jurisdiction and for your purpose. If you need certainty, LinkedIn's official Marketing and Talent APIs are the sanctioned route.
Limitations
- Keyword-only coverage is not complete. Without a login there is no LinkedIn-native post search, so an unscoped keyword run sees only what web search engines have indexed. Name the authors you care about to get reliable coverage of them. See How keyword search works.
- Author pages show recent posts only. LinkedIn's signed-out rendering lists roughly the last 10–25 posts per author, and offers no pagination to a visitor. Older posts are reachable only through search-engine discovery.
- An author page also lists what that author reshared. Those posts are returned under their original author, not the person whose page they were found on — the same convention as comparable Actors, and the reason
author.namein your dataset will not always be the identifier you asked for. Filter onauthor.publicIdentifierif you need strictly first-party posts. engagement.sharesis alwaysnull. Repost counts are not rendered to signed-out visitors.- Reaction identities are not available. You get totals and which reaction types appear, not who reacted. Retrieving that requires a logged-in session, which this Actor does not use.
- Comments are capped by LinkedIn, not by this Actor. A post with 400 comments shows about ten publicly;
engagement.commentsreports the true total. - Reposts are collapsed onto the original. A reshare and the post it shares have different activity IDs but one permalink; the original is what you get.
author.infovaries by author type. For a member it is their headline; for a company it is the follower line.- Search-engine ranking is not reproducible.
relevanceordering is the engines' and differs run to run. UsesortBy: datefor anything you need to be deterministic. - Markup changes break parsers. LinkedIn changes its HTML without notice. Parsing is isolated in
src/parsers.pyand covered by fixture-based tests so fixes are quick — please report breakage via the Issues tab.
Troubleshooting
Zero results, and the log says search engines found nothing.
Most often the engines served challenge pages instead of results. DuckDuckGo's challenge (served as HTTP 202), Google's "unusual traffic" page and Mojeek's refusal are recognised and retried through a fresh proxy session, so a run that still ends this way has used up its rotations: enable Apify Proxy with RESIDENTIAL. If the log instead says posts were discovered but fell outside the time window, the engines' date filters found only older posts for that query: widen postedLimit, raise scrapePages, or name authors. To take search engines out of the picture entirely, set searchEngines: [] and name authors instead.
Zero results with respectRobotsTxt: true.
That is the designed behaviour — see Legal and compliance.
Posts were discovered but few were saved.
They did not match your query. The terminal status message reports how many were discarded by the keyword filter. Boolean queries are strict: hiring AND engineer needs both words, and terms match whole words only (engine does not match engineering). Accents and curly quotes are not a cause: matching folds both sides, so hopital americain finds a post that says “l’Hôpital Américain”, and either spelling finds the other.
"No posts found on the public page" for an author.
Check the identifier is the one in their URL (linkedin.com/in/**williamhgates**). LinkedIn also hides the post section from throttled requests — enable a proxy and retry.
Fewer items than maxPosts.
Usually discovery ran out of candidates: add authors, raise scrapePages, widen postedLimit, or loosen the query. If the run stopped for another reason it says so — see the next entry.
The run ended early, or returned less than I asked for. Read the run's terminal status message (top of the run page, also the last line of the log): a run that stops before its limits always names the reason there. The reasons are:
- "the most a
maxPostsof N allows" — the run hit its ceiling of 5 ×maxPostsfetched post pages while most of them were being rejected by the keyword filter. BroadensearchQueries, or narrow discovery withauthorPublicIdentifiersso fewer irrelevant posts get fetched. See Pricing. - A charge limit was reached. You set
maxTotalChargeUsd(or your account hit its own limit) and the run stopped rather than doing work it cannot bill for. Raise the limit or lowermaxPosts. - The run would have cost more to produce than it charges. The status message names which of several causes was actually observed — no candidates found, fetches being blocked, or the keyword filter rejecting nearly everything — and each has a different fix. See When a run stops early.
- Discovery was capped. The log says how many search pages were dropped. Raise
maxPosts(up to the point the 200-page ceiling binds), or narrowsearchQueries/scrapePages/the author list. See Performance and cost.
Whatever the reason, the dataset keeps everything collected up to that point, and you are charged only for the events already charged.
The run failed saying pay-per-event pricing is incomplete or not in effect. That is deliberate and it is not your input: the Actor refuses to scrape when the platform is not applying its per-event prices, because it would otherwise do the whole run and charge nothing. Report it via the Issues tab — nothing you can change in the input will fix it.
Comments are empty.
scrapeComments is false. Turn it on.
Local development
pip install -r requirements.txtpip install pytest pytest-asyncio ruffpython -m pytest # runs offline against saved fixturesruff check .
Run the Actor locally with the Apify CLI:
$apify run --input='{"searchQueries":["climate"],"authorPublicIdentifiers":["williamhgates"],"searchEngines":[],"maxPosts":5}'
Tests never hit the network: tests/fixtures/ holds real captured LinkedIn responses, so parser regressions are caught without generating traffic.
Changelog
See CHANGELOG.md.