Facebook Videos Scraper: Commenters, Reactors & Sharers avatar

Facebook Videos Scraper: Commenters, Reactors & Sharers

Pricing

Pay per usage

Go to Apify Store
Facebook Videos Scraper: Commenters, Reactors & Sharers

Facebook Videos Scraper: Commenters, Reactors & Sharers

Facebook Videos Scraper extracts public Facebook video data fast. Get titles, URLs, views, creators, descriptions, and engagement metrics. Ideal for research, trend analysis, monitoring competitors, and automating workflows that require structured Facebook video insights.

Pricing

Pay per usage

Rating

0.0

(0)

Developer

API Empire

API Empire

Maintained by Community

Actor stats

0

Bookmarked

4

Total users

1

Monthly active users

3 days ago

Last modified

Share

Facebook Video Scraper — Reels, Commenters, Reactors & Sharers

This Facebook video scraper extracts three distinct entity types from public Facebook Reels in one run: the video (metadata plus DASH and progressive download formats), the people who engaged with it — commenters, reactors and resharers as individually labelled rows — and the parent-anchored engagement counts that tie them together. Video rows land in the main dataset; participant identities land in a per-run child dataset keyed by parentId. Every response is typed, normalized JSON — no HTML, no selectors, no parsing step. By the end of this page you will know exactly which fields you get, which you provably cannot get logged-out, and what you are charged for.


What is Facebook Videos Scraper: Commenters, Reactors & Sharers?

Facebook Videos Scraper: Commenters, Reactors & Sharers is an Apify Actor that takes public Facebook Reel URLs (or bare numeric reel IDs) and returns structured JSON describing the video and the audience underneath it. It is a multi-entity Facebook Reels scraper: one input URL produces one video row plus up to three families of participant rows — commenter, reactor and resharer.

No Facebook account is required. The Actor runs fully logged-out. There is no cookie field, no c_user field, no session token, and no credential parameter of any kind in its input schema (.actor/actor.json) — you can verify that by opening the schema yourself. It fetches the public reel page with a desktop Chrome user-agent (src/main.py, FETCH_HEADERS) and calls three logged-out-permitted Facebook GraphQL tooltip queries for participant identities.

What it does:

  • Scrape Facebook Reel videos — title, description, creator, duration, upload date, thumbnail, and the full yt-dlp-shaped formats[] array covering DASH audio, DASH video and progressive sd/hd renditions.
  • Scrape Facebook commenters — the identity of people who commented (name, profile ID, profile URL). Comment text is not returned; see Coverage and limits.
  • Scrape Facebook reactors — the people who reacted to the reel, plus the exact reaction total.
  • Scrape Facebook resharers — the people who reshared the reel, plus the exact reshare total.
  • Export as JSON, CSV, Excel, XML or HTML table from the Apify Console, or pull the dataset over the Apify API.
  • No proxy management and no parsing — proxy configuration is optional, retries and residential escalation are built in, and the output is already normalized.

What data does this Facebook Reels scraper collect?

The Actor returns four row shapes across two datasets: video rows, commenter rows, reactor rows and resharer rows — plus two uncharged accounting shapes for inputs Facebook served empty or that failed outright.

Data TypeKey FieldsJSON Field Names
Reel video (main dataset)Video ID, title, caption, duration, upload date, thumbnail, canonical URLvideoId, title, description, duration, duration_string, upload_date, timestamp, thumbnail, webpage_url, original_url, urlShape
Creator (on the video row)Creator display name, numeric owner ID, profile linkcreatorName, creatorId, creatorUrl, uploader, uploader_id
Engagement counts (parent-anchored)Exact reactions, exact comments, exact and rounded reshares, join keysreactionsCount, commentsCount, resharesCount, shareCountRounded, feedbackId, storyId
Commenters (child dataset)Name, profile ID, profile URL, account type, rank, totaltype = "commenter", name, id, profileUrl, accountType, sampleRank, totalOfType
Reactors (child dataset)Name, profile ID, profile URL, account type, rank, totaltype = "reactor", name, id, profileUrl, accountType, sampleRank, totalOfType
Resharers (child dataset)Name, profile ID, profile URL, account type, rank, totaltype = "resharer", name, id, profileUrl, accountType, sampleRank, totalOfType
Download formats (array on the video row)Format ID, codec, resolution, bitrate, direct media URLformats[], requested_formats[], format_id, vcodec, acodec, resolution, tbr, url, manifest_url
Availability accountingWhether Facebook served the video, why not, row statusisAvailable, unavailableReason, status, error, scrapedAt

Every participant row also carries isChild: true, parentId (the videoId), parentUrl, videoFeedbackId, creatorName, creatorId and isFromCreator, so the audience joins straight back onto the video row without a lookup table.

Need more Facebook data?

This Actor goes deep on one reel and the people underneath it. If you need breadth instead — searching Facebook video results by keyword, walking a page's video archive, or pulling profile and page-level records — pair it with a Facebook search or page scraper and feed the reel URLs it discovers into this Actor. videoId, feedbackId and storyId are stable Facebook node identifiers, so they work as primary keys across a multi-Actor pipeline.


How does this Facebook scraper differ from the official Facebook Graph API?

Meta's Graph API is an authenticated, permission-scoped API: you register an App, request permissions, pass App Review, and then read data for assets you administer or that have explicitly granted your App access (developers.facebook.com/docs/graph-api). This Facebook video scraper reads the same public reel page a logged-out browser sees and normalizes it into JSON — no App, no review, no Page token.

FeatureFacebook Graph APIFacebook Videos Scraper: Commenters, Reactors & Sharers
AuthenticationApp ID, App Secret and an access token; permissions such as pages_read_engagement must be granted and reviewedNone. Runs logged-out — the input schema contains no credential field at all
Approval processApp Review required before most Page and engagement permissions can be used in productionNo approval. Start the Actor from the Apify Console or Apify API
Whose content you can readContent you administer, or content whose owner granted your App accessAny publicly visible Facebook Reel URL you paste in
Reactor / commenter identities on third-party videosNot exposed for content you do not administerReturned as labelled reactor and commenter rows, up to the platform's logged-out ceiling
Media file URLsNot provided as ready-to-fetch DASH/progressive renditionsformats[] with DASH audio, DASH video and progressive sd/hd URLs, plus manifest_url
Comment body textAvailable for content you administer, with the right permissionsNot available. Every logged-out comment-list query is refused by Facebook — see below
Output shapeGraph API JSON, versioned per Graph API releaseFixed Actor schema; the same field names every run

Use the Graph API when you own the Page and need first-party analytics, comment text, or write access — it is the correct and supported tool for that job. Use this Actor when you need public competitive or audience data across creators you do not administer, and you want it as flat JSON without an App Review cycle.


⚠️ Coverage and limits: what Facebook's logged-out surface actually allows

These constraints come from the Actor's own source code and input schema, and they decide whether it fits your use case.

Comment body text and threaded replies are not returned

The Actor returns who commented, not what they wrote. src/graph.py records that every Facebook GraphQL operation carrying comment bodies or threaded replies is login-gated — GFICommentsContainerPaginationQuery, FBReelsFeedbackRightRailWithCommentPreloadingQuery, CometVideoHomeUFICommentListRendererRootQuery, GFIReplyPaneQuery and CometUFICommentRefetchQuery all respond with Unauthorized logged out query, error code 1675002, and CommentsListComponentsPaginationQuery is not present in any logged-out bundle. The three operations this Actor does use are tooltip queries that Facebook serves logged-out and that return identities plus exact totals:

  • CometUFICommentsCountTooltipContentQuery → commenters
  • CometUFIReactionsCountTooltipContentQuery → reactors
  • CometUFISharesCountTooltipContentQuery → resharers

If you need comment text, this is not the Actor for it — and it will not ask for a Facebook cookie to pretend otherwise.

Participant lists are capped by Facebook, not by the Actor

The maxParticipantsPerVideo schema entry states Facebook's logged-out ceiling is about 19 people per kind, and that a higher cap cannot raise the yield. Practically:

  • If a reel has 19 or fewer people of a kind, you get all of them.
  • If it has more, you get a sample and the exact true total in totalOfType on every participant row (and in resharesCount on the video row for resharers).

commentersSampled, reactorsSampled and resharersSampled on the video row report how many you actually received, so you always know whether a list is complete or a sample.

Only /reel/<id> URLs carry video data

src/main.py classifies inputs Facebook serves without the requested video and writes an uncharged flagged row instead of a success row. The unavailableReason values produced by _classify_unavailable() are:

unavailableReasonWhen it fires
watch_landing_page_no_video_dataURL contains /watch/ and v= — the generic Watch landing page
page_video_url_serves_unrelated_feedURL contains /videos/ — Facebook serves a recommendation feed rather than the requested video
no_video_id_in_documentNo video ID could be recovered from the URL
video_not_available_deleted_private_or_not_a_reelThe page loaded but carried no media keys
fetch_failedAll three fetch attempts failed; the row also carries an error string

Availability is decided by has_media() in src/extract.py, which requires "dash_manifest_xml_string" or "browser_native_sd_url" to be present in the document. Pages without those keys are never stamped Success.

Counts are parent-anchored, and that matters

A Facebook reel page carries several engagement blocks — the requested reel plus sibling reels from the same creator — and src/anchor.py documents that their order is randomised server-side on every fetch, so a first-match regex for "unified_reactors" returns a different, unrelated video's numbers run to run. This Actor instead decodes the base64 story token (UzpfS…S:_I<ownerId>:VK:<videoId>), splits the document on those tokens, and reads counts only from the segment whose decoded video ID matches the one you requested. When no matching block exists, every count is null — never a fabricated 0.

isFromCreator is matched on name and can false-positive

Participant IDs on this surface are opaque pfbid… strings while the creator ID is numeric, so the two cannot be compared directly. isFromCreator therefore falls back to a case-insensitive display-name match (src/main.py). A different account sharing the creator's display name will be flagged. Treat it as a strong hint, not proof of identity.

resharesCount depends on your participant settings

resharesCount on the video row is populated from the resharers query's total. If you set includeParticipants to false, or remove "resharers" from participantTypes, it stays null. shareCountRounded (Facebook's own rounded string, e.g. "1.2K") is read from the anchored page block and is unaffected.


🎯 Why do developers and teams scrape Facebook Reels?

Reels are where Facebook's short-form attention lives, and the interesting signal is rarely the view counter — it is which named accounts showed up under a video. Four audiences use this Actor differently.

For AI engineers and agent builders

The output is typed JSON with stable keys, so it drops into an agent tool or a RAG index without a parsing step. A common pattern: run the Actor over a competitor's reel back-catalogue, index each video row on title, description, creatorName, reactionsCount and commentsCount into a vector store, and keep the child dataset as a companion table keyed on parentId. An agent can then answer "which accounts engage with more than one of this creator's reels?" by grouping participant rows on id — a query the platform never exposes. Because formats[] carries direct media URLs and manifest_url, multimodal pipelines fetch the video itself from the same row.

For marketers and brand teams

Reaction and comment totals tell you how much; commenter, reactor and resharer rows tell you who. Point the Actor at a competitor's top reels, set minReactions to skip the long tail, and read the child dataset: name plus profileUrl for every account that engaged, accountType to separate User from Page accounts, and totalOfType to gauge how deep your sample reaches. Accounts appearing as resharers across several of a rival's reels are the highest-value organic advocates — resharing is a far stronger signal than a reaction, and it is a list Facebook's own dashboards will not give you for a Page you do not own.

For researchers and analysts

Everything returned is public, logged-out data — the Actor holds no credentials and sees only what an anonymous visitor sees. That suits social-science work on public discourse, diffusion and amplification: sampleRank preserves the order Facebook returned participants in, totalOfType gives the population size behind each sample, and scrapedAt timestamps every row for longitudinal work. Because counts are parent-anchored rather than first-match, repeated captures of the same reel measure genuine change over time instead of noise from Facebook's randomised block ordering.

For developers building data products

The Actor is callable from the Apify API, so it slots into any scheduled pipeline. A typical build: a nightly run over a watchlist of reel URLs, maxItems capping the batch, minComments filtering out reels not worth paying for, and a downstream job reading the main dataset for metadata and the dataset named in childDatasetId for audience. SUMMARY.json in the run's key-value store is a per-run reconciliation object — total_requested, videos_with_data, participants_written, child_dataset_id and a per-URL stats map — so your pipeline can assert on completeness before it commits.


How to scrape Facebook Reels (step by step)

This Actor runs on the Apify platform. You start it from the Apify Console or the Apify API — there is no separate signup, no API key of its own, and no credit-balance concept outside Apify's normal billing.

  1. Open the Actor on its Apify Store listing and click Try for free (or open it from your Apify Console if you have already added it).
  2. Paste your reel URLs into videoUrls — one per line. Full links (https://www.facebook.com/reel/2009829369661635) and bare numeric IDs (2009829369661635) both work; bare IDs are rewritten to /reel/<id> automatically. No parameter is strictly required by the schema, but a run with no URLs logs ❌ No Facebook video URLs provided. and exits immediately.
  3. Choose which participants you want. Leave includeParticipants on (the default) and trim participantTypes to just the kinds you need — commenters, reactors, resharers. Turn includeParticipants off for a metadata-and-formats-only run.
  4. Add filters and caps if you want them. minComments and minReactions drop low-engagement reels before the row is pushed, so they are never charged. maxItems caps how many videos are processed; maxParticipantsPerVideo caps rows per kind, per video.
  5. Start the run, then export. Video rows appear in the run's default dataset; participants appear in the child dataset named on childDatasetId. Download either as JSON, CSV, Excel, XML or an HTML table, or fetch them over the Apify API.

What to do when Facebook changes its structure

Nothing on your side. The Actor resolves Facebook's persisted GraphQL doc_id values at runtime from the page's own JS bundles rather than hardcoding them (src/graph.py), so a Facebook release bump does not silently kill participant collection. The output schema — field names and types — is what your integration depends on, and it stays stable across maintenance updates.


⬇️ Input

Every parameter below comes from .actor/actor.json. No parameter is marked required, though a run without URLs produces no data. There are no credential, cookie or token parameters.

ParameterRequiredTypeDescriptionExample Value
videoUrlsNoarray of strings (editor: stringList)Reel URLs or bare numeric reel IDs, one per line: https://www.facebook.com/reel/<id> or a bare ID like 2009829369661635. Page-video URLs (/<page>/videos/<id>/) and /watch/?v= are accepted but Facebook serves them without the requested video — they come back flagged isAvailable: false and are never charged. No default; Console-prefilled with two example reel URLs.["https://www.facebook.com/reel/2009829369661635"]
maxItemsNointeger, default 0, minimum 0Caps how many videos are processed (parents only). Participants are additional and capped separately. 0 = no cap.25
includeParticipantsNoboolean, default trueFetch the people who commented, reacted or reshared each video into the child dataset — names and profile links, not comment text. Off = metadata-only run.true
participantTypesNoarray of strings (editor: select), default ["commenters", "reactors", "resharers"]Which participant kinds to collect. Allowed values: commenters, reactors, resharers. Each becomes rows with type = commenter / reactor / resharer.["commenters", "resharers"]
maxParticipantsPerVideoNointeger, default 0, minimum 0Caps participant rows per kind, per video. Facebook's own ceiling is about 19, so a higher value cannot raise the yield. 0 = take whatever Facebook returns.10
minCommentsNointeger, default 0, minimum 0Skip videos with fewer comments than this. 10 keeps only videos with 10+ comments. 0 = no filter.10
minReactionsNointeger, default 0, minimum 0Skip videos with fewer reactions than this. 500 keeps only videos with 500+ reactions. 0 = no filter.500
skipUnavailableNoboolean, default falseOff (default) writes a flagged, uncharged row for every video Facebook served without data, so you can audit each input. On drops them entirely.false
urlsNoarray of strings (editor: stringList)Backward-compatible alias for the original Actor's input key. When present it takes precedence over videoUrls, so existing saved inputs and API calls keep working unchanged. No default.["https://www.facebook.com/reel/974692562269765"]
proxyConfigurationNoobject (editor: proxy)Optional. Apify Proxy (residential or datacenter) or custom proxy URLs. Facebook reels resolve without a proxy from Apify containers; if a request looks blocked the Actor escalates to residential automatically, before the first row for that video is written. Prefilled {"useApifyProxy": false}.{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}

Example JSON input

Every parameter, with realistic values:

{
"videoUrls": [
"https://www.facebook.com/reel/2009829369661635",
"https://www.facebook.com/reel/974692562269765",
"1234567890123456"
],
"maxItems": 25,
"includeParticipants": true,
"participantTypes": ["commenters", "reactors", "resharers"],
"maxParticipantsPerVideo": 0,
"minComments": 5,
"minReactions": 100,
"skipUnavailable": false,
"urls": [],
"proxyConfiguration": {
"useApifyProxy": false
}
}

Minimal input

{
"videoUrls": ["https://www.facebook.com/reel/2009829369661635"]
}

Common pitfalls

  • The single most frequent mistake is pasting a page-video or Watch URL. https://www.facebook.com/<page>/videos/<id>/ and https://www.facebook.com/watch/?v=<id> are accepted, but Facebook does not serve the requested video on either shape — you get an uncharged row with isAvailable: false and an unavailableReason instead of data. Convert to https://www.facebook.com/reel/<id> when you can.
  • urls beats videoUrls. If a saved input or an API payload still carries a non-empty legacy urls array, videoUrls is ignored entirely. Clear urls when you switch to the new field.
  • Unrecognised participantTypes values are dropped silently. Only commenters, reactors and resharers are accepted. A list containing none of those results in no participants being collected, with no error.
  • Duplicate URLs are not de-duplicated. The same reel listed twice is fetched twice and produces two charged rows.
  • maxParticipantsPerVideo cannot raise the ceiling. It only truncates; Facebook's logged-out cap of roughly 19 per kind is the binding constraint.

⬆️ Output

Results are typed, normalized JSON with stable field names — no HTML fragments, nothing to parse before you use it. Export from the Apify Console as JSON, JSON Lines, CSV, Excel, XML or an HTML table, or read the dataset over the Apify API.

The Actor writes to two datasets:

  • the run's default dataset — one row per video (plus uncharged accounting rows), and
  • a child dataset named participants-<runId> — one row per participant, with its ID echoed on every video row as childDatasetId.

It also writes SUMMARY.json to the run's key-value store.

Scraped Facebook Reel (video row, default dataset)

The video row is the extract_from_html() output merged with the anchored engagement block and the run's accounting fields. The dataset's default table view surfaces 26 columns; the row itself carries far more, listed in full after the example.

{
"videoId": "2009829369661635",
"id": "2009829369661635",
"display_id": "2009829369661635",
"isAvailable": true,
"unavailableReason": null,
"urlShape": "reel",
"title": "Snow leopards are almost impossible to film",
"fulltitle": "Snow leopards are almost impossible to film",
"description": "Three years, one camera trap, and a single 40-second sequence.",
"uploader": "National Geographic",
"uploader_id": "100044150487581",
"creatorName": "National Geographic",
"creatorId": "100044150487581",
"creatorUrl": "https://www.facebook.com/100044150487581",
"duration": 41.6,
"duration_string": "0:41",
"timestamp": 1750000000,
"upload_date": "20250615",
"thumbnail": "https://scontent.xx.fbcdn.net/v/t15.5256-10/preferred_thumbnail.jpg",
"thumbnails": [{ "url": "https://scontent.xx.fbcdn.net/v/t15.5256-10/preferred_thumbnail.jpg", "id": "0" }],
"reactionsCount": 3439,
"commentsCount": 69,
"resharesCount": 412,
"shareCountRounded": "412",
"commentersSampled": 19,
"reactorsSampled": 19,
"resharersSampled": 8,
"participantsTotal": 46,
"feedbackId": "1606190650878359",
"storyId": "S:_I100044150487581:VK:2009829369661635",
"webpage_url": "https://www.facebook.com/100044150487581/videos/2009829369661635/",
"webpage_url_basename": "2009829369661635",
"webpage_url_domain": "facebook.com",
"original_url": "https://www.facebook.com/reel/2009829369661635",
"extractor": "facebook",
"extractor_key": "Facebook",
"format": "1474688177015818v - 1080x1920 (DASH video)+815764524126361a - audio only (DASH audio)",
"format_id": "1474688177015818v+815764524126361a",
"format_note": "DASH video+DASH audio",
"ext": "mp4",
"protocol": "https+https",
"width": 1080,
"height": 1920,
"resolution": "1080x1920",
"aspect_ratio": 0.56,
"fps": 30.0,
"tbr": 2116.494,
"vbr": 2057.163,
"abr": 59.331,
"asr": 48000,
"audio_channels": 2,
"vcodec": "avc1.640028",
"acodec": "mp4a.40.5",
"dynamic_range": "SDR",
"filesize_approx": 11002891,
"language": null,
"stretched_ratio": null,
"release_year": null,
"requested_subtitles": null,
"automatic_captions": {},
"subtitles": {},
"playlist": null,
"playlist_index": null,
"_has_drm": null,
"epoch": 1753401600,
"formats": [
{
"format_id": "815764524126361a",
"manifest_url": "https://www.facebook.com/dash_mpd_debug.mpd?v=2009829369661635&dummy=.mpd",
"ext": "m4a",
"format_note": "DASH audio",
"container": "m4a_dash",
"vcodec": "none",
"acodec": "mp4a.40.5",
"tbr": 59.331,
"asr": 48000,
"audio_channels": 2,
"filesize": 308421,
"url": "https://video.xx.fbcdn.net/o1/v/t2/f2/m412/AQM...mp4",
"resolution": "audio only",
"format": "815764524126361a - audio only (DASH audio)"
},
{
"format_id": "sd",
"quality": -3,
"ext": "mp4",
"tbr": 385.867,
"dynamic_range": "SDR",
"filesize_approx": 2006506,
"url": "https://video.xx.fbcdn.net/o1/v/t2/f2/m412/AQO...mp4?bitrate=385867&tag=sve_sd",
"format": "sd - unknown"
}
],
"childDatasetId": "participants-K1qA9ZxYb2LmN0pQ",
"status": "Success",
"scrapedAt": "2026-07-25T09:14:02.481930+00:00"
}

The formats[] entries above are abridged for readability, and requested_formats is omitted — it repeats, in full, the highest-bandwidth DASH video object and the DASH audio object already present in formats[]. Complete key sets follow the table. Every key the Actor writes to a video row, grouped by origin:

GroupKeys
Identity & availabilityvideoId, id, display_id, webpage_url_basename, isAvailable, unavailableReason, urlShape, status, scrapedAt
Contenttitle, fulltitle, description, thumbnail, thumbnails, duration, duration_string, timestamp, upload_date, release_year
CreatorcreatorName, creatorId, creatorUrl, uploader, uploader_id
Engagement (parent-anchored)reactionsCount, commentsCount, resharesCount, shareCountRounded, feedbackId, storyId
Participant accountingcommentersSampled, reactorsSampled, resharersSampled, participantsTotal, childDatasetId
URLs & provenancewebpage_url, original_url, webpage_url_domain, extractor, extractor_key, playlist, playlist_index, epoch
Media formats (always present)formats
Media summary (present only when at least one DASH video or DASH audio representation was found)requested_formats, format, format_id, ext, protocol, language, format_note, filesize_approx, tbr, width, height, resolution, fps, dynamic_range, vcodec, vbr, stretched_ratio, aspect_ratio, acodec, abr, asr, audio_channels
Subtitles (always emitted, empty on this surface)automatic_captions, subtitles, requested_subtitles, _has_drm

formats[] entries come in three shapes. DASH audio entries carry format_id, manifest_url, ext, width, height, tbr, asr, fps, language, format_note, filesize, container, vcodec, acodec, dynamic_range, url, manifest_stream_number, is_dash_periods, http_headers, downloader_options, protocol, audio_ext, video_ext, audio_channels, vbr, abr, resolution, aspect_ratio, format. DASH video entries carry the same keys with video_ext/audio_ext reversed and no audio_channels. Progressive entries (format_id = "sd" or "hd") carry format_id, quality, url, http_headers, downloader_options, protocol, ext, video_ext, audio_ext, vbr, abr, tbr, resolution, dynamic_range, aspect_ratio, filesize_approx, format.

The three participant shapes below share exactly the same 15 keys — id, type, isChild, parentId, parentUrl, name, profileUrl, accountType, isFromCreator, sampleRank, creatorName, creatorId, videoFeedbackId, totalOfType, scrapedAt — so a single table definition holds all of them and type does the discrimination. accountType is Facebook's own __typename for the node, typically User or Page.

Scraped commenter (child dataset)

{
"id": "pfbid02Xq7mKvJ4nR8sT1uW3yZ6aB9cD2eF5gH8jK1lM4nP7qR0sT3uV6wX",
"type": "commenter",
"isChild": true,
"parentId": "2009829369661635",
"parentUrl": "https://www.facebook.com/reel/2009829369661635",
"name": "Marta Kowalczyk",
"profileUrl": "https://www.facebook.com/pfbid02Xq7mKvJ4nR8sT1uW3yZ6aB9cD2eF5gH8jK1lM4nP7qR0sT3uV6wX",
"accountType": "User",
"isFromCreator": false,
"sampleRank": 1,
"creatorName": "National Geographic",
"creatorId": "100044150487581",
"videoFeedbackId": "1606190650878359",
"totalOfType": 69,
"scrapedAt": "2026-07-25T09:14:04.117044+00:00"
}

Scraped reactor (child dataset)

{
"id": "pfbid0A3bC6dE9fG2hJ5kL8mN1pQ4rS7tU0vW3xY6zA9bC2dE5fG8hJ1kL",
"type": "reactor",
"isChild": true,
"parentId": "2009829369661635",
"parentUrl": "https://www.facebook.com/reel/2009829369661635",
"name": "Wildlife Photography Weekly",
"profileUrl": "https://www.facebook.com/pfbid0A3bC6dE9fG2hJ5kL8mN1pQ4rS7tU0vW3xY6zA9bC2dE5fG8hJ1kL",
"accountType": "Page",
"isFromCreator": false,
"sampleRank": 7,
"creatorName": "National Geographic",
"creatorId": "100044150487581",
"videoFeedbackId": "1606190650878359",
"totalOfType": 3439,
"scrapedAt": "2026-07-25T09:14:04.522781+00:00"
}

Scraped resharer (child dataset)

{
"id": "pfbid0Z9yX6wV3uT0sR7qP4nM1lK8jH5gF2eD9cB6aZ3yX0wV7uT4sR1qP",
"type": "resharer",
"isChild": true,
"parentId": "2009829369661635",
"parentUrl": "https://www.facebook.com/reel/2009829369661635",
"name": "Tomás Ribeiro",
"profileUrl": "https://www.facebook.com/pfbid0Z9yX6wV3uT0sR7qP4nM1lK8jH5gF2eD9cB6aZ3yX0wV7uT4sR1qP",
"accountType": "User",
"isFromCreator": false,
"sampleRank": 3,
"creatorName": "National Geographic",
"creatorId": "100044150487581",
"videoFeedbackId": "1606190650878359",
"totalOfType": 412,
"scrapedAt": "2026-07-25T09:14:04.903512+00:00"
}

Unavailable row (uncharged, default dataset)

Written when skipUnavailable is false and Facebook served the page without the requested video. Note that this shape carries the video ID under id, not videoId.

{
"original_url": "https://www.facebook.com/watch/?v=1602444103877126",
"id": "1602444103877126",
"urlShape": "watch",
"isAvailable": false,
"unavailableReason": "watch_landing_page_no_video_data",
"status": "Unavailable",
"scrapedAt": "2026-07-25T09:14:06.230991+00:00"
}

Failed row (uncharged, default dataset)

Written when all fetch attempts fail or an exception is raised while the row is being built.

{
"original_url": "https://www.facebook.com/reel/9999999999999999",
"status": "Failed",
"error": "RuntimeError: Failed to fetch https://www.facebook.com/reel/9999999999999999 after 3 attempts: ...",
"isAvailable": false,
"unavailableReason": "fetch_failed",
"scrapedAt": "2026-07-25T09:14:09.771002+00:00"
}

SUMMARY.json (key-value store)

{
"total_requested": 3,
"videos_with_data": 2,
"participants_written": 46,
"child_dataset_id": "participants-K1qA9ZxYb2LmN0pQ",
"stats": {
"https://www.facebook.com/reel/2009829369661635": "Success",
"https://www.facebook.com/reel/974692562269765": "Filtered: minReactions",
"https://www.facebook.com/watch/?v=1602444103877126": "Unavailable: watch_landing_page_no_video_data"
},
"note": "Video rows -> default dataset (charged only when the page really carried the video). Participant rows -> child dataset, uncharged."
}

stats values are one of Success, Filtered: minComments, Filtered: minReactions, Unavailable: <reason> or Failed: <ExceptionType>: <message>.


🗂️ How to separate commenters, reactors and sharers in the dataset

The three participant types are not split across three datasets. They all land in the same child dataset (participants-<runId>, whose ID is on every video row as childDatasetId) and are discriminated by the type field. Video rows never carry type or isChild, so the separation is unambiguous.

Row typeWhere it livesDiscriminatorFilter expression
Video row (charged)Default datasetstatus === "Success"item.status === "Success"
Video row, unavailable (uncharged)Default datasetstatus === "Unavailable"item.isAvailable === false && item.status === "Unavailable"
Video row, failed (uncharged)Default datasetstatus === "Failed"item.status === "Failed"
CommenterChild datasettype === "commenter"item.isChild === true && item.type === "commenter"
ReactorChild datasettype === "reactor"item.isChild === true && item.type === "reactor"
ResharerChild datasettype === "resharer"item.isChild === true && item.type === "resharer"

Equivalent expressions in other tools:

  • jq — commenters only: jq '[.[] | select(.type == "commenter")]' participants.json
  • jq — charged video rows only: jq '[.[] | select(.status == "Success")]' dataset.json
  • Python / pandasreactors = df[df["type"] == "reactor"]
  • SQL, after loading both datasets — SELECT p.* FROM participants p JOIN videos v ON p.parentId = v.videoId WHERE p.type = 'resharer'
  • Apify API — request only the columns you need with the dataset items fields query parameter, for example fields=type,name,profileUrl,parentId,totalOfType.

To join audience back to video, use parentIdvideoId; both hold the same reel ID string. videoFeedbackIdfeedbackId gives you the same join through Facebook's feedback node instead.

To exclude uncharged accounting rows from a video-level export in one expression: item.isAvailable === true. Every charged row has isAvailable: true and status: "Success"; every uncharged row has isAvailable: false.


💰 What gets charged

This Actor uses Apify's pay-per-event billing with a single event, row_result, charged once per video row that actually contained a video (src/main.py pushes the success row with charged_event_name="row_result").

OutcomeWritten to a dataset?Charged?
Video fetched, media presentYes — default dataset, status: "Success"Yes, one row_result
Facebook served the page without the videoYes, unless skipUnavailable is on — status: "Unavailable"No
Fetch failed after 3 attempts, or an exception was raisedYes — status: "Failed"No
Filtered out by minComments or minReactionsNo row at allNo
Commenter / reactor / resharer rowsYes — child datasetNo, never

Two consequences worth planning around. Participants are free: a video with 46 participant rows costs the same single row_result as one with none, so there is no billing reason to disable includeParticipants. Filters are cheaper than post-processing: minComments and minReactions are evaluated from the anchored counts before the row is pushed, so reels below the threshold never generate a charge.

Note that duplicate URLs in the input are not de-duplicated — the same reel listed twice is fetched twice and produces two charged rows.


How does this Facebook video scraper compare to the alternatives?

FeatureFacebook Videos Scraper: Commenters, Reactors & SharersGeneric Facebook video scraper
Entity types returnedFour: video, commenter, reactor, resharer — the last three as separately labelled rows in a child datasetTypically one: the video record, with engagement as counters only
Participant identitiesNames, profile IDs, profile URLs and accountType for each engaging accountUsually not returned
Engagement count sourcingParent-anchored — counts read only from the document segment whose decoded story token matches the requested video ID (src/anchor.py)Commonly first-match extraction from the page
Empty and failed inputsFlagged isAvailable: false with a specific unavailableReason, pushed unchargedVaries; often silently absent or stamped as success
Download formatsFull formats[] array — DASH audio, DASH video and progressive sd/hd with direct URLs and manifest_urlOften a single video_url
Input flexibilityReel URLs, bare numeric IDs, and a legacy urls alias for backward compatibilityUsually URL-only

For reference on the named alternatives, as observed on their Apify Store listings on 2026-07-25 and not measured here: lexis-solutions/facebook-videos-watch-scraper documents the output fields title, url, thumbnail, date, views, comments, reactions, channelName, channelUrl and accepts a video URL or a search query; easyapi/facebook-videos-scraper documents a query + maxItems input over Facebook Watch search results returning video metadata with a video_owner_profile object; scraper-engine/facebook-videos-scraper documents a urls + proxyConfiguration input and yt-dlp-shaped output with a raw block. None of the three describes returning commenter, reactor or resharer identities as separate rows.

If you're building an AI agent or RAG pipeline, the output format row is the decision-maker — parsing HTML inside an agent loop is a reliability failure mode, not a feature. And if your question is "which named accounts engaged with this video", the entity-coverage row is the one that matters: counters cannot answer it at all.


How many results can you scrape with this Facebook video scraper?

There is no hard cap in the Actor itself. maxItems defaults to 0, which means no limit — the Actor processes every URL you supply, one at a time, in input order. maxParticipantsPerVideo likewise defaults to 0, meaning "take whatever Facebook returns".

The real ceilings are Facebook's, and there are two:

  • Participants per kind, per video. The input schema documents Facebook's logged-out ceiling at about 19 people per kind, and states that raising maxParticipantsPerVideo above it cannot increase the yield. The Actor does not paginate participant lists, because the tooltip queries it uses return a single fixed-size connection rather than a cursor. When a list is a sample rather than the complete set, totalOfType on each participant row and commentersSampled / reactorsSampled / resharersSampled on the video row let you detect that programmatically.
  • Video availability by URL shape. Only /reel/<id> URLs reliably carry video data. Page-video and Watch URLs return uncharged isAvailable: false rows, so a batch of 1,000 Watch URLs yields 1,000 accounting rows and zero videos.

There is no discovery or pagination step: the Actor resolves the URLs you give it. To cover a creator's whole catalogue, feed it reel URLs collected by a Facebook page or search scraper. Each URL is fetched with up to 3 attempts and a 45-second request timeout (MAX_RETRIES and REQUEST_TIMEOUT in src/main.py), with a growing backoff.


Integrate this Facebook scraper and automate your workflow

Facebook Videos Scraper: Commenters, Reactors & Sharers works with any language or tool that can send an HTTP request. It is an Apify Actor, started and read through the Apify API or any Apify client library — no separate service, endpoint or API key of its own.

REST API integration

from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("<YOUR_USERNAME>/facebook-videos-scraper-commenters-reactors-sharers").call(
run_input={
"videoUrls": ["https://www.facebook.com/reel/2009829369661635"],
"includeParticipants": True,
"participantTypes": ["commenters", "reactors", "resharers"],
"minReactions": 100,
}
)
for video in client.dataset(run["defaultDatasetId"]).iterate_items():
print(video["videoId"], video["creatorName"], video["reactionsCount"], video["participantsTotal"])
child_id = video.get("childDatasetId")
if child_id:
for person in client.dataset(child_id).iterate_items():
print(" ", person["type"], person["name"], person["profileUrl"])

Works in Python, Node.js, Go, Ruby and cURL — every Apify client exposes the same run-and-read pattern, and the raw REST endpoints work from anything that speaks HTTP. Note the two-step read: the video row's childDatasetId names the dataset holding that video's commenters, reactors and resharers.

Automation platforms (n8n, Make, LangChain)

n8n ships an official Apify node. Choose the Run an Actor operation, select this Actor, and pass your videoUrls array as the run input. Follow it with Get dataset items to pull the video rows, then a second Get dataset items pointed at childDatasetId for the participants. Put a Schedule trigger in front and you have a nightly audience snapshot with no code.

Make (formerly Integromat) has an Apify app with Run an Actor and Get Dataset Items modules. Chain them the same way, then route the type field through a router so commenter, reactor and resharer records land in different destination tables or CRM lists.

LangChain integrates through the ApifyDatasetLoader / ApifyWrapper classes in the Apify integration package. Point the loader at the run's defaultDatasetId and map title, description, creatorName, reactionsCount and commentsCount into your Document fields, or point it at the child dataset and map name, type and totalOfType to build an audience-graph index your agent can query.


Scraping publicly available data is broadly permitted in the EU, UK and US, but the data this Actor returns is personal data, and that is what governs your obligations. It returns only publicly visible information from public Facebook Reels, fetched logged-out — no credentials, no groups, no friends-only, private or age-restricted content.

Commenter, reactor and resharer rows contain names, profile identifiers and profile URLs. Under GDPR and CCPA/CPRA that is personal information, regardless of having been publicly posted. If you store, enrich or act on it you are the data controller: you need a lawful basis (legitimate interest is the usual one for research and competitive analysis, and it requires a balancing assessment), you must honour access and erasure requests, and you should retain only what your stated purpose needs.

Facebook's Terms of Service also apply to your use. Consult legal counsel for commercial use cases involving bulk personal data.


Frequently asked questions

Does this Facebook video scraper work without a Facebook account?

Yes. The Actor runs fully logged-out and its input schema contains no credential field — no cookie, no c_user, no session token, no login. It fetches the public reel page with a desktop browser user-agent and calls only the Facebook GraphQL operations that are served to anonymous visitors.

How often is the scraped data updated?

Every run is a live fetch. The Actor requests each reel page fresh with cache-control: max-age=0 and reads counts and participant lists out of that response, then stamps every row with scrapedAt in UTC ISO-8601. Nothing is cached between runs. Within a single run, one thing is reused: the participant GraphQL doc_id values are resolved once from the creator's page bundles and cached for the remaining videos, because Facebook's persisted-query IDs are global per release rather than per page.

Why does the scraper return no comment text?

Because Facebook does not serve it logged-out. src/graph.py documents that every comment-list operation returns Unauthorized logged out query (error code 1675002), and the reel page HTML carries no comment bodies. What you get instead is the commenter's identity — name, id, profileUrl, accountType — as type: "commenter" rows, plus the exact commentsCount on the video row.

What happens if a Reel is deleted, private or not actually a Reel?

You get an uncharged accounting row rather than a silent gap. The Actor checks the page for media keys first; if they are absent it writes a row with isAvailable: false, status: "Unavailable" and a specific unavailableReason. Set skipUnavailable to true to drop these rows entirely, or leave it off (the default) to keep a complete audit of every input. Rows that error out during fetching appear with status: "Failed", an error string and unavailableReason: "fetch_failed", also uncharged.

Can I scrape private, friends-only or login-gated Facebook content?

No. Only publicly accessible reels are returned. The Actor has no login mechanism, so anything behind Facebook's login wall is out of reach by construction — including comment bodies and threaded replies, which Facebook gates even on otherwise-public reels.

How do I get commenters, reactors and sharers into separate tables?

Filter on type in the child dataset. All three kinds share one dataset and one field set, discriminated by type = "commenter" / "reactor" / "resharer", and all carry isChild: true. Use item.isChild === true && item.type === "reactor", or the equivalent jq / pandas / SQL expression from the separation table above. Join back to the video with parentIdvideoId.

Where do I run this Actor, and what access do I need?

On the Apify platform, from the Apify Console UI or the Apify API — those are the only two run surfaces. You need an Apify account and an Apify API token for programmatic runs; the Actor itself requires no separate registration, no Facebook developer App, and no credentials of its own. Pricing and any free-trial terms are shown on the Actor's Apify Store listing, which is the authoritative source for them.

Does it work for AI agent workflows and LLM pipelines?

Yes. The Actor is an HTTP-callable Apify Actor, so any agent framework that can make an HTTP request — LangChain, LlamaIndex, a custom tool-calling loop, an n8n or Make automation — can invoke it and read the resulting dataset. There is no MCP server for this Actor; you call it as an HTTP endpoint. Every response is typed JSON with stable field names, so it goes straight into an LLM context window, a vector store or an agent tool result without a parsing step.

Does it return data in a format LLMs can use directly?

Yes. Typed, normalized JSON with stable field names — no HTML, no selectors, no parsing. Video rows and participant rows both use fixed key sets, so a schema you define once keeps working. For token efficiency in an LLM pipeline, request a subset with the Apify dataset fields parameter and drop the formats array, which is by far the largest part of a video row.

How does it handle Facebook's anti-bot defences?

With the countermeasures actually implemented in src/main.py: a full desktop Chrome header set including sec-ch-ua client hints and sec-fetch-* metadata, a persistent aiohttp session with its own cookie jar, up to 3 attempts per URL with an increasing backoff, and a 45-second timeout per request. If an error looks like a block — the Actor matches markers such as 403, 429, access denied, checkpoint, temporarily blocked, login required and not available in your region — and no proxy was configured, it escalates to an Apify residential proxy automatically, only before the first row for that video is written, so it can never duplicate a row or a charge.

Can I use it without managing proxies?

Yes. proxyConfiguration is optional and prefilled with {"useApifyProxy": false}. Facebook reels resolve without a proxy from Apify containers, and the automatic residential escalation above covers the case where a request looks blocked. If you prefer explicit control, select Apify Proxy with the group you want, or supply your own proxyUrls — the first entry is used, and http, https and socks5 schemes are accepted.

How does it compare to other Facebook video scrapers on the Store?

The clearest difference is entity coverage: this Actor returns commenter, reactor and resharer identities as labelled rows alongside the video record. See the comparison section above for the field-level detail on lexis-solutions/facebook-videos-watch-scraper, easyapi/facebook-videos-scraper and scraper-engine/facebook-videos-scraper, as observed on their Apify Store listings on 2026-07-25 — none of the three describes returning the identities of individual commenters, reactors or resharers.

What happens when Facebook changes its structure or blocks the scraper?

The Actor is maintained, and the output schema stays stable — field names and types do not change on your end, so integrations built on videoId, type, parentId and the rest keep working. Two design choices reduce breakage in the first place: Facebook's persisted GraphQL doc_id values are resolved at runtime from the page's own JS bundles instead of being hardcoded, and engagement counts are read from the anchored document segment rather than by first match, so a change in Facebook's block ordering cannot corrupt the numbers.


Your feedback

Found a bug, hit a reel shape the Actor mis-classifies, or need a field that isn't in the output yet? We want to know. Open a ticket on the Issues tab of this Actor's Apify Store listing — the fastest route, since it attaches your run ID and input automatically. Feature requests for additional participant metadata or output fields are welcome there too.