# Facebook Comments Scraper — Post Comments, Facebook Reactions (`steadyfetch/facebook-comments-scraper`) Actor

Facebook reactions and comments for any post, reel, watch or fb.watch link — text, author, date, reply count and the ten-way reaction breakdown (like, love, care, haha, wow, sad, angry). No login, no cookies. Top-level comments only. Exact limit per link. No start fee, nothing undelivered charged.

- **URL**: https://apify.com/steadyfetch/facebook-comments-scraper.md
- **Developed by:** [Steadyfetch Team](https://apify.com/steadyfetch) (community)
- **Categories:** Social media, Automation, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.60 / 1,000 comments

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 Comments Scraper — Post Comments, Facebook Reactions

Paste the Facebook post, reel, watch or fb.watch links you want and get the comments underneath them: what was written, who wrote it, when, how many replies sit under it, and the full reaction breakdown — like, love, care, haha, wow, sad, angry, thankful, pride, confused. No login, no cookies, no proxy to configure. **Your limit is exact and it is per post — set 100 and you get 100 from each link**, and a comment we could not deliver is never charged. Nothing is charged for starting a run. **From $0.60 per 1,000 comments** on the cheapest paid tier, $2.40 per 1,000 on the Apify free plan, platform usage included.

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

- **Actor id:** `steadyfetch/facebook-comments-scraper`
- **Input:** `{ "postUrls": ["https://www.facebook.com/reel/2430902544051423"] }` — the one field you have to set. A post, reel, watch or fb.watch link, as many as you like. Add `"resultsLimit": 100` for the per-post limit and `"maxItems": 1000` for the whole run.
- **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.

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

### Replies are counted, not pulled

Read this first, because it is the one thing this actor deliberately does not do. Every comment row carries `replyCount`, so you can see at a glance which threads are busy — but the replies themselves are not returned. Facebook's own reply feed hands back a **subset** of a thread rather than the thread: measured on this data, 2 replies came back on a comment declaring 37, and 1 on a comment declaring 5, both reporting no further pages. Selling a subset as a thread would be a lie, so this actor does not sell it at all. Top-level comments are what it delivers, and the only thing it charges for.

### What one row looks like

A real delivered row, built by this actor's own parser from a harvested comment page:

```json
{
  "commentId": "1927177644651092",
  "commentUrl": "https://www.facebook.com/reel/2430902544051423?comment_id=1927177644651092",
  "postUrl": "https://www.facebook.com/reel/2430902544051423",
  "postId": "1389159413245942",
  "text": "Omg the thumbnail makes him look like Moby without glasses 🤣",
  "createdAt": "2026-05-20T18:11:13.000Z",
  "authorName": "…",
  "authorId": "…",
  "authorProfileUrl": "…",
  "reactionCount": 314,
  "replyCount": 37,
  "reactionLike": 291,
  "reactionLove": 9,
  "reactionCare": 2,
  "reactionHaha": 12,
  "reactionWow": 0,
  "reactionSad": 0,
  "reactionAnger": 0,
  "reactionThankful": 0,
  "reactionPride": 0,
  "reactionConfused": 0,
  "source": "facebook",
  "status": "delivered",
  "charged": true
}
```

The three author fields are trimmed for this page; a real row carries the name, the Facebook ID and the profile link. Every column is listed in the table below, and a link that could not be read carries the same columns with `null` in them plus a status saying why, and no result fee.

### What you get

One row per top-level comment, at one price.

| Column | What it holds |
|---|---|
| `commentId` | the comment's own Facebook id |
| `commentUrl` | a permalink to that comment, built on the post link you gave |
| `text` | what they wrote |
| `authorName` · `authorId` · `authorProfileUrl` | who wrote it |
| `createdAt` | when it was posted, ISO-8601 UTC |
| `reactionCount` | reactions on the comment, all kinds together |
| `reactionLike` · `reactionLove` · `reactionCare` · `reactionHaha` · `reactionWow` · `reactionSad` · `reactionAnger` · `reactionThankful` · `reactionPride` · `reactionConfused` | the ten-way breakdown, one column each |
| `replyCount` | how many replies sit under the comment |
| `postUrl` · `postId` · `source` | which post it belongs to (`source` is always `facebook`) |
| `repeat` | this account already had this comment and asked for it again |
| `charged` · `status` · `statusReason` | whether this row was billed, and why, so your invoice reconciles from the dataset itself |

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

### Price

One charged event, `comment`, and it lands only on a comment that was actually delivered.

| Apify plan | Per comment | Per 1,000 comments |
|---|---|---|
| Free plan | $0.0024 | $2.40 |
| Bronze | $0.0014 | $1.40 |
| Silver | $0.0009 | $0.90 |
| Gold and above | $0.0006 | **$0.60 per 1,000** |

Platform usage is included in that price — there is no separate compute bill on top, and no start fee, so a run that returns nothing costs 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.

### Honest rows: what "not charged" actually means

Every run closes its own books. A post that returned less than it was asked for leaves an uncharged row saying which of five things happened, and none of them is billed:

- **`post_not_found`** — Facebook has nothing at that link: the link is wrong, the post was deleted, or it sits behind a login. A definitive answer about your link.
- **`no_comments`** — the post gave no more. Either Facebook had no next page for it (comments turned off, or nobody has commented — a definitive answer, not a failure), or this run reached one of the actor's own bounds: 500 pages of comments on one link, or three pages in a row that held only comments you already have. The row says which of the three it was, and whose bound it was.
- **`stopped_at_limit`** — one of your own limits, or the run clock, stopped it first. The row names which.
- **`vendor_unavailable`** — the read did not go through. Temporary, says nothing about your post, and a re-run is the fix.
- **`vendor_budget`** — this actor reached its own monthly collection allowance and stopped rather than collecting more.

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 — comments already in hand are always written out.

`maxItems` also answers to `limit` and `count`, the names other Facebook comment scrapers use, and the smallest limit present binds. One uncharged note row says which name was read and what it was read as, so a limit typed under the wrong name never quietly runs up a bill. Asking for more than the actor serves is never a refusal: up to 5,000 comments per post and 100,000 rows per run, with a run clock between 30 and 3,600 seconds — ask past any of those and the run still starts, continues at the nearest end of the range, and one uncharged row names what you asked for and what bound it.

### How often this data changes

Comments keep arriving on a Facebook post for days after it goes up, and the busiest hours are the first few. So the useful pattern here is not one run — it is the same links again tomorrow. Re-running would otherwise re-buy the comments you already have, so it does not: every run remembers what it delivered to your account, in the key-value store `fb-comments-account` in your own Apify account, and a later run skips those. Not delivered again, not charged, and the slot goes to the next new comment instead, so you still get the number you asked for. That makes a daily watch on the same posts cost only what is new. A run started with a scoped API token in restricted-access mode needs key-value store Read, Write and Create permission on that store (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).

Switch **Include comments you already have** on to get them back anyway, marked `repeat: true` and still uncharged, and use **Skip comments in this dataset** to carry the same memory across accounts.

### Reliability

The comments come from a licensed data feed rather than from scraping Facebook's own pages, which is why there is nothing to log into and nothing to configure. When that feed cannot answer, the run says so on an uncharged row and finishes successfully — a failed read is never billed and never dressed up as a post with no comments.

### Related actors

- 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)
- The same job on Instagram, replies included: [Instagram comments](https://apify.com/steadyfetch/instagram-comments-scraper)

### Limits and good manners

Only public posts are returned — a post behind a login, an age gate or a private group is an uncharged `post_not_found` row, not a workaround. Facebook serves comments in its own ranked order rather than strictly newest-first, so a deep ask on a busy post walks that order and eventually runs out; the run says so instead of padding. This actor may stop working if Facebook changes how the data is served; if it does, items that fail are never charged.

### Support

Something off, or a column you need that is not here? Open an issue on the Issues tab — we usually reply within a couple of hours.

If it earned its keep, a rating helps other buyers find it, and saving the actor keeps it one click away.

# Actor input Schema

## `postUrls` (type: `array`):

Facebook post links, one per line — https://www.facebook.com/reel/1234567890, https://www.facebook.com/watch/?v=1234567890, a page post permalink or an fb.watch short link. Leave it empty and the run returns built-in sample rows instead of collecting anything, so you can see the output shape at no result fee.

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

Hard cap on the comments delivered for EACH post link — three links at 100 collect up to 300 in total. Only top-level comments count: replies are counted on the comment they answer and never pulled. A post that runs short leaves an uncharged row saying so. Ask for more than this actor serves per post and the run still starts: it continues at the actor's own ceiling of 5,000 comments, with one uncharged row saying what was asked and what bound it.

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

Hard ceiling across every link in the run, whatever the per-post limit says. Also answers to `limit` and `count` — the names other Facebook comment scrapers use — and the smallest one present binds. One uncharged note row says which name was read and what it was read as. Ask for more than this actor serves in one run and the run still starts: it continues at the actor's own ceiling of 100,000 rows, with one uncharged row saying so.

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

The run stops cleanly before this many seconds and reports what is left, instead of being killed by a timeout. A time limit ends the collecting, never the delivering: comments already in hand are always written out. Anything outside the actor's own range of 30 to 3,600 seconds still starts a run: the clock is set to the nearest end of that range and one uncharged row says so.

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

OFF (default): a comment already delivered to your account is skipped — no row, no charge — and the next new comment takes its place, so you still get the number you asked for. ON: those comments are delivered again anyway, marked `repeat: true`, and still not charged. Comments 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. Comments already in it are treated as comments 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
{
  "postUrls": [],
  "resultsLimit": 100,
  "maxItems": 1000,
  "maxRunSeconds": 600,
  "includeSeen": false
}
```

# Actor output Schema

## `comments` (type: `string`):

One row per top-level comment delivered: text, a permalink to the comment, the author's name, id and profile link, when it was posted, the reaction total, the ten-way reaction breakdown, how many replies sit under it, and the post it belongs to. Replies are COUNTED on the row and never pulled — Facebook's own reply feed hands back a subset, so this actor does not sell it. 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. Comments your account already had are skipped before they take a slot and are never charged again; with "Include comments you already have" they come back marked `repeat: true`, still uncharged.

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

One uncharged row for every post that returned less than it was asked for, saying which of five things happened: `post_not_found` (the link is wrong, the post was deleted, or it sits behind a login), `no_comments` (the post is real and has none — comments off, or nobody has commented), `stopped_at_limit` (one of your own limits or the run clock stopped it first), `vendor_unavailable` (the read failed and a re-run is the fix), `vendor_budget` (this actor reached its own monthly collection allowance). None of these is charged.

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

Comments delivered, what was asked for, comments 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 = {
    "postUrls": []
};

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

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

```

## MCP server setup

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