# LinkedIn Post Comments Scraper (`coregent/linkedin-post-comments-scraper`) Actor

Scrape the top public comments from LinkedIn posts - the most relevant comments LinkedIn shows publicly (about 10 per post), with comment text, commenter names and profile URLs, exact timestamps, reaction counts, mentions, links and hashtags. No LinkedIn login or cookies required.

- **URL**: https://apify.com/coregent/linkedin-post-comments-scraper.md
- **Developed by:** [Delowar Munna](https://apify.com/coregent) (community)
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.00 / 1,000 comment results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## LinkedIn Post Comments Scraper

![LinkedIn Post Comments Scraper](https://github.com/coregentdevspace/linkedin-post-comments-scraper-assets/blob/main/thumbnail-linkedin-post-comments-scraper.png?raw=true)

Scrape the **top public comments from LinkedIn posts**. Paste one or more post URLs and get one clean row per comment for the comments LinkedIn shows publicly on each post - its **most relevant ~10**: comment text, exact timestamp, commenter name and profile URL, reaction count, mentions, links, hashtags, attached images and source-post context. Export as JSON, CSV or Excel.

Built for quick engagement checks across many posts: who the most visible commenters are, what they are saying, and which companies are joining in. It does **not** yet collect every comment on a post, replies, newest-first order or commenter headlines (see [Limitations](#limitations)).

**No LinkedIn login, cookies or browser extension required.**

### What you get

| Field                                    | Description                                                                                                     |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `recordType`                             | `COMMENT` (top-level). `REPLY` is reserved for when replies become available                                    |
| `commentId`, `commentUrn`, `commentUrl`  | Stable comment identifiers and a permalink that opens the post at that comment                                  |
| `text`, `createdAt`                      | Comment text (line breaks preserved) and exact ISO timestamp                                                    |
| `parentCommentId`, `rootCommentId`       | Thread links: a comment's `rootCommentId` is its own ID and `parentCommentId` is `null`                         |
| `commentRank`, `replyRank`               | Position under the requested sort order                                                                         |
| `replyCount`, `isPinned`, `isEdited`     | Not shown on LinkedIn's public page, so currently `null`                                                        |
| `reactions`                              | `total`; the per-type keys (`like`, `praise`, ...) are currently `null`                                         |
| `commenter`                              | `name`, `profileUrl`, `publicIdentifier`, `imageUrl`, `isCompany` (`headline` and `profileId` currently `null`) |
| `mentions`, `links`, `hashtags`, `media` | Tagged people/companies, outbound links, hashtags, attached images                                              |
| `post`                                   | `postId`, `postUrn`, `activityId`, `postUrl`, `inputUrl`, `reportedCommentsCount`                               |
| `sortOrder`, `dataSource`, `scrapedAt`   | Order actually applied, which source served the row, scrape time                                                |

**Never estimated.** If a value is not publicly available for a comment, it is `null` - not `0` and not `false`. For example, when only a reaction total is visible, the breakdown fields are `null`.

### How it works

1. Each post URL is normalised (tracking parameters, fragments and comment-specific links are reduced to the parent post) and duplicates are removed.
2. The actor opens the post's **public page** - the same page a logged-out visitor sees. It proves the post is public and shows the post's total comment count and its most relevant top-level comments (typically about 10).
3. Those comments are parsed into rows in LinkedIn's relevance order. When a post has more comments than its public page shows, you get the visible ones and the post is marked `PARTIAL_RESULT` in the run summary, with LinkedIn's total.
4. Comments are deduplicated by comment ID across posts, so the same comment reached through two URLs is never saved or billed twice.

### FAQ

**Which post URL formats are supported?**

- `https://www.linkedin.com/feed/update/urn:li:activity:<id>`
- `https://www.linkedin.com/feed/update/urn:li:ugcPost:<id>`
- `https://www.linkedin.com/posts/<author>_<slug>-activity-<id>-<hash>`
- `https://www.linkedin.com/posts/<author>_<slug>-ugcPost-<id>-<hash>`
- Country subdomains (`uk.linkedin.com`, `de.linkedin.com`, ...) work too.

**Can I use comment-specific URLs?** Yes. A link containing `?commentUrn=...` (or a `/feed/update/urn:li:comment:(...)` link) is normalised to its parent post, and all of that post's comments are collected.

**Do I need to log in or provide cookies?** No. The actor never asks for credentials or cookies.

**Can I scrape multiple posts?** Yes - up to 1,000 post URLs per run, processed in parallel. One failed post never fails the run.

**Can I scrape all comments?** Not yet. The actor returns the comments LinkedIn shows on a post's public page - about the 10 most relevant. For a post with more, the run summary marks it `PARTIAL_RESULT` and gives LinkedIn's total comment count. **Scrape all comments** is accepted but currently returns the same ~10.

**How do comment limits work?** **Max comments per post** caps top-level comments per post. Values above ~10 currently return the ~10 the public page shows.

**Are replies included?** Not yet. LinkedIn's public page doesn't show replies, so **Include replies** currently returns no reply rows. Replies will come as their own rows (`recordType: "REPLY"`, linked by `parentCommentId` and `rootCommentId`) once a source for them is available.

**Which sort orders are supported?** Relevance - LinkedIn's own order, which `commentRank` follows. `NEWEST` is accepted but not available yet: comments come back in relevance order, each row records the order actually applied in `sortOrder`, and the run summary notes it.

**Are commenter headlines and profile URLs included?** Profile URLs (people and company pages) are included for virtually every comment. Headlines are not: LinkedIn's public page doesn't display them, so `headline` is `null`. To get them, pass `commenter.profileUrl` to a profile scraper.

**Are reaction types included?** The reaction total is included. The per-type breakdown isn't shown on the public page, so those fields are `null` - never estimated.

**Are mentions, links and hashtags included?** Yes. Mentions are structured (`name`, `type`, `publicIdentifier`, `profileUrl`); links have LinkedIn's redirect and tracking wrappers removed.

**What happens when a post can't be scraped?** Nothing is charged for it. Every post gets a status in the `RUN_SUMMARY` key-value record:

| Status                                                  | Meaning                                                              |
| ------------------------------------------------------- | -------------------------------------------------------------------- |
| `SUCCESS`                                               | All requested comments were collected                                |
| `PARTIAL_RESULT`                                        | Some comments were collected but fewer than requested (reason given) |
| `NO_COMMENTS`                                           | The post has no comments                                             |
| `INVALID_POST_URL`                                      | The URL is not a LinkedIn post URL (e.g. a profile or company page)  |
| `POST_NOT_FOUND`                                        | The post does not exist or was deleted                               |
| `POST_UNAVAILABLE`                                      | The post is not public                                               |
| `RATE_LIMITED` / `BLOCKED` / `TEMPORARY_LINKEDIN_ERROR` | LinkedIn refused or failed the request - try again later             |
| `SKIPPED_SPENDING_LIMIT`                                | Your per-run spending limit was reached before this post             |

Posts worth retrying are written to the `RETRY_INPUT` record - paste it back as input to re-run just those.

**How does pricing work?** Pay per result: you are charged once per unique comment or reply row saved to the dataset, at the same price for both. Post lookups, pagination, retries, duplicates, failed or comment-less posts and the run summary are free, and a run with no results costs nothing in result charges. See the **Pricing** tab for current rates - this README deliberately doesn't quote figures, so it never goes out of date. The actor stops cleanly at your per-run spending limit.

**Can I schedule recurring collection?** Yes - use Apify Schedules to re-run the same input (for example daily on a campaign post).

**Can I use it from the API or an AI agent (MCP)?** Yes. Use the Apify API, any official client, or the Apify MCP server; the input and output are plain JSON. See [API example](#api-example).

**How do I enrich commenters afterwards?** Feed `commenter.profileUrl` into a LinkedIn profile scraper for full profiles, then into a work-email finder. To find posts in the first place, use Coregent's [LinkedIn Profile Posts Scraper](https://apify.com/coregent/linkedin-profile-posts-scraper) (a person's or company's posts) or [LinkedIn Posts Search Scraper](https://apify.com/coregent/linkedin-posts-search-scraper) (posts by keyword), then pass their post URLs here.

### Input examples

**Minimal** - one post:

```json
{
    "postUrls": ["https://www.linkedin.com/feed/update/urn:li:activity:7514290662138068992/"]
}
```

**Several posts** - mixed URL forms. This returned 24 comments (10 + 9 + 5) in a test run:

```json
{
    "postUrls": [
        "https://www.linkedin.com/posts/justinwelsh_make-enough-money-to-stop-thinking-about-activity-7514290662138068992-LIFY",
        "https://www.linkedin.com/feed/update/urn:li:ugcPost:7513980032084770816/",
        "https://www.linkedin.com/feed/update/urn:li:activity:7513993096116895744/"
    ]
}
```

### Output example

Results appear in the **Comments & replies** table (one row per comment or reply; **All fields** shows every column) and can be exported as JSON, CSV, Excel or HTML, or read through the API.

![Comments & replies table: comments with commenter, text, reactions and source post](https://github.com/coregentdevspace/linkedin-post-comments-scraper-assets/blob/main/linkedin-post-comments-scraper-output-comments-n-replies-table-view.png?raw=true)

#### Comments & replies tab - sample record

A real row from a 3-post run (24 rows, public post pages). This one is a company commenter with a mention, a link and hashtags. The commenter's headline and reaction breakdown are `null` because LinkedIn's public page doesn't show them - never estimated.

```json
{
    "rowType": "comment",
    "recordType": "COMMENT",
    "commentId": "7514001514433269761",
    "commentUrn": "urn:li:comment:(urn:li:ugcPost:7513993095022104578,7514001514433269761)",
    "commentUrl": "https://www.linkedin.com/feed/update/urn:li:activity:7513993096116895744/?commentUrn=urn%3Ali%3Acomment%3A(urn%3Ali%3Aactivity%3A7513993096116895744%2C7514001514433269761)",
    "text": "That 'stop unauthorized actions in real time' point is critical for data privacy, especially as agents handle sensitive inputs. We've found that client-side RAM-only tokenization is essential to eliminate egress risk before data even reaches the LLM, a core principle Privacy Scrubber applies. You can see more on our approach to AI privacy controls here: https://privacyscrubber.com/compliance/soc2/ai-privacy-controls/\n#CISO #ZTDS #AIPrivacy",
    "createdAt": "2026-10-08T16:39:24.439Z",
    "isEdited": null,
    "isPinned": null,
    "parentCommentId": null,
    "rootCommentId": "7514001514433269761",
    "commentRank": 3,
    "replyRank": null,
    "replyCount": null,
    "reactions": {
        "total": 1,
        "like": null,
        "praise": null,
        "empathy": null,
        "appreciation": null,
        "interest": null,
        "other": null
    },
    "commenter": {
        "name": "Privacy Scrubber",
        "headline": null,
        "profileUrl": "https://www.linkedin.com/company/privacyscrubber/",
        "publicIdentifier": "privacyscrubber",
        "profileId": null,
        "imageUrl": "https://media.licdn.com/dms/image/v2/D4D0BAQEjHr0ClZ5zSg/company-logo_100_100/B4DaBULsNLLAAI-/0/1788118746169/privacyscrubber_logo?e=2147483647&v=beta&t=sw11Ksg9mJhJW5e9O5q_jB9bQG15E1-NF_arIJc5Res",
        "isCompany": true
    },
    "mentions": [
        {
            "name": "Privacy Scrubber",
            "type": "COMPANY",
            "publicIdentifier": "privacyscrubber",
            "profileUrl": "https://www.linkedin.com/company/privacyscrubber/"
        }
    ],
    "links": ["https://privacyscrubber.com/compliance/soc2/ai-privacy-controls"],
    "hashtags": ["CISO", "ZTDS", "AIPrivacy"],
    "media": [],
    "post": {
        "postId": "7513993096116895744",
        "postUrn": "urn:li:activity:7513993096116895744",
        "activityId": "7513993096116895744",
        "postUrl": "https://www.linkedin.com/feed/update/urn:li:activity:7513993096116895744/",
        "inputUrl": "https://www.linkedin.com/feed/update/urn:li:activity:7513993096116895744/",
        "reportedCommentsCount": 6
    },
    "sortOrder": "RELEVANCE",
    "dataSource": "guest",
    "scrapedAt": "2026-10-13T07:52:46.693Z"
}
```

### API example

Run the actor and get the comment rows back in one call (replace `<YOUR_APIFY_TOKEN>`):

```bash
curl -X POST "https://api.apify.com/v2/acts/coregent~linkedin-post-comments-scraper/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"postUrls": ["https://www.linkedin.com/feed/update/urn:li:activity:7514290662138068992/"], "maxCommentsPerPost": 50}'
```

Or with the JavaScript client:

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('coregent/linkedin-post-comments-scraper').call({
    postUrls: ['https://www.linkedin.com/feed/update/urn:li:activity:7514290662138068992/'],
    maxCommentsPerPost: 50,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const summary = await client.keyValueStore(run.defaultKeyValueStoreId).getRecord('RUN_SUMMARY');
```

### Run summary

The `RUN_SUMMARY` key-value record contains per-post statuses plus run metrics: posts processed / partial / failed, comments saved, rows per post, profile-URL availability, duplicate rate, retries, runtime per 1,000 rows and which data source served each post.

Each post gets an entry in `posts`. A real example - a post with over 1,000 comments, of which the public page shows the top 10:

```json
{
    "inputUrl": "https://www.linkedin.com/feed/update/urn:li:activity:7514290662138068992/",
    "postId": "7514290662138068992",
    "status": "PARTIAL_RESULT",
    "reason": "only the top 10 publicly visible comment(s) were available (LinkedIn reports 1133)",
    "dataSource": "guest",
    "sortApplied": "RELEVANCE",
    "reportedCommentsCount": 1133,
    "rootCommentsFound": 10,
    "repliesFound": 0,
    "rowsSaved": 10
}
```

You are charged only for the 10 rows saved.

#### 🚦 Proxy policy

Use **Apify Datacenter** proxy (the default) or **no proxy** for normal runs - both work reliably for public LinkedIn post pages at this actor's conservative concurrency.

**Apify Residential proxy is not supported.** The actor will fail at startup if `apifyProxyGroups` includes `RESIDENTIAL`. Reason: in pay-per-event actors, residential bandwidth is billed to the developer, not the run user, so a single bandwidth-heavy run could exceed the per-result event revenue.

If you genuinely need residential routing, supply your own residential provider via the proxy editor's **Custom proxy URLs** field - that traffic goes through your provider, not Apify, and is unaffected:

```
http://user:pass@proxy.iproyal.com:12321
http://user:pass@proxy.brightdata.com:22225
http://user:pass@proxy.oxylabs.io:7777
```

### Limitations

- **Top comments only.** LinkedIn's public page shows a post's most relevant comments - typically about 10. Posts with more are returned with those comments and marked `PARTIAL_RESULT`. Every comment on a post, replies, newest-first order, commenter headlines, reaction breakdowns and pinned/edited flags are not available yet; those fields are `null`, never guessed.
- Only **public** posts can be read. Private, deleted or connection-only posts are reported as `POST_UNAVAILABLE` / `POST_NOT_FOUND` and never charged.
- Ranks reflect LinkedIn's order at scrape time and can change. Comments may later be edited or deleted, and engagement counts change over time.
- Individual reactor identities, full commenter profiles, sentiment and email enrichment are out of scope (see the related actors above).

### Compliance

This is an unofficial tool and is not affiliated with, endorsed by or sponsored by LinkedIn. It processes publicly available professional data only and does not log in, use cookies, bypass private content, message anyone or infer sensitive attributes. You are responsible for processing the data lawfully (e.g. GDPR/CCPA) in your downstream use.

# Actor input Schema

## `postUrls` (type: `array`):

Public LinkedIn post URLs, one per line. Supported forms:

- `https://www.linkedin.com/feed/update/urn:li:activity:<id>`
- `https://www.linkedin.com/posts/<author>_<slug>-activity-<id>-<hash>`
- `https://www.linkedin.com/posts/<author>_<slug>-ugcPost-<id>-<hash>`

Comment-specific links (`?commentUrn=...`) are normalised to their parent post, tracking parameters are stripped and duplicates are removed (first one wins). Profile, company and article URLs are skipped as invalid and never charged. Up to 1,000 posts per run.

## `maxCommentsPerPost` (type: `integer`):

Maximum number of top-level comments to collect per post. LinkedIn's public page shows about the 10 most relevant comments, so a post with more comments returns those ~10 and is marked `PARTIAL_RESULT` in the run summary. Replies do not count toward this limit. Ignored when "Scrape all comments" is on.

## `scrapeAllComments` (type: `boolean`):

Ignore the per-post limit and collect every top-level comment the post exposes. Deeper pagination is not available yet, so this currently returns the ~10 comments LinkedIn shows publicly.

## `sortOrder` (type: `string`):

Order in which comments are collected. `commentRank` in the output reflects this order. Newest-first is not available yet: comments come back in LinkedIn's relevance order and the post's entry in the run summary says so.

## `scrapeReplies` (type: `boolean`):

Return replies to each comment as their own rows (recordType REPLY, linked to their parent and root comment), billed exactly like comments. Not available yet: LinkedIn's public page does not show replies, so no reply rows are returned for now.

## `maxRepliesPerComment` (type: `integer`):

Maximum number of replies to keep per top-level comment. 0 = all available. Has no effect until replies are available.

## `includeReactionBreakdown` (type: `boolean`):

Split reactions into like / praise / empathy / appreciation / interest / other where LinkedIn exposes them. The public page shows only the total, so the breakdown fields are currently null - counts are never estimated.

## `includeMedia` (type: `boolean`):

Return images attached to comments (when public).

## `includeRawData` (type: `boolean`):

Add a `raw` field with the unprocessed source record for each row. Useful for debugging; makes the dataset larger.

## `maxConcurrency` (type: `integer`):

How many posts are processed at the same time. Higher is faster but more likely to be rate limited.

## `proxyConfiguration` (type: `object`):

Apify Proxy configuration. Defaults to Apify Proxy (datacenter). Apify Residential proxy is not supported - supply your own residential provider via Custom proxy URLs if you need one.

## Actor input object example

```json
{
  "postUrls": [
    "https://www.linkedin.com/feed/update/urn:li:activity:7514290662138068992/"
  ],
  "maxCommentsPerPost": 100,
  "scrapeAllComments": false,
  "sortOrder": "RELEVANCE",
  "scrapeReplies": true,
  "maxRepliesPerComment": 0,
  "includeReactionBreakdown": true,
  "includeMedia": true,
  "includeRawData": false,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `overview` (type: `string`):

One row per comment or reply: text, timestamp, commenter identity, reactions, reply relationships and source-post context.

## `runSummary` (type: `string`):

Per-post statuses (SUCCESS, PARTIAL_RESULT, NO_COMMENTS, POST_UNAVAILABLE, ...) and run metrics.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "postUrls": [
        "https://www.linkedin.com/feed/update/urn:li:activity:7514290662138068992/"
    ],
    "maxCommentsPerPost": 100,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("coregent/linkedin-post-comments-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "postUrls": ["https://www.linkedin.com/feed/update/urn:li:activity:7514290662138068992/"],
    "maxCommentsPerPost": 100,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("coregent/linkedin-post-comments-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "postUrls": [
    "https://www.linkedin.com/feed/update/urn:li:activity:7514290662138068992/"
  ],
  "maxCommentsPerPost": 100,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call coregent/linkedin-post-comments-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,coregent/linkedin-post-comments-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/IeTbOp2DhRO1thByd/builds/xhjC00ai4qK2aRVEA/openapi.json
