# Facebook Posts Scraper — Facebook Page Posts & Group Posts (`steadyfetch/facebook-posts-scraper`) Actor

Facebook posts scraper for pages and public groups: paste a link and get posts as rows — text, date, ten-way reactions, comment count, image and a playable MP4 link on video posts. Optional page details: followers, category, email, phone, address, hours, ads. No login, exact limit, no start fee.

- **URL**: https://apify.com/steadyfetch/facebook-posts-scraper.md
- **Developed by:** [Steadyfetch Team](https://apify.com/steadyfetch) (community)
- **Categories:** Social media, Lead generation, Automation
- **Stats:** 2 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?

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

## Facebook Posts Scraper — Facebook Page Posts & Group Posts

**Click Start with nothing set** and the run returns the built-in sample rows — a three-post sample from one public brand page with page details off, so you see the exact output shape before you spend anything: nothing is collected from Facebook, and there is no result fee. Then **paste a Facebook page and get its posts, paste a public group and get its posts — same run, same columns.** One JSON row per post: the post ID and permalink, the page or group it is in, who posted it, the text, the date, the post type, one image, a playable MP4 link with the moment it expires on video posts, the reaction total and its ten-way breakdown, the comment count, and the top comments Facebook serves with the post. **From $2.00/1,000 posts** on the Business plan ($5.00 on the Apify free plan), platform usage included, no start fee.

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

- **Actor id:** `steadyfetch/facebook-posts-scraper`
- **Input:** `{ "pages": ["ladbible"] }` — the one field you have to set. Or `{ "groups": ["https://www.facebook.com/groups/dogspotting"] }` for a public group, or `startUrls` with both mixed.
- **Cap the bill:** set `maxTotalChargeUsd` on the run (a run option, not Actor input), e.g. `0.50` — the run stops when it reaches it.
- **Your cap is the cap.** `resultsLimit` (default 20) is exact and per page or group; `maxItems` (default 1000) caps the whole run. If your input carries `limit`, `count` or `maxPosts` instead — the names other Facebook post scrapers use — the smallest of them is read as `maxItems` and one uncharged note row says so.

*Unofficial. Not affiliated with, endorsed by, or sponsored by Facebook or Meta Platforms, Inc.*

### What a row does NOT carry

Read this before the feature list, because it is the part a listing usually leaves for the Issues tab. Two numbers are **not** in the feed a logged-out reader of these pages and groups is shown, so this actor does not sell them:

- **`shareCount` is always `null`.** How many times a post was shared is not published on the surface this actor reads.
- **`videoViewCount` is always `null`** on every video post. The key is served, and its value is empty every time.

Both ship as explicit `null` columns rather than as missing ones, so a sheet or a fixed-schema table never breaks and you never have to wonder whether a zero was really a zero. This actor will not promise a column it cannot fill. Everything else on the row is a real value, or an honest `null` with a reason beside it.

### What you put in

| Input | What it takes |
|---|---|
| `pages` | Facebook pages, one per line — a name (`ladbible`), `@ladbible`, a full page link, or a `profile.php?id=…` link. Posts come back newest first. |
| `groups` | **Public** groups: a group link, a `/groups/<numeric id>` link, or a bare numeric group id. Same columns as a page, with `source: group`. |
| `startUrls` | Page links and group links in one list, for moving over from another scraper. Each line is sent to the right door by its own shape, before anything is bought. |
| `resultsLimit` | Posts per page and per group, exact. Default 20 — three targets at 20 collect up to 60. Set it to `0` with page details on for a details-only run. |
| `maxItems` | The ceiling on posts for the whole run. Default 1000. It also answers to `limit`, `count` and `maxPosts`, and the smallest one present binds — one uncharged note row says which name was read. |
| `includePageDetails` | One extra row per PAGE: followers, likes, talking-about, category, intro, creation date, website and links, email, phone, address, opening hours, price range, services, rating, ad status, verification and Instagram handle. Off by default, and groups never get one. |
| `newerThan` · `olderThan` | A window on the post's own publish date — `2026-09-01`, an ISO timestamp, or `7 days`. The walk is newest first, so `newerThan` ends it at the boundary and buys nothing past it. **No filter fee.** |
| `maxRunSeconds` | A wall clock on the run. Default 1800. The run stops cleanly and says what is left instead of being killed. |
| `includeSeen` · `resumeFromDatasetId` | Re-run the same pages and groups without paying again for posts this account already has. |

### What you get

One row per post, from a page and from a public group alike.

| Column | What it holds |
|---|---|
| `postId` · `url` | The post's own ID and its permalink, on both doors. |
| `source` | `page` or `group` — which door the row came through. |
| `pageName` · `pageId` · `groupName` · `groupId` | Where the post lives. |
| `authorName` · `authorId` | Who posted it. |
| `text` | What was written. |
| `createdAt` | When it was posted, ISO-8601 UTC, from Facebook's own publish time. |
| `postType` | `video`, `image` or `text`. |
| `imageUrl` | The post's image — **one image per post**, which is what the feed carries. |
| `videoUrl` · `videoSdUrl` · `videoUrlExpiresAt` | A directly playable MP4 link in HD and SD on a video post, and the moment Facebook's signature on it stops working. |
| `thumbnailUrl` | The video's cover frame. |
| `reactionCount` | Reactions on the post, all kinds together. |
| `reactions` | The ten-way breakdown, one number each: like, love, care, haha, wow, sad, anger, thankful, pride, confused. |
| `commentCount` | How many comments sit under the post. |
| `shareCount` · `videoViewCount` | Always `null` — see the block above. |
| `topComments` | 0 to 2 comments, exactly as the feed serves them, and never charged. |
| `repeat` | This account already had this post and asked for it again. |
| `charged` · `status` · `statusReason` | Whether this row was billed and what happened, so your invoice reconciles from the dataset itself. |

The ten reaction columns are the reason most people are here: a post with 40 `anger` and 3 `like` is a different signal from 43 reactions, and you cannot get that from a total.

**Column names are ours and they stay put.** The wire is read in one place and renamed once, so a Facebook rename does not rename your columns, and a fixed-schema table never breaks on a new row.

### Price

Two charged events, and each lands only on a row that was actually delivered. A row is charged as **either** a Post **or** a Page details row, never both.

| Event | Apify free plan | Bronze | Silver | Gold and above |
|---|---|---|---|---|
| **Post** — one page or group post | $0.005 | $0.0035 | $0.0026 | **$0.002** |
| **Page details** — one page row, only with `includePageDetails` | $0.012 | $0.008 | $0.006 | **$0.005** |

The store header is the cheapest tier of the primary event: **from $2.00/1,000 posts**. Platform usage is included in both prices — there is no separate compute bill on top. **No start fee, no filter fee, and no result fee on a miss.** A date window costs nothing, and a run that finds nothing to deliver charges nothing. Set `maxTotalChargeUsd` on the run and it is a hard ceiling: the run stops cleanly under it, delivers everything already in hand, and the last row says what is left.

### What is never charged

Every run closes its own books, and each of these ships as a labelled, uncharged row rather than as silence:

- **`page_not_found`** — Facebook says there is no such page. A definitive answer about your link.
- **`page_private`** — the page is real and Facebook shows its posts to logged-in users only.
- **`page_no_posts`** — the page is real and its own feed ended the walk short of what you asked for: it ran out of posts, or your own `newerThan` / `olderThan` window closed the walk. Any posts already delivered say what each was charged, so the no-fee statement covers only what did not arrive.
- **`group_not_found`** — Facebook says there is no such group.
- **`group_private`** — the group is real and Facebook shows its posts to members only.
- **`group_no_posts`** — the same two endings on the group door: the group is real and public, and its own feed ran out or your window closed the walk.
- **`skipped.page_cap`** — a bound of THIS ACTOR'S own, never a verdict about your page or group: the feed was still serving and the run had read the 40 feed pages it buys for a single input. Split fewer targets across more runs to go deeper.
- **`skipped.all_repeat`** — the other bound of ours: two feed pages running that added no new post, so the run stopped paying to look. On a repeat run that usually means you already have everything there is.
- **`feed_unavailable`** — the read did not go through. Temporary, says nothing about your target, and a re-run is the fix. When it lands part-way through an input, the row opens with the posts already delivered and charged, and the no-fee statement covers only the rest.
- Posts dropped by your own `newerThan` / `olderThan` window, posts past your own `resultsLimit` or `maxItems`, posts the run clock or the cost cap did not reach, and posts this account already had.
- Starting a run. There is no start fee, per post or per run.

The last row of every run is a receipt: delivered, asked for, skipped, what stopped it, and the charged-event totals. **A cost cap or a time limit ends the collecting, never the delivering** — posts already in hand are always written out.

### How often the data changes

A busy page posts hourly and an active group posts about daily, so the useful pattern here is the same targets again tomorrow rather than one deep run. Reactions and comment counts keep moving for roughly **72 hours** after a post goes up, which is the window worth re-reading if you are measuring engagement. And `videoUrl` is a **signed** link that Facebook expires about **four days** out (`videoUrlExpiresAt` is on the row) — download the MP4 in the same week, or re-run for fresh links.

Re-running would otherwise re-buy the posts you already have, so it does not: every run remembers what it delivered to your account, and a later run skips those — not delivered again, not charged, and the slot goes to the next new post instead, so you still get the number you asked for. That makes a daily watch on the same pages cost only what is new. 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 (Write is what lets the run remember what it delivered; without it every run pays again). Turn on **Include posts you already have** to get them back anyway, marked `repeat: true` and still uncharged, and use **Skip posts in this dataset** (`resumeFromDatasetId`) to carry the same memory across accounts.

### Honest caps

- **Public groups only.** A private group returns an uncharged row — Facebook shows its posts to members only, with or without a login, so there is no setting that opens one. A group that does not exist is a different uncharged row, so "private" and "gone" are never the same answer.
- **Newest first, and that is the only order.** There is no "top posts" order here, and no input that asks for one. A sort sent under any name comes back as one uncharged note row saying newest-first is what you get, never as a silent substitution.
- **One image per post**, not a carousel — the feed carries a single image per post.
- **0 to 2 top comments**, exactly as the feed serves them, and never charged. They are a taste, not a thread; the thread is a different actor's job.
- Facebook may change how any of this is served. If it does, the rows that fail are labelled and not charged.

### Where the other Facebook links go

- A reel, watch, share or fb.watch link: [Facebook reels, with the MP4 and the transcript](https://apify.com/steadyfetch/facebook-reels-scraper)
- The comments under a post, in full, with their own reaction breakdown: [Facebook comments](https://apify.com/steadyfetch/facebook-comments-scraper)
- An Ad Library link, or the ads an advertiser is running right now: [Meta Ad Library rows](https://apify.com/steadyfetch/facebook-ad-library-scraper)
- The spoken words inside a Facebook video ad: [Facebook ad transcripts](https://apify.com/steadyfetch/facebook-ads-transcript-scraper)

Paste one of those into this actor and it comes back as an uncharged row naming the actor that takes it, rather than as a failed run.

### Something not right?

Open the **Issues** tab on this actor with your run ID and the input you used, and it gets looked at. If it earned its keep, a rating helps other buyers find it, and saving the actor keeps it one click away.

# Actor input Schema

## `pages` (type: `array`):

Facebook pages — a name like "ladbible", @ladbible, https://www.facebook.com/ladbible, or a profile.php?id=… link. Never charged for a page that does not exist, a page Facebook shows to logged-in users only, or a page with no posts. A reel, watch, share or fb.watch link belongs to steadyfetch/facebook-reels-scraper and comes back uncharged naming it. Leave it untouched in the console and click Start for the built-in three-post sample of one public brand page: nothing is collected from Facebook and there is no result fee.

## `groups` (type: `array`):

Public Facebook groups — https://www.facebook.com/groups/dogspotting, a /groups/<numeric id> link, or a bare numeric group id. Never charged for a group that does not exist or one Facebook shows to members only: a private group returns one uncharged row saying so. Private groups cannot be collected at all — Facebook shows their posts to members only, with or without a login.

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

Page links and group links in one list — each is sent to the right door by its own shape, so you never have to split them. Never charged for a link this actor cannot collect: a reel, watch, share or fb.watch link comes back uncharged naming steadyfetch/facebook-reels-scraper, a single post permalink naming steadyfetch/facebook-comments-scraper, and an Ad Library link naming steadyfetch/facebook-ad-library-scraper. Entries may be plain links or the crawler shape \[{"url": "https://www.facebook.com/ladbible"}]; both are read.

## `resultsLimit` (type: `integer`):

Hard cap on the posts delivered for EACH page and EACH group — three targets at 20 collect up to 60 in total. Set it to 0 with "Include page details" on for a details-only run that collects no posts at all. A page or group that runs short leaves an uncharged row saying so.

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

Hard ceiling across every page and group in the run, whatever the per-target limit says. Also answers to `limit`, `count` and `maxPosts` — the names other Facebook post scrapers use — and the smallest one present binds. One uncharged note row says which name was read and what it was read as.

## `includePageDetails` (type: `boolean`):

ON adds one Page details row per page target: follower, like and talking-about counts, category, intro, creation date, website and links, email, phone, address, opening hours, price range, services, rating, whether the page is running ads, and its Instagram handle where Facebook shows one. Charged as one Page details unit per row delivered, $0.012 on the Apify free plan and from $0.005 on paid plans. A page that does not exist is never charged, and a detail the page does not list comes back as an explicit empty value rather than a guess.

## `newerThan` (type: `string`):

Only posts published on or after this point — a date like 2026-09-01, an ISO timestamp, or a window like "7 days", "3 weeks", "2 months". Never charged for a post the window dropped. A value this actor cannot read leaves one uncharged row naming the field and the run collects as if no window were set, rather than returning nothing.

## `olderThan` (type: `string`):

Only posts published on or before this point — a date like 2026-09-01, an ISO timestamp, or a window like "7 days". Never charged for a post the window dropped. Use it with "Posts newer than" to take a slice between two dates.

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

The run stops cleanly before this many seconds and reports what is left, instead of being killed by a timeout. Never charged for the posts it did not reach — they ship as uncharged rows naming the clock.

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

OFF (default): a post already delivered to your account is skipped — no row, no charge — and the next new 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
{
  "pages": [],
  "groups": [],
  "resultsLimit": 20,
  "maxItems": 1000,
  "includePageDetails": false,
  "maxRunSeconds": 1800,
  "includeSeen": false
}
```

# Actor output Schema

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

One row per post delivered, from a page feed or a public group feed — the SAME columns either way, with `source` saying which door it came through: the post ID and permalink, the page or group it is in, who posted it, the text, the date, the post type, one image, a directly playable MP4 link with the moment it expires on video posts, the reaction total and its ten-way breakdown, the comment count, and the 0–2 top comments Facebook serves with the post. `shareCount` and `videoViewCount` are always an explicit null: a logged-out reader of these feeds is not shown either number, and this actor will not promise a column it cannot fill. 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. Posts your account already had are skipped before they take a slot and are never charged again; with "Include posts you already have" they come back marked `repeat: true`, still uncharged.

## `pageDetails` (type: `string`):

One `rowType: page` row per PAGE target, and only with "Include page details" on: follower, like and talking-about counts, category, intro, creation date, website and links, email, phone, address, opening hours folded to one row, price range, services, the rating percent and count parsed out of Facebook's own sentence, whether the page is running ads, its Ad Library page ID, its verification badge and its Instagram handle where Facebook shows one. A detail the page does not list is an explicit empty value, never a guess. An age-gated page delivers the shorter set Facebook serves for it, marked `gated: true`. Groups never produce one of these rows.

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

One uncharged row for every page or group that returned less than it was asked for, saying which of eight things happened: `page_not_found` (Facebook says the page does not exist), `page_private` (the page is real and shows its posts to logged-in users only), `page_no_posts` (the page is real and has posted nothing), `group_not_found` (Facebook says the group does not exist), `group_private` (the group is real and shows its posts to members only), `group_no_posts` (the group is real, public and quiet), `feed_unavailable` (the read failed and a re-run is the fix), and the stop rows naming your own limit, date window, run clock or cost cap. None of these is charged.

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

Posts delivered, what was asked for, page detail rows delivered, 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 = {
    "pages": [],
    "groups": []
};

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

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

```

## MCP server setup

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