# Facebook Group Posts & Comments Scraper (`amrhassan25/facebook-actor`) Actor

Every post from any public Facebook group with its complete comment thread — nested replies included and correctly attached to their parent comment. No login, no cookies, no second Actor. Large groups resume across runs, so you never pay twice for the same post.

- **URL**: https://apify.com/amrhassan25/facebook-actor.md
- **Developed by:** [Amr Abdo](https://apify.com/amrhassan25) (community)
- **Categories:** Social media, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.40 / 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/platform/actors/running/actors-in-store#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 Group Posts & Comments Scraper

Extract posts from any **public** Facebook group together with their **complete comment threads** — nested replies included, correctly attached to the comment they answer — in a single run.

**No login. No cookies. No account to get banned. Just the group link.**

***

### The problem this solves

Try to get Facebook group posts *with* their comments today and you hit one of two walls:

- **The two-Actor chain.** One scraper collects posts, a second collects comments from every post URL, and you join the two datasets yourself. Two runs, two schemas, two bills, and a join key you have to trust.
- **The all-in-one that isn't.** It returns posts with a `comments` field, and the field holds the three or four "top comments" Facebook bundles into the feed. A post with 400 comments gives you 4. Nothing tells you the rest exist.

And when either one *does* return replies, they arrive as top-level comments. Two comments and five replies become seven flat rows with nothing saying which reply answered which comment. The conversation is gone; only the sentences are left.

This Actor does the whole job in one run, and keeps the thread structure intact.

| | Two-Actor chain | Typical all-in-one | **This Actor** |
|---|---|---|---|
| Runs needed | 2 + a manual join | 1 | **1** |
| Comments per post | All | Only "top comments" | **All** |
| Replies | Flat, unattributed | Usually missing | **Nested, with `parentCommentId`** |
| Cost for 200 posts / 3,000 comments | ~$6.80 | ~$0.30 (incomplete) | **~$2.60** |
| Login or cookies | No | Varies | **Never** |
| Output | Two schemas, raw API fields | Varies | **One clean schema** |

***

### Proven at scale

Two production runs on 7 August 2026, across two different public groups, logged out:

| | Group A | Group B | **Total** |
|---|---|---|---|
| Posts | 17,599 | 9,612 | **27,211** |
| Comments | 46,224 | 23,998 | **70,222** |
| Comments with no identifiable parent | 0 | 0 | **0** |

**Every one of those 70,222 comments landed in the right place.** Not one orphan across either run.

More than **a third of them were replies** — comments that answer another comment rather than the post. A scraper that flattens replies hands you those as if they were standalone opinions. On a Facebook group, that is a third of the conversation silently misattributed.

#### It keeps going when Facebook goes quiet

Facebook never announces the end of an anonymous feed. It just stops returning posts — and a temporary block looks exactly the same as a finished feed.

In the 17,599-post run above, Facebook went silent **five separate times**. Each time, this Actor waited and asked again, and each time the posts resumed. A scraper that took the first silence at face value would have stopped at 11,149 posts and reported success — losing **36% of the run** without a single error message.

***

### Quick start

1. Paste one or more public Facebook group URLs into **Facebook group URLs**.
2. Set **Max posts per group** — or `0` for every post the group feed serves.
3. Set **Max comments per post** — or `0` for every comment.
4. Click **Start**.

That's the whole setup. No cookies to export, no session to keep alive, no account to warm up.

Any public group URL form works:

```
https://www.facebook.com/groups/874728723021553
https://www.facebook.com/groups/my-group-slug/
https://m.facebook.com/groups/123?ref=share
```

***

### What you get

Exactly two things: posts, and their comments. No group metadata dumps, no marketplace fields, no leftover API internals.

```json
{
  "postId": "9988776655",
  "postUrl": "https://www.facebook.com/groups/12345/posts/9988776655/",
  "groupId": "12345",
  "groupName": "North Side Neighbours",
  "text": "Looking for a plumber in the north side, any recommendations?",
  "publishedAt": "2026-08-02T16:00:00.000Z",
  "author": {
    "id": "user-1",
    "name": "Dana Whitfield",
    "profileUrl": "https://www.facebook.com/dana"
  },
  "stats": { "reactions": 42, "comments": 17, "shares": 3 },
  "comments": [
    {
      "commentId": "1001",
      "parentCommentId": null,
      "text": "Rivera Plumbing did our bathroom, very fair.",
      "publishedAt": "2026-08-02T17:00:00.000Z",
      "author": { "id": "u2", "name": "Sam Okafor", "profileUrl": "https://www.facebook.com/sam" },
      "likes": 5,
      "replyCount": 1,
      "replies": [
        {
          "commentId": "1002",
          "parentCommentId": "1001",
          "text": "Seconding Rivera, they came out same day.",
          "publishedAt": "2026-08-02T17:24:00.000Z",
          "author": { "id": "u3", "name": "Priya Raman", "profileUrl": null },
          "likes": 2,
          "replyCount": 0,
          "replies": []
        }
      ]
    }
  ],
  "scrapedCommentsCount": 17,
  "declaredCommentsCount": 17
}
```

Every reply is nested under the comment it answers **and** names its parent outright, so the thread survives being flattened, filtered, or re-sorted downstream.

Working in spreadsheets? Turn on **One row per comment** and every comment becomes its own row, with `parentCommentId` and `depth` preserving the structure.

***

### Large groups: resume from where you stopped

**This is the feature that makes big groups possible.**

Facebook does not hand an anonymous visitor a 500,000-post archive, and no single run will get one. What matters is what happens next — and for most scrapers, the answer is: you run it again and pay to collect the same posts a second time.

This Actor records the feed position on **every** run. Turn on **Resume from where the last run stopped** and the next run continues from that exact position.

It works. Group B above delivered **3,678 posts in an earlier run and 9,612 more in the next one** — 13,290 posts from a single group, with no post collected or paid for twice.

#### How to use it

1. **First run** — leave the option off. Set **Max posts per group** to `0`. Let it run until it stops.

2. **Read the last line of the log.** It names exactly how the run ended:

   | In the log | What it means | Run again? |
   |---|---|---|
   | `[exhausted]` / `[end-of-feed]` | Facebook served the whole feed. | No — nothing is left. |
   | `[feed-empty]` / `[feed-repeating]` | Facebook went quiet. The position is saved. | **Yes** |
   | `[throttled]` / `[error]` | Blocked, or a request failed. The position is saved. | **Yes** |
   | `[max-posts]` / `[too-old]` | You got exactly what you asked for. | Only to go deeper. |

3. **Next run** — turn **Resume from where the last run stopped** on. Repeat until you see `[exhausted]`.

Schedule it and the crawl gets deeper every day, without ever re-collecting a post you already paid for.

#### Details worth knowing

- **It resumes deeper into older posts** — it is *not* how you pick up newly published ones. For that, use **Only posts newer than**.
- **The position is recorded whether or not the option is on**, so you can switch it on *after* a run has already stopped and still carry on from there.
- **The position is only discarded when Facebook confirms the feed is finished.** An ambiguous ending keeps it, because losing a valid position costs re-walking the whole feed, while keeping a dead one costs a single wasted page.
- **A stale position self-heals.** If Facebook no longer honours the saved position, the run says so and the next one starts fresh.
- **Platform migrations are handled automatically.** If Apify relocates your run to another server mid-crawl, it picks up where it left off — and **Max posts per group** still means what you asked for across the whole run.

#### How far can one run get?

Measured at **3,200–3,500 posts an hour** with full comment threads, across both runs above.

Two things end a long run before the group does. Facebook may go quiet for good — that is what the log codes are for. And **your own Apify residential proxy quota** may run out first; that is what ended the 17,599-post run. Either way the position is saved, so the next run continues rather than restarting.

***

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `startUrls` | array | — | Public group URLs. **Required.** |
| `maxPosts` | integer | `20` | Posts per group. `0` = no limit. |
| `maxCommentsPerPost` | integer | `0` | Comments per post, replies included. `0` = all. |
| `includeReplies` | boolean | `true` | Expand nested reply threads. |
| `onlyPostsNewerThan` | string | — | `2026-07-01` or `7 days`. Stops paginating past it. |
| `sortBy` | enum | `CHRONOLOGICAL` | Feed order: chronological, recent activity, or top posts. |
| `commentsOrder` | enum | `ALL` | `ALL` is the most complete; `NEWEST` and `TOP` mirror Facebook's own orderings. |
| `flattenComments` | boolean | `false` | One row per comment instead of nested. |
| `resumeFromLastRun` | boolean | `false` | Continue a group's feed from where the last run stopped. |
| `maxConcurrency` | integer | `8` | Posts processed in parallel. |
| `maxRequestRetries` | integer | `4` | Retries before an item is given up on. |
| `proxyConfiguration` | object | Residential | Residential proxies strongly recommended. |

***

### Pricing

Pay per event — you pay for data you actually receive, and nothing else.

| Event | Price |
|---|---|
| Post scraped | $0.004 |
| Comment scraped | $0.0006 |
| Actor start | $0.00005 |

**What that works out to:**

| Job | Cost |
|---|---|
| 100 posts + 500 comments | **$0.70** |
| 200 posts + 3,000 comments | **$2.60** |
| 1,000 posts + 1,000 comments | **$4.60** |
| 1,000 posts + 2,500 comments | **$5.50** |

Comments are priced separately from posts because they are the work: a post is one unit, and its comments are however many more it takes to reach the end of the thread. Pricing them together would mean quiet posts subsidising busy ones.

#### Cheaper on higher Apify plans

| | Free | Starter | Scale | Business |
|---|---|---|---|---|
| 1,000 posts | $4.00 | $3.80 | $3.60 | $3.40 |
| 1,000 comments | $0.60 | $0.57 | $0.54 | $0.51 |

#### Platform usage is included

You pay the prices above and nothing else. This Actor does **not** pass Apify platform usage — compute units, proxy traffic, storage operations — on to you as a separate line. Many scrapers do, which is what turns a quoted price into a surprise at the end of the month.

#### What is never charged

- **Failed groups.** A private group, a login wall, a group that no longer exists — each returns an error record naming the cause, and none of them are billed.
- **Retries and blocked requests.** The charge happens when a post is *written to your dataset*, not when a request is made. A run that fights through throttling to deliver 500 posts bills you for 500 posts.
- **Comments Facebook never served.** `scrapedCommentsCount` is what was actually captured; you are not charged for the gap against Facebook's own counter.

#### Spending limits are honoured

Set a **maximum cost per run** and this Actor respects it: the run stops cleanly at the limit and keeps everything collected up to that point, rather than being killed mid-write and losing the tail of the data you already paid for.

Your feed position is saved either way, so a run that stops at its limit can be continued by the next one with **Resume from where the last run stopped** — you never pay twice for the same post.

***

### FAQ

**`scrapedCommentsCount` is lower than `declaredCommentsCount`. Is something missing?**
No. Facebook's own counter includes comments that are deleted, hidden by moderators, or filtered as spam — counted but never served to visitors. `scrapedCommentsCount` is what genuinely existed and was captured. Both numbers are in the output so the difference is always visible.

**Can it scrape private groups?**
No, and that is deliberate. Private groups require a logged-in account. Cookie-based scrapers break constantly, get accounts locked, and produce the worst reliability of any approach. This Actor only reads what a logged-out visitor can see — which is why it has no account to lose.

**How far back can it go?**
As far as Facebook's group feed serves, which is often several months on an active group. There is no archive search behind the feed, so **Only posts newer than** filters what the feed returns rather than unlocking older history.

**A very large group stopped before I got everything.**
Expected — see [Large groups](#large-groups-resume-from-where-you-stopped). Check the last log line, turn on resume, run again.

**A group returned an error record instead of posts.**
Each error record names the cause in plain language — private group, login wall, group not found. One bad group never fails the run, and error records are never billed.

**Can I use it with AI agents?**
Yes. The Actor declares its output schema, so agents reaching it through Apify's MCP server or the API know what to expect and can chain it into a workflow without guesswork.

**Is it fast?**
3,200–3,500 posts an hour with every comment thread attached, measured across the runs above. There is no browser to start, no page to render, and no screenshot to wait for.

# Actor input Schema

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

One or more URLs of <strong>public</strong> Facebook groups. Private groups cannot be scraped without logging in, which this Actor deliberately never does.

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

How many posts to scrape from each group. Set to <strong>0</strong> to keep going until the group feed is exhausted.

## `maxCommentsPerPost` (type: `integer`):

How many comments to collect per post. Set to <strong>0</strong> to collect <strong>all</strong> comments. Replies count toward this limit.

## `includeReplies` (type: `boolean`):

Expand nested reply threads under each comment. Turn off to collect only top-level comments and run faster.

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

Stop paginating once posts get older than this. Accepts an absolute date (<code>2026-07-01</code>) or a relative period (<code>7 days</code>, <code>3 months</code>). Leave empty for no date limit.

## `sortBy` (type: `string`):

How the group feed is ordered while scraping. <code>CHRONOLOGICAL</code> returns newest posts first.

## `commentsOrder` (type: `string`):

<code>ALL</code> returns every comment including low-ranked ones and is the most complete. <code>NEWEST</code> orders by time, <code>TOP</code> returns Facebook's ranked selection.

## `flattenComments` (type: `boolean`):

Off (default): one dataset item per post, with comments nested inside it. On: every comment becomes its own row, which suits CSV and Excel exports.

## `resumeFromLastRun` (type: `boolean`):

Facebook throttles anonymous traffic, so a very large group can outlast a single run. Turn this on to continue that group's feed from the position the previous run reached, instead of paying to collect the same posts again.<br><br><strong>How to use it:</strong> run once with this off, then read the last line of the log. It names how the run ended — <code>\[exhausted]</code> or <code>\[end-of-feed]</code> means the whole feed was served and there is nothing left; <code>\[feed-empty]</code>, <code>\[feed-repeating]</code>, <code>\[throttled]</code> or <code>\[error]</code> means the position was saved and running again with this option on will go deeper. Repeat until you see <code>\[exhausted]</code>.<br><br>This resumes <strong>deeper into older posts</strong> — it is not how you collect newly published ones, which is what "Only posts newer than" is for. The position is recorded on every run whether or not this is on, so you can switch it on after a run has already stopped, and it is discarded only once Facebook confirms the feed is finished.

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

How many posts are processed in parallel. Lower this if you hit rate limits.

## `maxRequestRetries` (type: `integer`):

How many times a failed request is retried before the item is given up on.

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

Residential proxies are strongly recommended and used by default. Facebook blocks datacenter IPs aggressively.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.facebook.com/groups/874728723021553"
    }
  ],
  "maxPosts": 20,
  "maxCommentsPerPost": 50,
  "includeReplies": true,
  "sortBy": "CHRONOLOGICAL",
  "commentsOrder": "ALL",
  "flattenComments": false,
  "resumeFromLastRun": false,
  "maxConcurrency": 8,
  "maxRequestRetries": 4,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

One item per post, each carrying the full comment thread in its `comments` array with replies nested under the comment they answer and naming it in `parentCommentId`. When the `flattenComments` input is on, the shape changes to one item per comment instead, with `postId`, `commentId`, `parentCommentId` and `depth` on every row so the thread can be rebuilt. Groups that could not be scraped append an error record naming the cause in plain language rather than failing the whole run.

# 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.facebook.com/groups/874728723021553"
        }
    ],
    "maxPosts": 20,
    "maxCommentsPerPost": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("amrhassan25/facebook-actor").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.facebook.com/groups/874728723021553" }],
    "maxPosts": 20,
    "maxCommentsPerPost": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("amrhassan25/facebook-actor").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.facebook.com/groups/874728723021553"
    }
  ],
  "maxPosts": 20,
  "maxCommentsPerPost": 50
}' |
apify call amrhassan25/facebook-actor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,amrhassan25/facebook-actor"
        }
    }
}

```

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/Nm4bfb2qGDN9YdcfQ/builds/Dme64jOe5NvgU7e6g/openapi.json
