# LinkedIn Profile Posts Lookup: posts scraper from $3/1k (`accountable_eel/linkedin-profile-posts-lookup`) Actor

LinkedIn posts scraper with no cookies and no account. Give it public LinkedIn profile links and get one flat row per post: link, exact timestamp, text, reaction and comment counts, media type and repost flag. Nothing found, nothing charged.

- **URL**: https://apify.com/accountable\_eel/linkedin-profile-posts-lookup.md
- **Developed by:** [Adrian Voss](https://apify.com/accountable_eel) (community)
- **Categories:** Lead generation, Social media
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.28 / 1,000 post returneds

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 Profile Posts Lookup: LinkedIn Posts Scraper, No Login

Give it public LinkedIn profile links and get one flat row per post: the permalink, an exact
timestamp, the post text, reaction and comment counts, what media it carried, whether it links
out, and whether it is a repost of someone else. No account, no cookies, no session token. You
pay per post that came back, and a profile that returns nothing costs nothing.

### Who it's for

The people who need this are usually watching a short list of named humans rather than crawling
LinkedIn at large.

- **Founders and demand-gen teams running a warm-outbound motion.** You have thirty prospects in
  a table and you want a first line that references what each of them actually said last week,
  not a merge field. One run gives you their recent posts with dates you can sort by.
- **Competitive and partner intelligence.** Track what the four executives at a competitor are
  publishing, how it is landing, and what they are amplifying from other people. The repost flag
  and the original author column make "what are they signal-boosting" a filter rather than a
  reading exercise.
- **Creator and influencer scouting.** Reaction and comment counts per post, with the exact post
  time, so you can rank a shortlist on engagement that is real and recent instead of on follower
  count.
- **Anyone feeding an agent or an LLM.** Field names are stable, one post is one row, and the
  timestamp is ISO 8601, so a model gets a table it can reason over instead of "3d ago".

If you need a member's whole posting history back to 2015, this is the wrong tool and the FAQ
says so plainly.

### Why this one

- **Exact post times, not "2w".** LinkedIn prints a relative age next to every post and that is
  all most scrapers pass on. A post's activity id is a Snowflake-style integer whose top bits are
  the millisecond it was created, so this actor decodes the real timestamp and returns it as ISO
  8601 in `postedAt`. It was checked against all nine posts in the capture this actor was built
  from and agreed with LinkedIn's own label every time. `postedAtText` keeps the page's wording
  next to it. "Everything since Tuesday" becomes a filter you can write.
- **No login, no cookies, no session token, and the actor has no field to put one in.** It reads
  the same public page a signed-out browser gets. When LinkedIn decides a request needs an
  account it answers with a sign-in wall, and this actor reports that as `BLOCKED` and charges
  you nothing rather than trying to get around it.
- **A repost is one row and it says so.** LinkedIn renders a repost as a card wrapping the
  original post. Read carelessly that is two rows for one post, and you get billed twice for a
  duplicate. Here it is one row with `isRepost: true` and `originalAuthor` filled in.
- **You are billed per post, and you can cap it.** `maxPosts` sets the most any single profile
  can return and therefore the most it can cost. A profile that returns nothing is a free row.
- **Reactions and comments are the page's own numbers.** Exact integers off the markup, not
  rounded display strings. A post published minutes ago that genuinely has no reactions yet comes
  back as `null`, not a fake zero.

### What you get

One row per post, with the profile-level fields repeated on each row so the table joins cleanly.

| Field | What it is |
|---|---|
| `profileUrl`, `publicId`, `authorName` | Who the posts belong to, echoed on every row |
| `postCount` | How many posts this run returned for that profile, which is also what it billed |
| `postsAvailable` | How many the page actually carried, before `maxPosts` capped it |
| `postUrl`, `postId` | The post permalink and its numeric activity id |
| `postedAt` | Exact ISO 8601 timestamp, decoded from the activity id |
| `postedAtText` | LinkedIn's own wording: `Now`, `5h`, `3d`, `2w` |
| `text` | The poster's commentary, line breaks kept, cut at 1000 characters |
| `textTruncated` | True when the cut happened, so you know the text is partial |
| `reactionCount`, `commentCount` | Exact counts, or `null` when the page published none |
| `mediaType` | `video`, `image`, `document`, `article` or `none` |
| `hasLink`, `linkUrl` | Whether the post sends people somewhere, and where, unwrapped out of LinkedIn's redirect |
| `isRepost`, `originalAuthor` | Whether this is someone else's post being amplified, and whose |
| `query`, `found`, `status`, `message`, `scrapedAt` | The standard result columns on every actor here |

Two things the public page does not publish, and this actor therefore does not invent:

- **There is no repost or share counter anywhere in the markup.** Reaction and comment counts are
  the only numbers LinkedIn emits. Rather than ship a column that is empty on every row, there is
  no repost-count column at all.
- **There is no full archive.** The page carries a recent window, nine posts in the profile this
  was built against, and there is no next page a signed-out visitor can request.

### Pricing

Misses are free. A profile that is blocked, gone, or has no public activity produces a row with
`found: false` and is never charged.

### Use it from Clay, n8n, Make, or an AI agent

This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.

```bash
curl "https://api.apify.com/v2/acts/accountable_eel~linkedin-profile-posts-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"profileUrls":["https://www.linkedin.com/in/satyanadella","reidhoffman"]}'
```

**n8n.** Add an HTTP Request node: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~linkedin-profile-posts-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body Content Type `JSON`, JSON Body `{"profileUrls":["https://www.linkedin.com/in/satyanadella","reidhoffman"]}` (swap in an expression from an earlier node for a real value).

**Clay.** Add an "HTTP API" column: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~linkedin-profile-posts-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body `{"profileUrls":["{{profile}}"]}`, mapping the row's profile into the `profileUrls` array.

**MCP.** In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "LinkedIn Posts Scraper, No Login | Apify" — the agent will find and run this actor.

### How to use

1. **In the Apify Console.** Open the actor page and click **Start** — the `profileUrls` field is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found.
2. **Via the API.** Call it directly with a POST request — no Console needed once you have an API token:
   ```bash
   curl "https://api.apify.com/v2/acts/accountable_eel~linkedin-profile-posts-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
     -X POST \
     -H "Content-Type: application/json" \
     -d '{"profileUrls":["https://www.linkedin.com/in/satyanadella","reidhoffman"]}'
   ```
3. **On a schedule.** Save this actor as an Apify **Task** with the input you want, then add a **Schedule** (hourly, daily, weekly) so it runs on its own — no server of your own required.

### Input

```json
{
  "profileUrls": [
    "https://www.linkedin.com/in/satyanadella",
    "reidhoffman"
  ]
}
```

One profile per line. Paste the full profile link, the /recent-activity/ link you copied while looking at someone's posts, or just the part after /in/. Accepted formats: https://www.linkedin.com/in/williamhgates, https://www.linkedin.com/in/satyanadella/recent-activity/all/, satyanadella.

Useful options, all optional:

| Option | What it does |
|---|---|
| `maxPosts` | The most posts one profile may return, newest first. This is also the most it can cost you for that profile. |
| `testRun` | Process only the first five profiles, so you can check the shape before spending on the whole list. |
| `onlyFound` | Drop the rows that came back with nothing. Misses are free either way. |
| `includeKeywords` / `excludeKeywords` | Keep or drop posts by the words in them. Filtered-out posts are not billed. |
| `columns` | Choose which fields land in the table. |

### Output

| query | found | status | profileUrl | publicId | authorName | postCount | postsAvailable | posts | postUrl | postId | postedAt | postedAtText | text | textTruncated | reactionCount | commentCount | mediaType | hasLink | linkUrl | isRepost | originalAuthor | scrapedAt |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| https://www.linkedin.com/in/satyanadella | true | OK | <profile link> | <public profile id> | <name> | <posts returned> | <posts the page carried> | \<all posts found (full list)> | <post link> | <post id> | \<posted at (exact)> | \<posted (as linkedin shows it)> | <post text> | \<text was cut at 1000 characters> | <reactions> | <comments> | <media> | <links out> | <links to> | <repost of someone else> | <originally posted by> | 1970-01-01T00:00:00.000Z |

### Tips

- **Sort on `postedAt`, filter on `postedAtText` never.** The relative label is what the page
  said at fetch time and it ages the moment you store it. The ISO timestamp does not.
- **`excludeKeywords` is the cheap way to drop noise.** A post filtered out by your keywords is
  not billed, so a run that only wants posts mentioning your category costs less than the same
  run unfiltered.
- **Set `maxPosts` to 3 for a wide sweep.** If you are checking two hundred people for "posted
  anything recently", three posts each answers it at a fifth of the cost of twenty.
- **Leave the residential proxy on.** LinkedIn's sign-in wall is IP-reputation driven. The very
  first probe of this build took a wall on a clean home address, so a datacenter address is not
  a saving.
- **Keep concurrency low.** Two at a time is the default for a reason. Pushing it raises the
  share of rows that come back `BLOCKED`, and while those are free, they are also not data.

### vs. alternatives

| | What it costs | What you get | Trade-off |
|---|---|---|---|
| **This actor** (`linkedin-profile-posts-lookup`) | $0.003 per post returned, $0.00005 actor start, nothing for a miss | One row per post with an exact ISO timestamp, text, reaction and comment counts, media type, outbound link and a repost flag | It reads the recent window the public page serves, roughly the last handful of posts. There is no way to page back through a member's history without an account, and this actor will not try. |
| **harvestapi/linkedin-profile-posts** | $2 per 1,000 posts | The category leader by a distance, 8,248 users in the last 30 days at a 4.41 rating, with deeper post history | Cheaper per post and it goes back further. Pick this one when you need the exact publish time, a repost separated from an original, or a per-profile cost cap. |
| **Clay** | $0.08 to $0.40 per enriched row in credits, on top of a seat | A whole enrichment workspace, waterfalls across dozens of providers, plus the table and the sequencing around it | If you want one place that does everything and you are not counting rows, that is Clay. This is one column, priced per column, callable from Clay via its HTTP step. |
| Doing it yourself | Your time, plus a sign-in wall that fires on reputation rather than on rate, and markup that nests a repost inside its own container | The same posts | This absorbs the wall handling, the session rotation, the repost de-duplication and the timestamp decoding, and it stops charging the moment a page comes back empty. |

Prices for third-party tools are their published list prices as of September 2026 and are not
tracked here. Check the vendor before relying on the comparison.

### FAQ

**Do I need a LinkedIn account, a cookie, or a `li_at` token?**
No, and there is nowhere to put one. The actor requests the public profile page a signed-out
browser gets. If LinkedIn decides that request needs an account, the row comes back `BLOCKED`
and free.

**How far back does it go?**
As far as the public page carries, which was nine posts for the profile this was built against
and varies by member. There is no page-two for a signed-out visitor, so `maxPosts` can lower
what you get but never raise it above what the page served. `postsAvailable` tells you what was
there.

**Why is `reactionCount` sometimes empty?**
Because the page published no number for that post. That is normal on a post published minutes
ago. It is reported as `null` rather than `0`, because "nobody has reacted yet" and "LinkedIn
did not say" are different facts.

**Why is there no repost count?**
Because LinkedIn's public page does not emit one. The only social numbers in the markup are
reactions and comments. A column that is empty on every row is worse than no column.

**A repost shows the original author's name. Is that a bug?**
No. When someone reposts without commentary, LinkedIn's own permalink points at the original
post, so that is what `postUrl` carries. `isRepost` is `true` and `originalAuthor` names the
person who wrote it. Filter on `isRepost` if you only want a person's own writing.

**Can it read comments, or who reacted?**
No. The public page renders a comments tab and a reactions tab, and both hold other people's
posts and other people's names. This actor reads only the member's own Posts tab.

**What happens to a profile with no public activity?**
One free row, `found: false`, with a message saying the page loaded but published no posts a
signed-out visitor can read. Nothing is charged.

**Does it work for company pages?**
No. Feed it a `/company/` link and it returns a free `BAD_FORMAT` row without spending a
request. Company pages have their own actor, `linkedin-company-posts-lookup`.

**Is the exact timestamp really exact?**
It is decoded from the post's own activity id, and it was checked against every relative label
on the page it was built from. If LinkedIn ever changes how those ids are minted, `postedAt`
comes back `null` rather than wrong, and `postedAtText` still carries the page's own wording.

### Personal data, and your responsibility

This actor reads public LinkedIn profile pages only. It never signs in, never sends a cookie or
a session token, and never requests anything behind the sign-in wall.

What it returns is still personal data about identifiable people. If the GDPR, the UK GDPR, the
CCPA or a comparable law applies to you, you need your own lawful basis for collecting and using
it, and your own answer on retention, notice and the rights of the people in your table.
LinkedIn's User Agreement also restricts automated collection from its services, and that
agreement is between you and LinkedIn.

None of this is legal advice, and nothing here says that any particular use is lawful. That call
is yours.

### Related actors

- **[LinkedIn Company Posts Lookup](https://apify.com/accountable_eel/linkedin-company-posts-lookup)**: the same thing for company pages.
- **[LinkedIn Profile Lookup](https://apify.com/accountable_eel/linkedin-profile-lookup)**: who the person is, rather than what they posted.
- **[LinkedIn Company Lookup](https://apify.com/accountable_eel/linkedin-company-lookup)**: firmographics behind a company page.
- **[LinkedIn Jobs Search Lookup](https://apify.com/accountable_eel/linkedin-jobs-search-lookup)**: open roles from LinkedIn's public job search.

# Actor input Schema

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

One profile per line. Paste the full profile link, the /recent-activity/ link you copied while looking at someone's posts, or just the part after /in/. Accepted formats: https://www.linkedin.com/in/williamhgates, https://www.linkedin.com/in/satyanadella/recent-activity/all/, satyanadella. You're only charged for the ones we actually find — a miss costs nothing.

## `testRun` (type: `boolean`):

Turn this on to test your input on a small sample before running the full list. Turn it off to process everything.

## `onlyFound` (type: `boolean`):

Only keep rows where something was actually found. Misses are always free, whether or not you show them here.

## `includeKeywords` (type: `array`):

Optional. Only keep results that mention at least one of these words (e.g. a job title, a city, a product name). Leave empty to keep everything.

## `excludeKeywords` (type: `array`):

Optional. Drop any result that mentions one of these words. Leave empty to skip nothing.

## `maxResults` (type: `integer`):

Optional. Stop the run once this many results have been found — useful for a quick, cheap sample. Leave blank for no limit.

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

Optional. Caps how many posts each profile returns, newest first, which is also the most you are billed for that profile. The public page carries up to 20; leave it at 20 to take everything it served.

## `columns` (type: `array`):

Choose which pieces of information to include in each result row. All are included by default.

## `expandRows` (type: `boolean`):

When on, each post found gets its own row instead of being grouped under its profile. You're still only charged once per profile, no matter how many rows it produces.

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

Parallel requests. Keep conservative — this target has no browser fallback, so getting blocked costs more than slow-and-steady.

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

Apify Proxy config. Residential recommended for anti-bot-sensitive targets.

## Actor input object example

```json
{
  "profileUrls": [
    "https://www.linkedin.com/in/satyanadella",
    "reidhoffman"
  ],
  "testRun": false,
  "onlyFound": false,
  "includeKeywords": [],
  "excludeKeywords": [],
  "maxPosts": 20,
  "columns": [
    "profileUrl",
    "publicId",
    "authorName",
    "postCount",
    "postsAvailable",
    "posts",
    "postUrl",
    "postId",
    "postedAt",
    "postedAtText",
    "text",
    "textTruncated",
    "reactionCount",
    "commentCount",
    "mediaType",
    "hasLink",
    "linkUrl",
    "isRepost",
    "originalAuthor"
  ],
  "expandRows": true,
  "maxConcurrency": 2,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# 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 = {
    "profileUrls": [
        "https://www.linkedin.com/in/satyanadella",
        "reidhoffman"
    ],
    "includeKeywords": [],
    "excludeKeywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("accountable_eel/linkedin-profile-posts-lookup").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 = {
    "profileUrls": [
        "https://www.linkedin.com/in/satyanadella",
        "reidhoffman",
    ],
    "includeKeywords": [],
    "excludeKeywords": [],
}

# Run the Actor and wait for it to finish
run = client.actor("accountable_eel/linkedin-profile-posts-lookup").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 '{
  "profileUrls": [
    "https://www.linkedin.com/in/satyanadella",
    "reidhoffman"
  ],
  "includeKeywords": [],
  "excludeKeywords": []
}' |
apify call accountable_eel/linkedin-profile-posts-lookup --silent --output-dataset

```

## MCP server setup

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

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/kTaQFhefRc3RthUUs/builds/8ZBDBoM6aFn08gaOa/openapi.json
