# LinkedIn Company Posts + Engagement (No Cookies) (`northbell/linkedin-company-posts-scraper`) Actor

Scrape a company's LinkedIn posts with exact reaction and comment counts — plus the posting date, which the public page does not show. Run it again and you get how fast each post is still gaining. No login, no cookies.

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

## Pricing

Pay per event

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?

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 Company Posts Scraper with Engagement Tracking

Scrape any company's recent LinkedIn posts — the text, whether it carries media, and the **exact** reaction and comment counts, not the `1K` the page displays.

Plus the two things the public page will not tell you: **when each post went up**, and **whether it is still gaining.**

### The page does not show the date. The post id does.

LinkedIn's public company page lists recent posts with no timestamp at all — not a date, not even "3 weeks ago". A post with 95 reactions could be from this morning or from March, and those are opposite facts.

The date is in the post id. LinkedIn ids carry a millisecond timestamp in their high bits, and this Actor recovers it:

```
urn:li:ugcPost:7496332962540589056  →  2026-08-20T22:30:53Z
urn:li:ugcPost:7488672358250233856  →  2026-07-30T19:10:23Z
```

The bit offset was determined by measurement, not by copying a blog post: across 27 real post ids from one company, a 22-bit shift put every single one inside a three-week window ending on the day they were fetched. A 23-bit shift — the value you would get from "41 bits of 64" — puts them all in **1998**.

And when an id does not decode to a plausible date, the Actor returns `postedAt: null` rather than shipping you a post from the last century.

### A post's reaction count is a snapshot of a moving thing

`95 reactions` on a post published four hours ago is a strong start. `95 reactions` on a post published three weeks ago is a post that died quietly.

Reaction counts only climb after publication, and **nothing back-fills them.** Run this Actor twice and you get what actually happened in between:

```
stripe · "Kraken is launching Krak Cards in the US..."
  posted   Aug 20 22:30
  Aug 23      101 reactions + comments
  Aug 24      441
  ────────────────────────────────
  +340 in 24h · still climbing
```

### What you get

Every run appends to your dataset. Rows are tagged by `type`.

**`post`** — one row per post.

| field | meaning |
|---|---|
| `postedAt` | recovered from the post id — the page has no date at all |
| `ageHours` | how old the post was when you observed it |
| `reactions`, `comments`, `engagement` | exact counts, read from the `aria-label`, not the rounded `1K` on screen |
| `engagementAdded`, `engagementPerDay` | how fast it is still gaining, across your own observations |
| `engagementGrowthReliable` | `false` when the figure cannot be trusted — see below |
| `engagementAddedSinceLastRun` | change since you last looked |
| `postsPerWeek`, `cadenceChange`, `cadenceDirection` | how often this company publishes, and whether that is rising or falling |
| `text`, `hasMedia`, `postUrl`, `observations` | the post itself |

**`vanished`** — a post that was in the company's recent list last run and is not now, with its last known engagement. It was either deleted or pushed out by newer posts, and either way you would otherwise lose the tail of its series.

**`error`** — anything that failed, written where you will actually see it.

### Three questions this answers that a snapshot cannot

**"Which post actually worked?"** — not the one with the highest count, the one with the highest count *for its age*. `engagementPerDay` ranks them honestly.

**"Has this company gone quiet?"** — `cadenceDirection` turns `down` when a company that was posting five times a week drops to one. Companies go quiet before they announce anything.

**"Is this post still alive?"** — a post gaining 300 a day is being circulated. One that has been flat for two days is finished, whatever its total says.

### Numbers this Actor refuses to give you

- **First observation** → no rate. One point has no slope, and inventing one would be a lie with a decimal point on it.
- **Under 6 hours of observation** → `observed-for-less-than-6-hours`. Engagement spikes immediately after publication; extrapolating the first hour to a daily rate produces a number that is wrong by an order of magnitude.
- **Engagement went down** → `engagement-went-down`. Reactions can be withdrawn and comments deleted; the Actor reports the fact and declines to call it a trend.
- **Fewer than two dated posts** → no cadence. You cannot measure an interval with one point.
- **All posts share a timestamp** → no cadence, rather than a division by zero dressed up as a number.

A rate without that flag is a rate that will eventually lie to you.

### No login. Not as a policy — as a property of the code.

This Actor never signs in, never asks you for a session cookie, and never sends one. It reads the public company page, the same one an anonymous visitor sees.

That is enforced, not promised:

- The request headers are a **frozen object** with no `Cookie` and no `Authorization` field, and nothing can add one at runtime.
- A guard rejects any attempt to attach a credential header, and **the input schema refuses any field** whose name looks like `cookie`, `session`, `token`, `auth` or `password`.
- Unit tests assert all of the above.

If a competing tool asks you to paste your `li_at` cookie, it is operating your LinkedIn account on your behalf. This one cannot, by construction.

### Two more things it gets right

**Exact counts, not the rounded ones.** The visible text says `1K`; the `aria-label` behind it says `1,234`. Diffing rounded numbers produces growth figures that are mostly rounding error, so this Actor reads the precise value.

**A failed fetch becomes a row, not a log line.** Nobody reads run logs. Failures land in the dataset as `error` rows, and the run is marked failed when a whole company produced nothing.

### Input

```json
{
  "companies": ["stripe", "https://www.linkedin.com/company/shopify"],
  "minEngagement": 0,
  "onlyTrackedPosts": false,
  "maxRequestsPerMinute": 20
}
```

One request per company, so 50 companies is 50 requests.

#### Daily runs

Run it on a schedule with the same company list. Each run adds a point to every post's series and to each company's cadence. Set `onlyTrackedPosts: true` if you would rather keep deepening the history you already have than pick up every new post.

### What you pay for

Pay per event, charged only for results actually delivered:

| event | when |
|---|---|
| Actor start | once per run |
| Post recorded | one post delivered with its counts and growth figures |

**A failed fetch is never charged**, and neither are posts filtered out by `minEngagement`. You are paying for rows you received.

### Limits worth knowing

- **About ten recent posts per company.** That is what LinkedIn puts on the public company page; the dedicated `/posts/` tab redirects to a login wall, so ten is the honest ceiling per run. Running daily accumulates far more than ten over time — which is the point.
- **Posting cadence is measured across those ten**, so it describes recent behaviour, not the company's whole history.
- **`postedAt` is derived, not published.** It has matched every id tested, and returns `null` rather than a guess when an id does not decode sensibly.
- The rate-limit budget persists in a key-value store, so overlapping runs of this Actor share one budget rather than stacking up.

### On data and privacy

This Actor collects **posts published by company pages**, not people. It does not read, store or return the identities of the individuals who reacted or commented — reactions and comments are counts and nothing else.

Post text passes through to your dataset but is **never written to the Actor's own history**. The persistent store holds numbers and identifiers only: post ids, counts, dates.

Images and video are not downloaded or redistributed; `hasMedia` is a boolean.

### Storage

History lives in a named key-value store, `linkedin-posts-history`, so it survives between runs. Deleting it resets the baselines — every post reports as a first observation again, and momentum goes quiet until it has two samples six hours apart.

### Running locally

```bash
npm install
npm test          # 30 unit tests, no network, including the no-login guarantees
```

# Actor input Schema

## `companies` (type: `array`):

Company handles or full LinkedIn URLs. The handle is the last part of the URL: linkedin.com/company/stripe → stripe.

## `minEngagement` (type: `integer`):

Skip posts with fewer than this many reactions plus comments. Leave at 0 to keep everything — you need the quiet posts too if you want an honest average.

## `onlyTrackedPosts` (type: `boolean`):

For daily runs. Skips posts you have never seen, so you only pay to keep watching the series you are already building.

## `maxRequestsPerMinute` (type: `integer`):

One request per company. Kept polite by default. The budget is shared across your runs of this Actor, so parallel runs do not stack up.

## Actor input object example

```json
{
  "companies": [
    "stripe",
    "shopify"
  ],
  "minEngagement": 0,
  "onlyTrackedPosts": false,
  "maxRequestsPerMinute": 20
}
```

# Actor output Schema

## `all` (type: `string`):

Every row: post, vanished, and error.

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

One row per post: text, exact reaction and comment counts, whether it carries media, and the posting date recovered from the post id.

## `momentum` (type: `string`):

How fast each post is still gaining reactions and comments, measured across your own observations.

## `cadence` (type: `string`):

How often each company publishes, and whether that rate is rising or falling since you last looked.

# 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 = {
    "companies": [
        "stripe",
        "shopify"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("northbell/linkedin-company-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 = { "companies": [
        "stripe",
        "shopify",
    ] }

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

```

## MCP server setup

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