# LinkedIn Posts Scraper — LinkedIn Profile Posts, LinkedIn Feed (`steadyfetch/linkedin-posts-scraper`) Actor

LinkedIn feed of any profile or company URL as rows — post text, a real publish timestamp to the second, likes, comments, shares, the reaction breakdown, images, video and article links, and the author. No login, no cookies. A page with nothing public is never charged.

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

## Pricing

from $1.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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## LinkedIn Posts Scraper — LinkedIn Profile Posts, LinkedIn Feed

Paste a LinkedIn company page or a person's profile and get that page's posts as rows: what was written, 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 author. No login, no cookies, no proxy to configure. **Your limit is exact and it is per page — set 50 and you get up to 50 from each link, never more**, and a page holding fewer says so on a row that costs nothing. A post we could not deliver is never charged, and nothing is charged for starting a run. Click Start with nothing set and you get a built-in sample: 3 posts from one public company page, 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-posts-scraper`

- **Actor id:** `steadyfetch/linkedin-posts-scraper`
- **Input:** `{ "startUrls": ["https://www.linkedin.com/company/microsoft/", "https://www.linkedin.com/in/<public-identifier>/"] }` — the one field you have to set. A company page and a person's profile can sit in the same list, each classified by its own path. Add `"postsPerTarget": 50` for the per-page limit and `"maxItems": 1000` for the whole run.
- **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.

*Unofficial. Not affiliated with, endorsed by, or sponsored by LinkedIn or Microsoft Corporation, nor with any company or person whose public posts appear in the built-in sample.*

### The publish time is the real one

Read this first, because it is the one thing that separates these rows from most of the field. 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 activity id. `postedAgo` carries the label too, as a second column, never as the date.

### What you get

One row per post, at one price.

| Column | What it holds |
|---|---|
| `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 |
| `authorType` · `authorName` · `authorHandle` · `authorUrl` · `authorId` | who posted it, and whether that is a person or a company |
| `authorHeadline` · `authorFollowers` · `authorAvatarUrl` | the headline on a person, the follower count on a company |
| `likes` · `comments` · `shares` · `reactions[]` | the engagement, with the per-type reaction breakdown |
| `countsHidden[]` | which counts LinkedIn is hiding on this page, 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, with the original underneath it |
| `newsletterTitle` · `newsletterUrl` | when the post is a newsletter issue |
| `targetUrl` | the page you asked for |
| `charged` · `status` · `statusReason` | on every row, so the invoice reconciles from the dataset |

### Hidden counts are never zeros

LinkedIn lets a page hide its reaction, comment, repost or view counts. When it does, the number in this dataset is `null` and `countsHidden` names which one is hidden — never `0`. A zero that means "we could not see it" is the same false precision as a date reconstructed from a label, and this actor does not ship either.

### How often the data changes

- A company page posts daily to weekly; a person weekly to monthly. A daily run on a company page and a weekly one on a person is usually the right shape.
- Like and comment counts keep moving for about 72 hours after a post goes up, then settle.
- **Image, video, document and avatar links are signed by LinkedIn and expire within days.** Re-run for fresh links; do not store them and expect them to keep working.
- Re-runs skip the posts your account already had, so a repeat run pays only for what is new. That memory lives in the key-value store `linkedin-posts-account` in your own Apify account, and posts older than 90 days in it count as new again.
- 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 (Write is what lets the run remember what it delivered; without it every run pays again); without it the run says so on its status line and cannot skip what you already have.

### Price

One charged unit is one post delivered. The store card shows this as **from $1.00/1,000 posts** — the cheapest tier, per 1,000 posts.

| Apify plan | Per post | 1,000 posts |
|---|---|---|
| Free plan | $0.0018 | $1.80 |
| Bronze | $0.0015 | $1.50 |
| Silver | $0.0012 | $1.20 |
| Gold and above | $0.0010 | $1.00 |

No start fee. No fee for a page that returned nothing. No second charge for the media, the article preview, the reaction breakdown or the author — they are columns on the one row you already paid for, and a repost is one post at the same price with the original underneath it.

`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.

### Honest rows

Nothing below is charged, and every one of them 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 |
|---|---|
| `profile_not_found` | LinkedIn does not list a public page at this URL, or it cannot be read without a login — nothing was charged for it. |
| `page_not_found` | The same answer on a company or showcase page: LinkedIn does not list one publicly at this URL. |
| `no_data` | This page exists and LinkedIn shows no public posts on it — nothing was charged. |
| `skipped.page_cap` | The walk reached the 200 pages this actor reads for one link and LinkedIn was still serving — that bound is this actor's own, not a verdict about the page, so one link can never spend a whole run. The row says what to change. Nothing was charged for the posts it did not deliver. |
| `input_error` | The link is not a LinkedIn profile or company page. A link to a single post says so and names the fix: paste the author's page URL instead. The row names the field the link arrived in, never what you typed. |
| `feed_unavailable` | LinkedIn did not answer for this page on this run. Please re-run — nothing was charged. If posts had already been delivered before it stopped answering, the row opens with how many and what they cost, and the re-run is for the rest. |
| `vendor_out_of_credit` · `vendor_budget` | The data feed behind this actor is out of allowance for now; nothing was charged. |
| `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. |

Posts your account already had are skipped rather than delivered twice, and are never charged either — unless the token that started the run cannot open key-value stores (see the scoped-token note above), and then the run says so on its status line, on a note row and on every row it charged.

### A link to one post is not this actor's door

This actor reads a PROFILE or a COMPANY page and returns that page's posts. A link to a single post — `/feed/update/…` or `/posts/…-activity-…` — comes back as an uncharged row telling you to paste the author's page instead.

### Other scrapers you might want

- Public job postings with their full descriptions: [LinkedIn jobs](https://apify.com/steadyfetch/linkedin-jobs-scraper)
- The spoken words inside a LinkedIn video ad: [LinkedIn ad transcripts](https://apify.com/steadyfetch/linkedin-ads-transcript-scraper)
- Career pages on a company's own domain: [Career sites by domain](https://apify.com/steadyfetch/company-jobs-by-domain)

### Limits and good manners

Only public pages are returned — a page behind a login 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.

# Actor input Schema

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

LinkedIn profile and company page links mixed in one list — https://www.linkedin.com/company/microsoft/, https://www.linkedin.com/showcase/<name>/ or https://www.linkedin.com/in/<public-identifier>/. A link to a single POST is not a page: it comes back as an uncharged row telling you to paste the author's page instead. Also answers to `urls` and `links`. Leave it empty and click Start for a small built-in sample of what a row looks like — that sample collects nothing and charges nothing.

## `profileUrls` (type: `array`):

People's LinkedIn profiles, one per line — a full https://www.linkedin.com/in/<public-identifier>/ link or the bare public identifier on its own. A profile that is not public, or that LinkedIn does not list, comes back as an uncharged row. Use this field when your list is people only; otherwise put everything in "LinkedIn page URLs".

## `companyUrls` (type: `array`):

LinkedIn company and showcase pages, one per line — a full https://www.linkedin.com/company/<name>/ link or the bare universal name on its own. A page with no public posts comes back as an uncharged row. Use this field when your list is companies only; otherwise put everything in "LinkedIn page URLs".

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

Hard cap on the posts delivered for EACH page you name — three pages at 50 collect up to 150 in total. Posts arrive from LinkedIn in pages of 50, so the smallest slice is 25 and the most this actor takes from one page in a run is 500; a number outside that is moved to the nearest end and one uncharged row says so, rather than the run being refused before it starts. A page that runs short leaves an uncharged row saying so. Also answers to `resultsLimit`, `limit`, `count`, `maxPosts` and `maxResults` — the names other LinkedIn post scrapers use — and the smallest limit present binds.

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

Hard ceiling across every page in the run, whatever the per-page 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: an ISO date (2026-08-01) or a window counted back from now — 24h, week, month, 3months, year. The walk stops as soon as a page's oldest post is older than this, so a narrow window costs less, not more.

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

Keep only posts published before this moment: an ISO date (2026-09-01) or a window counted back from now — 24h, week, month, 3months, year, 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 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, so the page's own writing fills the count instead.

## `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 — and the next post takes its place, so you still get the number you asked for. ON: those posts are delivered again anyway, marked `repeat: true`, and still not charged. Posts older than 90 days in your account's memory count as new again either way.

## `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": [],
  "profileUrls": [],
  "companyUrls": [],
  "postsPerTarget": 50,
  "maxItems": 1000,
  "includeReposts": true,
  "maxRunSeconds": 1800,
  "includeSeen": false
}
```

# Actor output Schema

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

One row per post delivered from a LinkedIn profile or company page: 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), likes, comments, shares and the per-type reaction breakdown, the image, video, document and article links attached to it, the author with their page link and follower count or headline, and — when the post is a repost — the original post's id, link, author and publish time. Where LinkedIn hides a count on a page, the number is null and `countsHidden` names which count is hidden, so a hidden total never reads as a zero. Every row carries `charged` and `status`, so the invoice reconciles from the dataset itself: only rows with charged = true were billed, and starting a run costs nothing.

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

One uncharged row for every page that returned less than it was asked for, saying which of these happened: `profile_not_found` / `page_not_found` (LinkedIn does not list a public page at this URL, or it cannot be read without a login), `no_data` (the page is real and LinkedIn shows no public posts on it), `skipped.page_cap` (the walk reached the 200 pages this actor reads for one link while LinkedIn was still serving — this actor's own bound, never a verdict about the page), `input_error` (the link is not a LinkedIn profile or company page — a link to a single post says so and names the field), `feed_unavailable` (the read failed on this run and a re-run is the fix), `vendor_out_of_credit` / `vendor_budget` (this actor reached its own collection allowance), and `skipped_max_items` / `skipped_budget` / `skipped_deadline` (one of your own limits or the run clock stopped it first). None of these is charged.

## `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": [],
    "profileUrls": [],
    "companyUrls": []
};

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

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

```

## MCP server setup

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