# Facebook Comments Scraper - $0.25 per 1,000 (`dami_studio/facebook-comments-scraper`) Actor

Turn public Facebook post and reel URLs into comment rows: author, text, timestamp, like count, reply count and nested replies. No login, no cookies, no browser. At $0.25 per 1,000 comments it is the cheapest Facebook comments scraper on the market, and that rate is the same on every plan.

- **URL**: https://apify.com/dami\_studio/facebook-comments-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (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 $0.25 / 1,000 comment scrapeds

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/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 Comments Scraper

Paste public Facebook post URLs, get the comments back as rows: who wrote it, what they wrote, when,
how many likes it got, how many replies it has, and — if you want them — the replies themselves.

No Facebook account. No cookies. No login. No browser. No proxy of your own.

### Price

**$0.25 per 1,000 comments**, plus a **$0.001 start fee per run** (one tenth of a cent, billed per
gigabyte of run memory, so exactly $0.001 on the default 1 GB).

This is **the cheapest Facebook comments scraper on the market**, and $0.25 is the rate on every
plan — free or paid. There are no volume tiers, no minimum spend and no add-on fees. Where a
headline rate is a tiered one, it is usually the rate you get only after committing to a large
subscription; this price has no tiers to hide behind, so what you read here is what you pay on day
one.

| Comments | Total cost |
|---|---|
| 100 | $0.026 |
| 1,000 | $0.251 |
| 10,000 | $2.501 |
| 100,000 | $25.001 |

#### What is actually charged

- **One `comment-scraped` event per comment row written to the dataset.** Nothing else is metered
  per row.
- Replies count as comments — each reply is its own row, so each one is one charge.
- **Free:** the sample row an empty run returns, every diagnostic row (blocked post, private post,
  post with comments turned off, bad URL), and every comment dropped by the
  `onlyCommentsNewerThan` filter. Those rows all carry `"charged": false`.
- A run that finds nothing costs the start fee and nothing more.
- Rows never leave the dataset without a charge, and are never charged without a row. The primary
  event is a named one — there is no price quietly attached to `apify-default-dataset-item`, which
  is the trick that makes some scrapers bill you for their own error messages.

### Input

```json
{
  "startUrls": [
    { "url": "https://www.facebook.com/natgeo/posts/pfbid02rAK2eMyfVYDZbESKoNVuwWjcFr5BgRBfjp27nhgQToSiQD9NF3ypNdXpQuwSp7qnl" },
    { "url": "https://www.facebook.com/NASAJSC/posts/1011509817757722" }
  ],
  "resultsLimit": 50,
  "includeNestedComments": true,
  "viewOption": "RANKED_UNFILTERED"
}
```

| Field | What it does |
|---|---|
| `startUrls` | Public Facebook post, video, reel, photo or group-post permalinks. Up to 100 per run. |
| `resultsLimit` | Maximum comments **per post URL**. Default 50, hard ceiling 5,000. |
| `includeNestedComments` | Also pull the replies under each comment. Each reply is its own row and counts towards the limit. |
| `viewOption` | `RANKED_UNFILTERED` (all comments, the default), `RANKED_THREADED` (most relevant), `RECENT_ACTIVITY` (newest first). |
| `onlyCommentsNewerThan` | Optional cut-off — `2026-01-31`, a full ISO timestamp, or relative like `3 days` / `2 months`. Dropped comments are not charged. |
| `proxyUrls` | Leave empty. Only fill this in if you want traffic to leave through proxy servers you already pay for. |

Run it with **empty input** and you get one clearly labelled sample row, free, so you can see the
output shape before you spend anything.

#### URL shapes that work

```
https://www.facebook.com/<page>/posts/<pfbid...>
https://www.facebook.com/<page>/posts/<numeric id>
https://www.facebook.com/<page>/videos/<numeric id>
https://www.facebook.com/reel/<numeric id>
https://www.facebook.com/groups/<group id>/posts/<numeric id>
https://www.facebook.com/permalink.php?story_fbid=<id>&id=<page id>
https://www.facebook.com/photo.php?fbid=<id>
```

A bare Page URL like `https://www.facebook.com/natgeo` is rejected with an explanation instead of
being scraped — a Page is not a post and has no single comment thread.

### Output

One row per comment. A real row from a National Geographic post:

```json
{
  "ok": true,
  "charged": true,
  "recordType": "comment",
  "inputUrl": "https://www.facebook.com/natgeo/posts/pfbid02rAK2eMyfVYDZbESKoNVuwWjcFr5BgRBfjp27nhgQToSiQD9NF3ypNdXpQuwSp7qnl",
  "facebookUrl": "https://www.facebook.com/natgeo/posts/pfbid02rAK2eMyfVYDZbESKoNVuwWjcFr5BgRBfjp27nhgQToSiQD9NF3ypNdXpQuwSp7qnl",
  "postId": "1634927484671342",
  "postTitle": "National Geographic",
  "postDescription": "A punishing heat wave has left much of Europe in drought, causing rivers to run at record-low levels.",
  "pageName": "National Geographic",
  "commentUrl": "https://www.facebook.com/natgeo/posts/pfbid02rAK2eMyfVYDZbESKoNVuwWjcFr5BgRBfjp27nhgQToSiQD9NF3ypNdXpQuwSp7qnl?comment_id=27860828630277533",
  "id": "Y29tbWVudDoxNjM0OTI3NDg0NjcxMzQyXzI3ODYwODI4NjMwMjc3NTMz",
  "commentId": "27860828630277533",
  "feedbackId": "ZmVlZGJhY2s6MTYzNDkyNzQ4NDY3MTM0Ml8yNzg2MDgyODYzMDI3NzUzMw==",
  "date": "2026-08-15T06:07:03.000Z",
  "timestamp": 1786860423,
  "text": "Perfect opportunity to clean our waters!!! Bring this historical junk up.",
  "likesCount": 28,
  "likesCountText": "28",
  "commentsCount": 1,
  "threadingDepth": 0,
  "replyToCommentId": null,
  "profileName": "Maggie Dalkowska",
  "profileId": "100000529787288",
  "profileUrl": "https://www.facebook.com/maggie.dalkowska",
  "profilePicture": "https://scontent.xx.fbcdn.net/v/...",
  "author": { "id": "100000529787288", "name": "Maggie Dalkowska", "url": "https://www.facebook.com/maggie.dalkowska" },
  "attachments": [],
  "scrapedAt": "2026-08-15T13:29:41.902Z"
}
```

A reply row looks the same but with `"threadingDepth": 1`, a `replyToCommentId` pointing at its
parent, and a `commentUrl` carrying both `comment_id` and `reply_comment_id`.

#### Field notes

- `likesCount` is a **number**. Facebook only publishes a display string for reaction counts
  (`"28"`, `"1.2K"`, `"3.4M"`), so it is parsed back into an integer here. `likesCountText` keeps
  the original string if you want to show exactly what Facebook shows. Above about a thousand
  reactions the display string is rounded by Facebook, so the number is too — `1.2K` becomes
  `1200`, not the exact count. Nobody can give you the exact count from the public page, because
  the public page does not contain it.
- `commentsCount` is the reply count on that comment, not the post's comment count.
- `attachments` is an array of `{type, url}` for photo, video and sticker comments, empty for the
  usual text-only ones.
- `profileUrl` is null for the small number of commenters whose profile Facebook renders without a
  public link.
- `postDescription` is the **post's** text, not the comment's. It is read from the post's own
  content section, which matters more than it sounds: taking the longest text block on the page
  instead returns whichever reader wrote the longest comment.

### Speed

Measured on the platform, at the default 1 GB:

- One post, 60 comments: **13 seconds**, from a cold container.
- Two posts, 8 comments each: **7 seconds** — up to three posts are worked on at once.

Facebook hands out comments in pages of ten and silently ignores any request for a larger page, so
a hundred comments is ten round trips no matter how it is asked for. That is the floor for this
target, not a limitation of this Actor.

### What this does not do

An honest list, because you will hit these otherwise.

- **Public posts only.** No login is ever performed, so anything that needs an account — private
  groups, friends-only posts, a Page that has restricted its audience by age or country — returns
  an uncharged diagnostic row saying so. This is a design decision, not a bug: an Actor holding
  someone's session cookie is one password change away from breaking and one policy change away
  from getting an account disabled.
- **No reactions breakdown.** You get the total reaction count, not how many were love, haha or
  angry. The logged-out page does not carry it.
- **No commenter email or phone.** Not published, not inferable, not returned.
- **One level of replies.** `includeNestedComments` pulls the replies under a top-level comment.
  Replies to replies are collapsed by Facebook into the same level, so that is as deep as the
  public thread goes.
- **Reply counts can exceed the rows you get.** A comment may report six replies and return four
  if some were made by accounts that later deactivated or were removed.
- **Very large threads.** The ceiling is 5,000 comments per post URL. Threads deeper than that are
  paginated by Facebook in a way that starts repeating rather than continuing, and repeated rows
  are dropped rather than charged.
- **A post with comments turned off** returns one uncharged diagnostic row, not an error.
- **Rate limiting.** Facebook meters logged-out reads per address. The Actor rotates through a large
  pool of datacenter addresses and retries, but a very large single run may still see a few posts
  come back as `BLOCKED` diagnostics. They are not charged. Re-running usually clears them.

### Diagnostics

Nothing about this Actor fails silently, and a failed run never bills you for the failure. Every
problem becomes a row with `"charged": false` and a `code`:

| `code` | Meaning |
|---|---|
| `BAD_INPUT` | That URL is not a Facebook post URL. The row says why. |
| `NOT_FOUND` | The post is private, deleted, or the URL points at something with no comment thread. |
| `NO_COMMENTS` | The post exists and is readable but has no comments, or none newer than your date filter. |
| `BLOCKED` | Every exit address tried was throttled on this post. Re-run. |
| `RATE_LIMITED` | Facebook asked for a slower pace. Re-run. |
| `DEAD_BATCH` | Fifteen URLs in a row returned nothing, so the run stopped early rather than working through a list of dead links. The rest were not touched and not charged. |
| `PROXY_INPUT_ADJUSTED` | You asked for a proxy setting this Actor does not use; it ran on its own rotating addresses instead. |
| `CHARGE_ERROR` | A billing event could not be recorded. Told to you rather than hidden. |

The run itself still ends **SUCCEEDED** in all of these cases, because a run that failed would bill
you the start fee for the privilege of being told you pasted the wrong link.

### FAQ

**Do I need a Facebook account, cookies or an access token?**
No. Nothing is logged in and nothing is stored between runs. You paste URLs, you get comments.

**Does it work on reels and videos?**
Yes. Reels, videos, photos and group posts all have the same comment thread underneath them and are
read the same way.

**How do I get the replies under a comment?**
Set `includeNestedComments` to `true`. Each reply arrives as its own row with `threadingDepth: 1`
and `replyToCommentId` set, and each one is charged as a comment because each one is a row.

**How do I only get recent comments?**
Set `onlyCommentsNewerThan` to `7 days`, or to a date like `2026-06-01`. Anything older is dropped
before it reaches the dataset, and dropped comments are never charged. Combine it with
`viewOption: "RECENT_ACTIVITY"` and the run also stops paging as soon as it goes past your cut-off,
which is faster and cheaper than filtering afterwards.

**How much does 10,000 comments cost?**
$2.50 plus the $0.001 start fee. There is no tier that makes it cheaper and no tier that makes it
more expensive.

**Why do I get exactly ten comments per request in the logs?**
Because that is what Facebook returns. Asking for 25, 50 or 100 returns ten. The Actor pages until
it reaches your `resultsLimit`.

**Can I use my own proxies?**
Yes — put them in `proxyUrls` as `http://user:pass@host:port`. Leave it empty and the run uses a
rotating pool of datacenter addresses, which costs you nothing extra. This Actor deliberately does
not offer residential proxy: on a target this data-heavy the bandwidth would cost more than the
data is worth, and you would be paying for it.

**Is the comment ID stable?**
`commentId` is Facebook's own numeric comment id and is stable. `id` is the base64 node id, also
stable. Either one works as a deduplication key across runs.

**What happens if I run it with no input?**
You get one sample row, clearly marked `"_sample": true` and `"charged": false`, showing every
field. Only the start fee applies.

**Can it scrape all comments from a whole Page?**
Not directly — this Actor takes post URLs. Collect the post URLs first, then feed them in here, up
to 100 per run.

### Integration

Standard Apify Actor. Start it from the API, the JS or Python client, a schedule, or an MCP client,
and read the default dataset:

```bash
curl -X POST "https://api.apify.com/v2/acts/dami_studio~facebook-comments-scraper/runs?token=<YOUR_TOKEN>" \
  -H 'content-type: application/json' \
  -d '{"startUrls":[{"url":"https://www.facebook.com/natgeo/posts/pfbid02rAK2eMyfVYDZbESKoNVuwWjcFr5BgRBfjp27nhgQToSiQD9NF3ypNdXpQuwSp7qnl"}],"resultsLimit":50}'
```

Output is available as JSON, CSV, Excel and XML from the dataset endpoint.

### Legal

This Actor reads only what Facebook serves to a logged-out visitor — the same content anyone can
see without an account. It does not log in, does not bypass a privacy setting, and does not touch
private groups or restricted posts. Comment text and profile names are personal data in most
jurisdictions: you are the controller of whatever you collect, and the GDPR, the CCPA and Facebook's
own terms are your responsibility to satisfy. Scrape public opinion, not people.

# Actor input Schema

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

Public Facebook post, video, reel, photo or group-post permalinks. You get one row per comment for each of them.

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

Stop after this many comments for each post URL. Keep it low while you are testing - you pay per comment.

## `includeNestedComments` (type: `boolean`):

Also fetch the replies underneath each comment. Every reply is returned as its own row and counts towards the limit.

## `viewOption` (type: `string`):

How Facebook should order the comments before they are read.

## `onlyCommentsNewerThan` (type: `string`):

Optional cut-off. Absolute (2026-01-31 or a full ISO timestamp) or relative (3 days, 2 months). Comments older than this are dropped and never charged. There is no add-on fee for using it.

## `proxyUrls` (type: `array`):

Leave this empty. By default the run rotates a large pool of datacenter addresses that cost you nothing per gigabyte. Fill it in only if you specifically want the traffic to leave through proxy servers you already pay for, in the form http://user:pass@host:port.

## `sessionCookies` (type: `array`):

Leave this empty unless you need it. Runs are logged out by default and that is enough for public Facebook content. Facebook shows some things only to a signed-in account, and it limits how fast any one account may read; supplying your own cookie uses your account and your own rate limit, shared with nobody. In Chrome: open facebook.com while signed in, press F12, then Application > Cookies > https://www.facebook.com, and paste the values as "c\_user=<value>; xs=<value>". One line per account. Treat these like a password: anyone with them can act as that account, and Facebook may sign the session out or restrict the account for automated use.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.facebook.com/natgeo/posts/pfbid02rAK2eMyfVYDZbESKoNVuwWjcFr5BgRBfjp27nhgQToSiQD9NF3ypNdXpQuwSp7qnl"
    }
  ],
  "resultsLimit": 50,
  "viewOption": "RANKED_UNFILTERED"
}
```

# Actor output Schema

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

Every comment row in the default dataset: profileName, text, date, likesCount, commentsCount, threadingDepth, commentUrl, facebookUrl, postId, postTitle and the rest. An empty, private or blocked run returns a single uncharged row explaining what happened instead.

# 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/natgeo/posts/pfbid02rAK2eMyfVYDZbESKoNVuwWjcFr5BgRBfjp27nhgQToSiQD9NF3ypNdXpQuwSp7qnl"
        }
    ],
    "resultsLimit": 50,
    "includeNestedComments": false,
    "viewOption": "RANKED_UNFILTERED",
    "onlyCommentsNewerThan": ""
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/facebook-comments-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 = {
    "startUrls": [{ "url": "https://www.facebook.com/natgeo/posts/pfbid02rAK2eMyfVYDZbESKoNVuwWjcFr5BgRBfjp27nhgQToSiQD9NF3ypNdXpQuwSp7qnl" }],
    "resultsLimit": 50,
    "includeNestedComments": False,
    "viewOption": "RANKED_UNFILTERED",
    "onlyCommentsNewerThan": "",
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/facebook-comments-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 '{
  "startUrls": [
    {
      "url": "https://www.facebook.com/natgeo/posts/pfbid02rAK2eMyfVYDZbESKoNVuwWjcFr5BgRBfjp27nhgQToSiQD9NF3ypNdXpQuwSp7qnl"
    }
  ],
  "resultsLimit": 50,
  "includeNestedComments": false,
  "viewOption": "RANKED_UNFILTERED",
  "onlyCommentsNewerThan": ""
}' |
apify call dami_studio/facebook-comments-scraper --silent --output-dataset

```

## MCP server setup

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