# LinkedIn Group Posts Scraper — Public Groups, New Posts Only (`steadyfetch/linkedin-group-posts-scraper`) Actor

LinkedIn group posts from any PUBLIC group link as rows — the group, post text, a real publish timestamp, likes, comments, shares, reactions, media and article links, and the member who posted. No login, no cookies. A daily run returns only new posts; a private group is never charged.

- **URL**: https://apify.com/steadyfetch/linkedin-group-posts-scraper.md
- **Developed by:** [Steadyfetch Team](https://apify.com/steadyfetch) (community)
- **Categories:** Social media, Lead generation, Automation
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 posts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Group Posts Scraper — Public Groups, New Posts Only

Paste a public LinkedIn group and get its posts as rows: the group, what each member wrote, when it was really published, how many likes, comments and shares it has, the reaction breakdown, the images, video, document and article links attached to it, and the member who posted it. No login, no cookies, no proxy to configure. **Run it daily and it returns only the posts that are new since your last run** — posts your account already has are skipped and never charged twice. Your limit is exact and it is per group. Click Start with nothing set and you get a built-in sample: 3 posts from one public group, served from a frozen copy rather than fetched from LinkedIn, and nothing is charged for it.

**Using an AI agent?** Pin this actor in Apify's MCP server with one link: `https://mcp.apify.com?tools=steadyfetch/linkedin-group-posts-scraper`

- **Actor id:** `steadyfetch/linkedin-group-posts-scraper`
- **Input:** `{ "startUrls": ["https://www.linkedin.com/groups/1898033/"] }` — the one field you have to set: public group links, or the group numbers on their own. Add `"postsPerTarget": 100` for the per-group limit and `"maxItems": 1000` for the whole run.
- **Public groups only:** LinkedIn shows a private group's posts to its members only, and most groups are private. Each group is looked up first, and a private one comes back as one row that costs nothing.
- **Cap the bill:** set `maxTotalChargeUsd` on the run (a run option, not Actor input), e.g. `1.00` — the run stops when it reaches it.
- **Price:** from **$2.00 per 1,000 posts** on Gold and above, $3.00 per 1,000 on the Apify free plan, plus one **$0.012 group check** for each public group a run reads. Full table below.
- **How often it changes:** a large public group posts dozens of times a day, a small one a few times a week, and like and comment counts keep moving for about 72 hours. A daily run is the shape that fits a busy group.

*Unofficial, not affiliated with LinkedIn — not endorsed by or sponsored by LinkedIn or Microsoft Corporation, nor by any group or member whose public posts appear in the built-in sample.*

### The publish time is the real one

Every row carries `postedAt` — the moment LinkedIn itself stamped on the post, to the millisecond — and not a reading of a "2 weeks ago" label. You can check it yourself: the timestamp is the same epoch LinkedIn encodes inside the post's own id. `postedAgo` carries the label too, as a second column, never as the date.

### What you get

One row per post.

| Column | What it holds |
|---|---|
| `groupId` · `groupName` · `groupUrl` · `groupMembers` | the group the post was made in |
| `postId` · `postUrl` · `shareUrl` · `shareUrn` | the post's own ids and its permalink |
| `text` | what was written, in full |
| `postedAt` · `postedAtTimestamp` · `postedAgo` | the real publish moment, and LinkedIn's own label beside it |
| `isPinned` | the group's admin pinned this post to the top of the group |
| `authorName` · `authorHandle` · `authorUrl` · `authorId` · `authorHeadline` · `authorAvatarUrl` | the member who posted it |
| `likes` · `comments` · `shares` · `reactions[]` | the engagement, with the per-type reaction breakdown |
| `countsHidden[]` | which counts LinkedIn is hiding, so a hidden total never reads as a zero |
| `imageUrls[]` · `videoUrl` · `videoThumbnailUrl` · `documentUrl` | the media attached to the post |
| `articleTitle` · `articleLink` · `articleDescription` | the link preview, when the post shares one |
| `isRepost` · `repostOfPostId` · `repostOfPostUrl` · `repostOfAuthorName` · `repostOfPostedAt` | a repost shared into the group, with the original underneath it |
| `targetUrl` | the group link you asked for |
| `charged` · `chargedEvents` · `status` · `statusReason` | on every row, so the invoice reconciles from the dataset |

### New posts only, on a schedule

- Every run remembers the posts it delivered to your account, in the key-value store `linkedin-group-posts-account` in your own Apify account. A later run skips them: not delivered again, not charged, and the status line says how many.
- LinkedIn lists a group's posts newest first, so once a page shows posts you already have, the run reads no older page for that group. A daily run on a busy group reads one or two pages and returns what is new.
- A group that had nothing new for your account is left alone by your runs for up to 24 hours under the same dates and settings: it gets one uncharged `skipped.all_repeat` row saying when it was checked and when a run reads it again, and it pays no group check.
- Turn on "Include posts you already have" to get them again (marked `repeat: true`, never charged), or set "Posted before" to reach older posts. Posts older than 90 days in your account's memory count as new again.
- **Image, video, document and avatar links are signed by LinkedIn and expire within days.** Re-run for fresh links.
- The memory is a key-value store in your own Apify account, so a run started with a scoped API token in restricted-access mode needs key-value store Read, Write and Create permission, or Actor runs set to Full access; without it the run says so on its status line and cannot skip what you already have.

### Price

Two charged events. The store card shows **from $2.00/1,000 posts** — the cheapest tier.

| Apify plan | Per post | 1,000 posts | Group check |
|---|---|---|---|
| Free plan | $0.003 | $3.00 | $0.012 |
| Bronze | $0.0026 | $2.60 | $0.012 |
| Silver | $0.0023 | $2.30 | $0.012 |
| Gold and above | $0.002 | $2.00 | $0.012 |

**The group check** is charged once for each public group a run reads from LinkedIn and finishes, whatever it finds — new posts or none. Every read of a group costs us the same before a single post is known, so a group with one new post pays the same check as a group with a hundred; more posts never cost you less than fewer. It is never charged for a private group, a link that is not a group, a group your runs left alone because it had nothing new in the last 24 hours, a group that lists no posts at all, a group a stop cut before it finished, or the built-in sample. The row that closes the group says when it paid one.

No start fee. No second charge for the media, the article preview, the reaction breakdown or the member — they are columns on the one row you already paid for, and a repost is one post at the same price.

`maxTotalChargeUsd` is a run option, not an input field, and it is a hard ceiling: the run stops collecting when it reaches it, writes out everything already in hand, and books the rest as rows that name the cap and cost nothing.

**On the Apify free plan there is a daily allowance.** One account makes at most 25 reads of LinkedIn groups in any 24 hours (each group a run reads takes its lookup and a page of up to 100 posts, so that covers about a dozen groups), and all accounts on the Apify free plan share a daily allowance on this actor that resets at midnight UTC. Past either, the run stops cleanly: posts already delivered stay delivered, nothing more is read or charged, and each group it did not reach gets an uncharged `skipped.free_plan_allowance` row naming the reset time in UTC. Paid Apify plans carry no daily allowance.

### Honest rows

None of these charges a post, and every one arrives as a row you can read rather than as a missing line. The row's `status` is the first column; `statusReason` is the sentence.

| `status` | What the row says |
|---|---|
| `group_private` | This group is private: LinkedIn shows its posts to members only, so only public LinkedIn groups can be read. Nothing was charged for it. |
| `group_not_found` | LinkedIn has no group with this number — check the link. Nothing was charged for it. |
| `no_data` | The group is public and LinkedIn shows no more posts in it. |
| `skipped_date_window` | The group has posts, but none of the ones left fall inside your dates — widen "Posted after" or "Posted before". |
| `skipped.all_repeat` | The group reached posts your account already had, so the run read no older page — or it had nothing new for your account when checked in the last 24 hours, so it was not read again. |
| `skipped.page_cap` | The read reached the 200 pages this actor reads for one group while LinkedIn was still serving — this actor's own bound. |
| `input_error` | The link is not a LinkedIn group. A person's or a company's page says so and names the actor that reads it; every such row names the link that works. |
| `feed_unavailable` | LinkedIn did not answer for this group on this run. Please re-run — nothing was charged. |
| `limit_reached` | This run reached its limit (a new run fetches the rest), or this actor is paused for now — the row says which. |
| `skipped_max_items` · `skipped_budget` · `skipped_deadline` | One of your own limits or the run clock stopped the run first, and the row names which. |
| `skipped.free_plan_allowance` | On the Apify free plan only: the plan's daily allowance on this actor was reached before this group. The row names the reset time in UTC. |

A group that was read and finished pays its one group check on the row that closes it (or on one note row when its whole share was delivered), and that row says so.

### The links it reads

`https://www.linkedin.com/groups/<number>/` and anything under it, the old `/groups/<Name>-<number>/` form, a link to a post inside a group (the group is named in it), a `lnkd.in` short link that leads to a group, and the group number on its own. Tracking parameters and a country subdomain make no difference. Posts on a person's profile or a company page are [LinkedIn profile and company posts](https://apify.com/steadyfetch/linkedin-posts-scraper).

### Use it as an API endpoint (Standby)

You can also call this actor like an API: send up to five public group links and get their latest posts back in the same response, with no run to start and no dataset to read. The address is `https://steadyfetch--linkedin-group-posts-scraper.apify.actor`, and the actor's **Endpoints** tab lets you try it from the browser.

```bash
curl -H "Authorization: Bearer <YOUR_APIFY_TOKEN>" \
  "https://steadyfetch--linkedin-group-posts-scraper.apify.actor/?startUrls=https://www.linkedin.com/groups/1898033/&postsPerTarget=20"
```

| Setting | What it does |
|---|---|
| `startUrls` | A public group link or number. Repeat it for more groups: up to 5 per request (1 on the Apify free plan). |
| `postsPerTarget` | How many of the latest posts to return from each group. 100 if you leave it out; at most 100 per group per request (25 on the Apify free plan). |
| `maxItems`, `postedAfter`, `postedBefore`, `includeReposts` | The same settings as on a run, read the same way. |

A POST with the same fields as a JSON body works too. The answer is JSON: `rows` (the same rows a run writes to its dataset, the run summary last), `delivered`, `charged`, and `notes` for anything that was not returned. Each request is charged exactly as a run with the same input: per post returned, and one group check per group read. If the charge for a request cannot be completed, its posts are not sent.

Good to know: the endpoint has no account memory, so it does not skip posts you already have. For a daily watch that returns only new posts, run the actor on a schedule.

### Other scrapers you might want

- Posts from a person's profile or a company page: [LinkedIn profile and company posts](https://apify.com/steadyfetch/linkedin-posts-scraper)
- Public job postings with their full descriptions: [LinkedIn jobs](https://apify.com/steadyfetch/linkedin-jobs-scraper)

### Limits and good manners

Only public groups are returned — a private group is an uncharged row, not a workaround. This actor may stop working if LinkedIn changes how the data is served; if it does, items that fail are never charged.

### Support

Something off, or a column you need that is not here? Open an issue on the Issues tab — we usually reply within a couple of hours.

If it earned its keep, a rating helps other buyers find it, and saving the actor keeps it one click away.

# Changelog

This Actor's version history is a separate document: https://apify.com/steadyfetch/linkedin-group-posts-scraper/changelog.md

# Actor input Schema

## `startUrls` (type: `array`):

Public LinkedIn group links, one per line — https://www.linkedin.com/groups/<number>/ (the old /groups/<Name>-<number>/ form, a group post link and the bare number work too). A private group comes back as an uncharged row: LinkedIn shows its posts to members only. A profile or company page is refused with a pointer to the actor that reads it. Also answers to `urls`, `links`, `groupUrls` and `groups`. Leave it empty and click Start for a small built-in sample that charges nothing.

## `postsPerTarget` (type: `integer`):

Hard cap on the posts delivered for EACH group — three groups at 100 collect up to 300 in total. Any number from 1 to 500 is used exactly as typed, and only delivered posts are charged (plus one group check per group read); a number above 500 is moved to 500 and one uncharged row says so. A group that runs short leaves an uncharged row saying so. Also answers to `postsPerGroup`, `resultsLimit`, `limit`, `count`, `maxPosts` and `maxResults`; the smallest limit present binds.

## `maxItems` (type: `integer`):

Hard ceiling across every group in the run, whatever the per-group limit says — between 1 and 100,000, and a number outside that is moved to the nearest end on an uncharged row instead of the run being refused. Leave it empty for 1,000. The smallest limit present binds, and one uncharged note row says which name was read and what it was read as.

## `postedAfter` (type: `string`):

Keep only posts published on or after this moment: a date (2026-08-01, 1 Aug 2026, or 01.08.2026 day first) or a span counted back from now (24h, 7 days, 2 weeks, 3 months, 1 year, 7d, last month, today, yesterday). LinkedIn lists a group's posts newest first, so the read stops once a page is mostly older than this — a narrow window costs less, not more.

## `postedBefore` (type: `string`):

Keep only posts published before this moment: a date (2026-09-01, 1 Sep 2026, or 01.09.2026 day first) or a span counted back from now (24h, 7 days, 2 weeks, 3 months, 1 year, 7d, last month, today, yesterday), the same forms as "Posted after". Pair it with "Posted after" for a closed window. A post outside the window is dropped before it takes a slot, so it is never delivered and never charged.

## `includeReposts` (type: `boolean`):

ON (default): a repost shared into the group is delivered as a row marked `isRepost: true`, carrying the original post's id, link, author and its own publish time. OFF: reposts are dropped before they take a slot.

## `maxRunSeconds` (type: `integer`):

The run stops cleanly before this many seconds and reports what is left, instead of being killed by a timeout — between 30 and 7,200, and a number outside that is moved to the nearest end on an uncharged row. A time limit ends the collecting, never the delivering: posts already in hand are always written out.

## `includeSeen` (type: `boolean`):

OFF (default): a post already delivered to your account is skipped — no row, no charge. LinkedIn lists a group's posts newest first, so once a page shows posts you already have the run reads no older page for that group: you get what is new since your last run, and the rest of the number you asked for closes on one uncharged `skipped.all_repeat` row. A group that had nothing new for your account is not read again for up to 24 hours under the same dates and the same setting, and one uncharged row says when. ON: those posts are delivered again, marked `repeat: true`, still not charged, and the run keeps reading older pages. Posts older than 90 days in your account's memory count as new again.

## `resumeFromDatasetId` (type: `string`):

The dataset ID of a previous run of this actor. Posts already in it are treated as posts you already have and are skipped, so a follow-up run collects only what is new even from a different Apify account. Selecting it here is what grants this run permission to read it.

## Actor input object example

```json
{
  "startUrls": [],
  "postsPerTarget": 100,
  "maxItems": 1000,
  "includeReposts": true,
  "maxRunSeconds": 1800,
  "includeSeen": false
}
```

# Actor output Schema

## `posts` (type: `string`):

One row per post delivered from a public LinkedIn group: the group's id, name, link and member count, the post's own id and permalink, the full text, the real publish timestamp LinkedIn stamps on it (to the millisecond, not a "2 weeks ago" label), whether the group's admin pinned it, likes, comments, shares and the per-type reaction breakdown, the image, video, document and article links attached to it, the member who posted it with their profile link and headline, and — when the post is a repost — the original post's id, link, author and publish time. Every row carries `charged`, `chargedEvents` and `status`, so the invoice reconciles from the dataset itself: a delivered post is charged per post, each group a run reads pays one group check on the row that closes it, and starting a run costs nothing.

## `misses` (type: `string`):

One uncharged row for every group that returned less than it was asked for, saying which of these happened: `group_private` (LinkedIn shows this group's posts to members only — only public groups can be read), `group_not_found` (LinkedIn has no group with this number), `no_data` (the group is public and LinkedIn shows no more posts in it), `skipped_date_window` (none of the posts left fall inside your dates), `skipped.all_repeat` (the group reached posts your account already had, so the run read no older page, or it had nothing new for your account when checked in the last 24 hours), `skipped.page_cap` (this actor's own page bound for one group), `input_error` (the link is not a LinkedIn group — the row names the link that works), `feed_unavailable` (the read failed on this run and a re-run is the fix), `limit_reached` (this run reached its limit and a new run fetches the remaining posts, or this actor is paused for now — the row says which), `skipped_max_items` / `skipped_budget` / `skipped_deadline` (one of your own limits or the run clock stopped it first), and `skipped.free_plan_allowance` (on the Apify free plan only, the plan's daily allowance on this actor was reached). None of these rows charges a post; a group that was read and finished pays its one group check on its closing row, and that row says so.

## `summary` (type: `string`):

Posts delivered, what was asked for, posts your account already had, what stopped the run, and the charged-event totals.

# 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 = {
    "startUrls": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadyfetch/linkedin-group-posts-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 = { "startUrls": [] }

# Run the Actor and wait for it to finish
run = client.actor("steadyfetch/linkedin-group-posts-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 '{
  "startUrls": []
}' |
apify call steadyfetch/linkedin-group-posts-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadyfetch/linkedin-group-posts-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/ifJzoGBj9wgxlaRBe/builds/BHkcEMj9cjH8PEFtI/openapi.json
