# LinkedIn Company Scraper – Posts & Followers (`simple.actors/linkedin-company-posts`) Actor

Scrape any public LinkedIn company page: follower count, tagline, full description, logo, website, industry, size, headquarters, address and founding year, plus its latest posts with text, publish time, reactions and comments. No login, no API key — one request a company. Unofficial.

- **URL**: https://apify.com/simple.actors/linkedin-company-posts.md
- **Developed by:** [Simple Actors](https://apify.com/simple.actors) (community)
- **Categories:** Lead generation, Social media
- **Stats:** 4 total users, 3 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.00 / 1,000 company reads

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 Scraper – Posts & Followers

Scrape any public LinkedIn company page: its **follower count, tagline, full
description, logo, website, industry, size, headquarters and founding year**,
plus its **latest posts** with full text, exact publish time, reactions and
comments. Built for B2B lead research, competitor and brand
monitoring, CRM enrichment and social listening across many companies at once.

**One request a company.** Everything comes out of the single page LinkedIn
serves at `/company/<slug>` — there is no second call, no cursor and no API.
A company reads in a few seconds.

**$1 per 1,000 companies read** — Apify platform usage and proxy included,
nothing else to pay. The charge is per company and does not depend on how many
posts come back, so a full read and a check that finds one new post cost the
same. A company that could not be read is never charged.

**No login, no cookies, no API key, no account of yours involved.** It reads
what LinkedIn already renders for logged-out visitors, so there is no session to
keep alive and nothing of yours to get restricted.

*Unofficial. Not affiliated with, endorsed by, or sponsored by LinkedIn or
Microsoft.*

> **Read this before you start: company pages only.** Personal profiles
> (`linkedin.com/in/…`) are served behind a login and cannot be read — they come
> back as a row saying so. And a company page renders roughly **9 to 11 posts**
> to a logged-out reader, with no way to page further. This is built to check
> *many* companies *often*, not to pull one company's archive.

### Features

- **The company record** — name, follower count, tagline, description and logo.
  The description is the company's **full About-us text**, not the shortened
  version LinkedIn puts in the page's preview tag.
- **The whole About-us panel** — website, industry, company size, headquarters,
  postal address, organisation type, founding year and specialities. The website
  comes out unwrapped, as the company's own address rather than the LinkedIn
  redirect the page links to.
- **The latest posts** — up to 12, newest first, each with its text, permalink,
  publish time, reaction count and comment count.
- **Exact publish times on every post**, including the ones LinkedIn does not
  date in its structured data — the time is recovered from the post's own
  activity id.
- **Untruncated text where LinkedIn provides it**, flagged per post with
  `textIsComplete` so a shortened card can never pass for a full one.
- **Reposts flagged** — `isRepost` marks a post the company shared rather than
  wrote, whose reactions belong to the original.
- **Stable post ids** — deduplicate across scheduled runs on `id`.
- **Every URL form works** — full URLs, locale hosts (`uk.linkedin.com`),
  sub-pages (`/about/`), `/school/` pages, or a bare slug pasted from a
  spreadsheet.
- **Two output shapes** — one row per post, or one row per company with its
  posts nested. Same price.
- **Incremental polling** — `onlyPostsNewerThan` takes `3 days`, `12 hours` or
  an ISO date, and refuses rather than misleads when the window reaches past
  what one page shows.
- **Datacenter proxy is enough**, which is what keeps it cheap.
- **Runs stay green.** Every company that cannot be read — one that does not
  exist, a personal-profile URL, a window too wide, or LinkedIn refusing on the
  day — is a row, not a failed run.

### Input

```json
{
  "startUrls": [{ "url": "https://www.linkedin.com/company/dishoom" }],  // company URLs
  "companies": ["vercel", "stripe"],   // or bare slugs — both lists are read as one
  "outputFormat": "posts",             // "posts" = row per post, "company" = row per company
  "maxPosts": 12,                      // latest posts per company (12 is the ceiling)
  "onlyPostsNewerThan": "7 days",      // optional window: "12 hours", "3 days", "2026-08-01"
  "proxy": { "useApifyProxy": true }
}
```

The shortest useful input is a list of slugs:

```json
{ "companies": ["dishoom", "vercel", "stripe"] }
```

| Field | Type | Default | What it does |
|---|---|---|---|
| `startUrls` | array | — | Company pages as URLs. Locale hosts, sub-pages and `/school/` all work. |
| `companies` | array | — | The slug on its own — the part after `/company/` in the address bar. Read together with `startUrls`. |
| `outputFormat` | string | `posts` | `posts` gives one row per post; `company` gives one row per company with its posts nested. |
| `maxPosts` | integer | `12` | Latest posts per company. 12 is the ceiling — one page is all LinkedIn serves. |
| `onlyPostsNewerThan` | string | — | Keep only posts after a window or ISO date. |
| `proxy` | object | datacenter | Datacenter is what this is built and measured on. |

**The slug is often not the brand name.** It is whatever appears after
`/company/` in the address bar — check there rather than guessing, or you will
get a `not_found` row.

### Output

One row per post by default, with the company record repeated on each:

```json
{
  "type": "post",
  "id": "7495121826755391488",
  "url": "https://www.linkedin.com/posts/dishoom_everything-we-ate-and-drank-at-dishoom-leeds-activity-7495121826755391488-6YOH",
  "text": "\"Taking over the old Flannels site on Vicar Lane, Dishoom Leeds is nothing short of beautiful…\"",
  "textIsComplete": true,
  "postedAt": "2026-08-17T14:18:16.104Z",
  "postedAtIsApproximate": false,
  "reactions": 61,
  "comments": 3,
  "isRepost": false,
  "postAuthorName": "Dishoom",
  "postAuthorUrl": "https://uk.linkedin.com/company/dishoom",

  "companyUrl": "https://www.linkedin.com/company/dishoom",
  "companyName": "Dishoom",
  "companySlug": "dishoom",
  "followers": 34996,
  "tagline": "From Bombay with love",
  "description": "Dishoom is proud to be recognised as one of the best places to work in hospitality…",
  "logo": "https://media.licdn.com/dms/image/…/company-logo_200_200/…",
  "website": "http://careers.dishoom.com/",
  "industry": "Restaurants",
  "companySize": "1,001-5,000 employees",
  "employeesOnLinkedIn": 996,
  "headquarters": "London",
  "organizationType": "Privately Held",
  "foundedYear": 2010,
  "specialties": [],
  "address": {
    "street": "141-143 Shoreditch High Street",
    "city": "London",
    "region": null,
    "postalCode": "E1 6JE",
    "country": "GB",
    "raw": "141-143 Shoreditch High Street, London, E1 6JE, GB"
  },

  "scrapedAt": "2026-08-30T09:00:00.000Z"
}
```

Set `outputFormat` to `company` and each company is a single row instead, with
its posts under `posts` — the shape to pick when you are looking at companies
rather than collecting posts.

| Field | What it is |
|---|---|
| `id` | LinkedIn's activity id. Stable across runs — deduplicate on it. |
| `url` | Permanent link to the post. |
| `text` | The post's text. |
| `textIsComplete` | `true` when the text is untruncated. `false` means it is the card's shortened commentary — LinkedIn publishes the full text for only some posts. |
| `postedAt` | When it was published, ISO 8601 UTC. Never null. |
| `postedAtIsApproximate` | `true` when the time was decoded from the activity id rather than stated by LinkedIn. Accurate to a fraction of a second either way. |
| `reactions`, `comments` | Counts as LinkedIn displays them. **`null` means none was shown, never zero.** |
| `isRepost` | `true` when the company shared someone else's post. Its engagement belongs to the original — do not credit it to this company. |
| `followers` | The company's follower count, taken from the page's own metadata rather than from the related-companies sidebar, which carries other companies' counts. |
| `description` | The company's About-us text in full, however long it runs. |
| `tagline` | The one-line slogan under the company name. |
| `website` | The company's own site, with LinkedIn's redirect stripped off — ready to feed to anything that takes a URL. |
| `industry` | The industry the company files itself under, in LinkedIn's words. |
| `companySize` | The headcount band the company declares, e.g. `11-50 employees`. |
| `employeesOnLinkedIn` | How many employee profiles LinkedIn associates with the company. **A different fact from `companySize`** — Vercel declares `501-1,000` and has 1,013 profiles. |
| `headquarters` | The head-office city as LinkedIn shows it. |
| `address` | Street, city, region, postal code, country, and `raw` — the whole thing on one line. More precise than `headquarters`. `null` when the company publishes none. |
| `organizationType` | Privately Held, Public Company, Partnership, Nonprofit, and so on. |
| `foundedYear` | The year the company says it was founded. |
| `specialties` | The specialities the company lists, one per entry. `[]` when it lists none. |
| `scrapedAt` | When the run read the page. |

### How to use

#### From Apify Console

1. Open the Actor and click **Try for free** / **Start**.
2. Paste company URLs into **LinkedIn company URLs**, or a column of bare slugs
   into **Company slugs** — both lists are read together.
3. Choose the row shape in **Row shape**: one row per post for collecting posts,
   one row per company for watching companies.
4. To poll for what is new, set **Only posts newer than** to a window shorter
   than the gap between your runs, and schedule the run.
5. Click **Start**, then open the **Dataset** tab and export as JSON, CSV or
   Excel.

**Batch your companies** — one run with fifty slugs is far cheaper per company
than fifty runs of one, because each run pays its own startup.

#### From the API

```bash
curl -s "https://api.apify.com/v2/acts/simple.actors~linkedin-company-posts/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"companies": ["dishoom", "vercel", "stripe"]}'
```

Monitoring a set of companies, with the JavaScript client:

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('simple.actors/linkedin-company-posts').call({
    companies: ['dishoom', 'vercel', 'stripe'],
    onlyPostsNewerThan: '24 hours',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();

for (const row of items) {
    if (row.error) { console.warn(row.companySlug, row.error); continue; }
    if (row.isRepost) continue;              // engagement belongs to the original
    console.log(row.companyName, row.postedAt, row.reactions, row.text?.slice(0, 60));
}
```

### Use cases

- **B2B lead research** — pull follower count, industry, company size, founding
  year and headquarters across a list of target companies to size and qualify
  them.
- **Competitor content monitoring** — see what rival companies posted, when, and
  how it performed, on a schedule.
- **Brand and campaign tracking** — watch reactions and comments on a company's
  own posts over time.
- **CRM enrichment** — attach a follower count, description, industry, size and
  postal address to accounts you already hold, matched by slug.
- **Website discovery** — the `website` field turns a list of LinkedIn slugs
  into a list of company domains, which is what a contact or tech-stack lookup
  needs as its input.
- **Social listening across platforms** — combine with the Instagram, TikTok, X,
  YouTube and Facebook readers for one view of a company's presence.
- **Employer-brand research** — a company's own posts are what it chooses to say
  publicly; collect them across an industry.

### Usage notes

Simple, transparent pay-per-result pricing — no subscriptions.

#### What it costs

**$0.0010 per company read** — $1 per 1,000 companies. One charge per company,
whatever the run finds. There is no per-run fee and no per-post fee: reading a
company costs the same whether ten posts come back or one, so charging for them
would bill you for something that is not what costs.

Two things follow from that:

- **Batch your companies.** Fifty slugs in one run is much cheaper per company
  than fifty runs of one, because each run pays for its own startup.
- **Take all the posts.** Asking for fewer saves you nothing.

**A company that could not be read is never charged** — a slug that does not
exist, a personal-profile URL, a window too wide, or a page that could not be
reached. So a list carrying dead entries costs only for the live ones.

**What an empty result means.** A company that was read but has posted nothing
in your window still comes back as a **company row**, so an empty-looking result
can never be confused with a company that was not read. A company that could
*not* be read is its own row carrying `error` and `errorDescription`.

**The rows are the failure channel, not the run status.** The run succeeds even
when some — or all — of its companies could not be read, and the status message
counts them. No unreadable company fails a run: not one of them, and not all of
them at once. If you schedule this, treat any row with an `error` field as the
failure signal — a run status of SUCCEEDED does not by itself mean every company
came back.

| `error` | what happened | Run status |
|---|---|---|
| `not_found` | LinkedIn has no company page at that slug | Succeeds |
| `bad_input` | The entry was never a company page — a personal profile, or not a LinkedIn URL at all | Succeeds |
| `window_too_wide` | The window reaches further back than one page shows | Succeeds |
| `read_failed` | LinkedIn could not be reached after four attempts | Succeeds |

**`onlyPostsNewerThan` refuses rather than misleads.** A run sees one page of
posts and no further back. If your window reaches past them, posts inside it
were never fetched — and returning what was found would read as "this is
everything since then". So that company gets a `window_too_wide` row instead,
naming how far back it could actually see. A company that posts daily will
answer that for most windows; use a shorter one, or run more often.

**LinkedIn refuses some requests outright** with its own HTTP 999 status. That
refusal is random per request rather than tied to a company, so it is retried on
a fresh address up to four times — which cleared every refusal measured.

### Limits

- **Company pages only.** Personal profiles need a login and come back as a
  `bad_input` row explaining that.
- **Roughly 9 to 11 posts per company**, 12 at most. That is what LinkedIn
  renders to a logged-out reader; there is no cursor to ask for more. Run on a
  schedule to follow a company over time.
- **No comment text.** The page carries comment *counts*, not the comments.
- **Reactions are not broken down by type.** LinkedIn shows a single total to a
  logged-out reader.
- **Reposts carry the original's engagement.** They are flagged `isRepost` so
  you can exclude them.

### FAQ

**Is scraping LinkedIn legal?**
This Actor reads only what LinkedIn already renders to a logged-out visitor,
without logging in or using cookies. LinkedIn's User Agreement restricts
automated collection, so check the platform's ToS and your own obligations —
particularly around personal data — before using it, and run it at a volume and
rate you are comfortable defending.

**Do I need a LinkedIn login, cookies or an API key?**
No. There is no session, token or account of yours involved.

**Can it read personal profiles (`/in/…`)?**
No. LinkedIn serves those behind an authwall to a logged-out reader, so there is
nothing to return. Those entries come back as a `bad_input` row saying so rather
than as a silent failure.

**Can I get a company's full post history?**
No. One page renders roughly 9 to 11 posts and offers no way to page further
without a login. This Actor is built to check many companies often; for one
company's archive it is the wrong tool.

**Why did I get `not_found` for a company I can see on LinkedIn?**
Almost always the slug. It is the part after `/company/` in the address bar and
is frequently not the brand name — check there rather than guessing.

**Why is `reactions` null on some posts?**
Because LinkedIn showed no count for that post. Null means "none was shown",
never zero, so a count of 0 and an absent count stay distinguishable.

**What is `postedAtIsApproximate`?**
LinkedIn states an exact timestamp for only some posts. For the rest the time is
decoded from the post's own activity id, which encodes its publish moment —
measured within a fraction of a second of LinkedIn's own figure. The flag tells
you which you got; both are reliable enough to sort and filter on.

**Why did my run succeed when a company was not read?**
By design. Every company that cannot be read comes back as a row with an `error`
field — whether that is an answer about the entry (missing, a personal profile,
a window too wide) or LinkedIn refusing the read on the day — and none of them
turns the run red. Treat those rows as the failure signal rather than the run
status, and read the run's status message for the counts.

### Note

This Actor is **not affiliated with, endorsed by, or sponsored by LinkedIn or
Microsoft**. It reads publicly visible company pages without logging in, and
cannot reach personal profiles, private pages, connections or anything else
behind a login.

# Actor input Schema

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

Public LinkedIn company pages, e.g. https://www.linkedin.com/company/dishoom. Locale hosts (uk.linkedin.com), sub-pages (/about/) and /school/ pages all work and are normalised for you. Personal profiles (/in/…) cannot be read — LinkedIn serves those only behind a login — and come back as a row saying so rather than failing the run.

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

The slug on its own — "dishoom", "vercel", "stripe" — which is easier to paste as a column out of a spreadsheet. The slug is the part after /company/ in the address bar, and it is often not the brand name.

## `outputFormat` (type: `string`):

**One row per post** (default) gives every post its own row with the company record repeated on each — the shape to pipe into a warehouse. **One row per company** gives each company a single row with its posts nested under `posts`, which is the one to pick when you are looking at companies rather than collecting posts. Same price either way.

A company that was read but has posted nothing in your window always comes back as a company row, so it can never be confused with one that was not read.

## `maxPosts` (type: `integer`):

How many of the latest posts to return per company, newest first. A LinkedIn company page renders roughly 9 to 11 posts to a logged-out reader and offers no way to page further without a login, so 12 is the ceiling rather than a target. Lowering it does not make the run cheaper — the charge is per company.

## `onlyPostsNewerThan` (type: `string`):

Keep only posts published after this point — ideal for incremental polling. Takes an ISO date like 2026-08-01, or a window like "3 days", "12 hours", "last 2 weeks". Because a run reads one page, a window reaching further back than that page shows returns an error row for that company (window\_too\_wide) rather than a short list that would read as a complete answer. The run itself still succeeds.

## `proxy` (type: `object`):

Defaults to Apify's datacenter proxy, which is what this Actor is built and measured on: 16 of 16 valid company pages answered over datacenter, and LinkedIn's occasional 999 refusal is random per request rather than tied to the company, so a retry on a fresh address clears it. Switch to residential only if companies start coming back as read failures.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.linkedin.com/company/dishoom"
    }
  ],
  "companies": [
    "dishoom"
  ],
  "outputFormat": "posts",
  "maxPosts": 12,
  "onlyPostsNewerThan": "7 days",
  "proxy": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

One row per post with its text and engagement.

## `companies` (type: `string`):

The company record behind these posts.

## `engagement` (type: `string`):

Reactions and comments per post, for ranking or trend work.

## `raw` (type: `string`):

Every field of every item, exactly as pushed.

# 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": [
        {
            "url": "https://www.linkedin.com/company/dishoom"
        }
    ],
    "companies": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("simple.actors/linkedin-company-posts").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": [{ "url": "https://www.linkedin.com/company/dishoom" }],
    "companies": [],
}

# Run the Actor and wait for it to finish
run = client.actor("simple.actors/linkedin-company-posts").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": [
    {
      "url": "https://www.linkedin.com/company/dishoom"
    }
  ],
  "companies": []
}' |
apify call simple.actors/linkedin-company-posts --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,simple.actors/linkedin-company-posts"
        }
    }
}

```

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/ec6bQMQp3BScX7eDn/builds/UVXaoyTUDKeiP4d4j/openapi.json
