facebook-hashtag-search-scraper-post-comments
Pricing
Pay per usage
facebook-hashtag-search-scraper-post-comments
Pricing
Pay per usage
Rating
0.0
(0)
Developer
API Empire
Maintained by CommunityActor stats
0
Bookmarked
3
Total users
1
Monthly active users
6 days ago
Last modified
Categories
Share
Facebook Hashtag Scraper — Posts, Comments and Replies as JSON
This Facebook hashtag scraper collects public posts from Facebook's hashtag feed and then expands each post into its own conversation: top-level comments, depth-1 nested replies, and the authors behind them. One run returns three row types in a single dataset — post rows, comment rows and reply rows — every one of them typed, normalized JSON with stable field names. No HTML, no CSS selectors, no parsing step. By the end of this page you will know exactly which input toggle produces which row, what every JSON key contains, and what the Actor charges for.
🔑 You need a Facebook session cookie
This is the one prerequisite that stops a run cold, so it belongs at the top rather than buried in a footnote.
Facebook's hashtag feed (https://www.facebook.com/hashtag/<tag>) is login-gated. When the Actor requests that page without a valid session, Facebook serves a stripped logged-out shell that does not ship the SearchCometResultsInitialResultsQuery GraphQL bundle at all — there is nothing to paginate. So the Actor needs two cookies from a browser where you are logged in:
c_user— your numeric Facebook user IDxs— the URL-encoded session token
Get them from DevTools → Application → Cookies → www.facebook.com, then supply them either way:
- Paste the whole raw cookie string into 🔑 Facebook cookie (
fbCookie) — the Actor parsesc_userandxsout of it for you - Or fill 🍪 c_user cookie (
c_user) and 🍪 xs cookie (xs) separately
fbCookie and xs are declared isSecret in the input schema, so Apify stores them encrypted and masks them in the Console. c_user is not marked secret — it is a public numeric profile ID rather than a credential on its own, but be aware it will appear in plain text in the run input.
If either cookie is missing, the Actor does not crash. It logs a warning, writes a single uncharged diagnostic row with status: "credentials_required" explaining exactly what to fix, and finishes as SUCCEEDED. Nothing is scraped and nothing is charged.
Two failure states are detected and reported instead of retried forever, because no proxy can fix either one:
| Detected state | status on the diagnostic row | What it means |
|---|---|---|
| Facebook served the logged-out page | cookies_expired | Your c_user / xs pair is stale or invalid — log in again and copy fresh values |
| Facebook served a checkpoint / lock page | account_checkpoint | The account is locked behind a security challenge; unlock it at facebook.com or use cookies from a different healthy account |
A note on account hygiene, straight from the Actor's own proxy strategy: Facebook ties account trust to a stable IP. That is why this Actor pins one sticky residential exit IP for a whole run rather than rotating per request, and never replays your session from a datacenter IP. Use cookies from a healthy, non-locked account.
What is Facebook Hashtag Search Scraper + Post Comments?
Facebook Hashtag Search Scraper + Post Comments is an Apify Actor that turns a list of Facebook hashtags into a structured dataset of posts and the discussion under them. It queries Facebook's own hashtag search GraphQL surface for posts, then opens each post permalink and paginates the public comment surface (CommentsListComponentsPaginationQuery) and the depth-1 reply surface (Depth1CommentsListPaginationQuery).
A Facebook login is required — see the section above. The Actor runs entirely on the Apify platform: start it from the Apify Console or call it through the Apify API with your Apify token. There is no separate signup, no vendor API key, and no browser extension to install.
What it handles:
- Scrape Facebook hashtag posts — bulk hashtags or hashtag URLs, with full post metadata, reactions, share counts, attachments and Reels video enrichment
- Scrape Facebook post comments — top-level comments per post with author, text, reaction count, reply count and timestamps
- Scrape nested comment replies — depth-1 replies attached to their parent comment and streamed as their own rows
- Sort and window the conversation — order comments by relevance, recency or Facebook's native order, and drop anything older than a date you choose
- Export as JSON, CSV, Excel, XML or HTML — every Apify dataset export format works, because the output is flat, typed JSON
- No proxy management and no parsing — the Actor acquires and pins its own residential session
What data does this Facebook hashtag scraper collect?
Three distinct entity types come back in one dataset, distinguished by the type and isChild keys on every row, plus a fourth diagnostic row type that only appears when a run produces nothing.
| Data type | Key fields | JSON field names |
|---|---|---|
| Hashtag posts | Post ID, author profile, permalink, caption text, reaction breakdown, comment/share/view counts, timestamps, attachments and Reels metadata | postId, user, url, text, topReactions, commentsCount, sharesCount, videoViewCount, time, timestamp, attachments, commentsFetched, comments |
| Post comments | Comment ID, commenter profile and avatar, comment body, reaction total, reply total, ISO date and unix timestamp, parent post | commentId, user, text, reactionsCount, repliesCount, time, timestamp, parentPostId, type ("comment") |
| Comment replies | Reply ID, replier profile, reply body, reaction total, the comment it answers, the post it belongs to | commentId, user, text, reactionsCount, parentCommentId, parentPostId, type ("reply") |
| Diagnostic rows | Why a run returned no posts, in plain language, with a machine-readable status code | type ("diagnostic"), status, message, text |
Every row — post, comment and reply alike — carries the same full set of columns, which keeps the dataset table renderable as a single flat grid and makes CSV export non-ragged. Fields that do not apply to a given row type are present with a neutral value ("", 0, {} or []) rather than absent, so downstream code never has to guard against a missing key.
Comments are delivered twice, and both copies are useful. Nested, inside the parent post row's comments array with each comment's own replies array — best when you want one record per post and the whole thread travelling with it. And flat, as their own child rows in the same dataset — best for CSV export, spreadsheet pivots, or a database table where one row equals one comment. When comments are enabled, the child rows are additionally mirrored into a separate named dataset called comments-<runId>, so you can consume the conversation alone without filtering the main dataset at all.
Need more Facebook data?
Hashtags are one entry point into Facebook, and API Empire publishes Actors for the others. If you are researching communities rather than tags, Facebook Groups Search Scraper (with group details and rules) and Facebook Group Active Members Extractor cover group discovery and membership. For brand-side work, Facebook Pages Scraper and Facebook Page Engagement Analytics Scraper handle Page profiles and engagement history. If your interest is video, Facebook Video Search Scraper builds a creator video catalogue and Facebook Videos Scraper (commenters, reactors, sharers) resolves the people behind video engagement. Pair this hashtag Actor with the video one when a campaign tag spans both feed posts and Reels.
How does this differ from the official Facebook Graph API?
Meta's Graph API is a permissioned API for content you own or have been explicitly granted access to; this Actor reads the public web surface Facebook already serves to a logged-in browser. Those are different products solving different problems, and the difference is not a matter of speed or price — it is a matter of what is reachable at all.
| Feature | Facebook Graph API | Facebook Hashtag Search Scraper + Post Comments |
|---|---|---|
| Hashtag-wide post search | Not offered for Facebook. Meta's hashtag search endpoints are part of the Instagram Graph API (ig_hashtag_search), and are scoped to Instagram Business/Creator accounts, per Meta's published Graph API reference | Core function — supply hashtags, get the posts Facebook's own hashtag feed returns |
| Access to arbitrary public posts | No. Page-level content requires the Page to be one you manage or one covered by an approved permission such as Page Public Content Access | Whatever the hashtag feed surfaces to a logged-in session |
| Setup | Create a Meta app, configure an app type, obtain access tokens, and pass App Review for any permission beyond the defaults, as documented on developers.facebook.com | Paste two cookie values into the input form and press Start |
| Comment threads on third-party posts | Only via /comments edges on objects your app is authorized to read | Any post the hashtag feed returned, with depth-1 replies |
| Rate limiting model | Documented platform, app-level and business-use-case rate limits enforced by Meta | Governed by the Actor's own pacing and page caps, documented below — not by an API quota you have to negotiate |
| Output shape | Graph API JSON, versioned; endpoints and fields deprecate on Meta's versioning schedule | One stable, documented dataset schema; field names and types do not change under your integration |
| Token lifecycle | Access tokens with expiry and refresh flows to maintain | A browser session cookie you refresh when it expires |
Use the Graph API when you are building on Pages you own or manage, when you need write access, or when you need Meta's own guarantees. Use this Actor when the data you need is public and spans accounts you have no relationship with — which is the normal situation for hashtag and social-listening work.
Why do developers and teams scrape Facebook hashtags?
For AI engineers and agent builders
A hashtag is a topic filter that Facebook maintains for you, and a comment thread is naturally occurring conversational data — which makes this Actor a convenient corpus tap for retrieval and evaluation work. A typical pipeline sets includeComments: true and includeReplies: true, then filters the dataset to type in ("comment", "reply"), embeds text, and stores commentId, parentPostId, parentCommentId, timestamp and reactionsCount as metadata so retrieval can reconstruct thread order and weight by engagement. Because the output is already typed JSON, an agent tool can consume dataset items directly — there is no HTML parsing step between the Actor and the model context window.
For marketers and brand teams
Campaign hashtags, branded tags and category tags are how a lot of Facebook activity is organized, and the comments are where the actual sentiment lives. A brand team runs the Actor daily on its own campaign tags plus two or three competitor tags, sets commentSort: "mostRelevant" so the highest-engagement comments come back first, and adds onlyCommentsNewerThan: "7 days" so each run returns only the week's fresh conversation. topReactions gives the reaction breakdown per post; reactionsCount on comment rows shows which individual replies resonated. Feed the result into a sentiment model or a weekly report.
For researchers and analysts
The Actor returns only content Facebook already serves publicly through its hashtag feed to a logged-in session — no private groups, no friends-only posts, no messages. That scope makes it usable for discourse analysis, public-health messaging research, misinformation tracking and social-movement studies, where the unit of analysis is a public post and the reactions to it. A concrete example: collect a policy hashtag with maxComments: 0 to remove the per-post comment cap, use commentSort: "chronological" to preserve Facebook's own ordering, then measure how reply depth and reactionsCount distribute across the thread. Every row carries a unix timestamp, so time series need no date parsing.
For developers building data products
Because an Apify run is an HTTP call, this Actor slots into a scheduled pipeline without glue code. Point a schedule at it, attach a webhook that fires when the run finishes, and pull defaultDatasetId items into your warehouse. The type discriminator makes routing trivial: post rows go to a posts table keyed on postId, comment and reply rows go to a comments table keyed on commentId with parentPostId as the foreign key. The comments-<runId> mirror dataset lets a second consumer read only the conversation without touching the primary dataset.
How to scrape Facebook hashtags and comments (step by step)
- Open the Actor on its Apify Store listing and click Try for free, or start it from the Apify Console. It also runs from the Apify API — see the integration section below.
- Provide the required input.
searchQueriesis the only required parameter: a list of hashtags. Plain (travel,#football) or full hashtag URLs (https://www.facebook.com/hashtag/football) both work, and the list can hold as many as you want. - Supply your Facebook session cookie. Paste the raw cookie string into
fbCookie, or fillc_userandxsindividually. Without this the run finishes cleanly but returns only a guidance row. - Set the comment options.
maxItemscontrols posts per hashtag;includeComments,maxComments,commentSort,includeReplies,maxRepliesPerCommentandonlyCommentsNewerThancontrol the conversation layer. Defaults already produce posts plus 20 comments and up to 10 replies each. - Start the run, then export. Watch the live log — every pushed post prints its author, reaction total, comment/share/view counts and a caption preview. When the run finishes, download the dataset as JSON, CSV, Excel, XML or HTML, or read it through the Apify API.
What to do when Facebook changes its structure
Facebook rotates its GraphQL persisted-query IDs and reshapes its response envelopes regularly, which is why this Actor discovers those IDs at runtime from the live page and its JavaScript bundles rather than hard-coding them. The Actor is maintained, and its dataset schema is the stable contract: field names, types and the type / isChild discriminators do not change on your end, so integrations built on them keep working across updates.
⬇️ Input
Every parameter below is read straight from .actor/actor.json. searchQueries is the only entry in the schema's required list; everything else is optional and falls back to the default shown.
| Parameter | Required | Type | Default | Constraints | What it does | Example |
|---|---|---|---|---|---|---|
searchQueries | Yes | array | ["football"] | Editor stringList; prefilled with ["football"] | One or more hashtags — plain (travel, #football) or full hashtag URLs (https://www.facebook.com/hashtag/football). Bulk input supported. | ["streetphotography", "vanlife"] |
maxItems | No | integer | 10 | minimum: 1, maximum: 10000 | How many posts to collect for each hashtag before moving on. Comments do not count against this limit. | 25 |
includeComments | No | boolean | true | — | When on, every collected post is expanded into its comment thread. Turn off to scrape posts only. | true |
maxComments | No | integer | 20 | minimum: 0; 0 means unlimited | Upper bound on top-level comments fetched per post. 0 lifts the cap and lets Facebook's own paging decide. | 30 |
commentSort | No | string | "mostRelevant" | Editor select; enum mostRelevant | newest | chronological | Orders the collected comments before capping. mostRelevant = highest reactions + replies first; newest = most recent first; chronological = as returned by Facebook. | "newest" |
includeReplies | No | boolean | true | — | Also fetch depth-1 replies for each comment that has them. | true |
maxRepliesPerComment | No | integer | 10 | minimum: 0; 0 skips replies entirely | Upper bound on replies fetched per comment. | 5 |
onlyCommentsNewerThan | No | string | (empty — no filter) | Editor datepicker, dateType: absoluteOrRelative | Keeps only comments and replies posted on or after this point. Absolute (2026-01-01) or relative (7 days, 2 weeks, 3 months). | "7 days" |
fbCookie | No | string | (empty) | Editor textfield, isSecret: true | Raw Facebook cookie string containing c_user and xs. Parsed into both automatically. | "c_user=100064381927465; xs=42%3A..." |
c_user | No | string | (empty, prefill "") | Editor textfield — not marked secret | Facebook c_user cookie (your numeric user ID). Only needed if fbCookie is empty. | "100064381927465" |
xs | No | string | (empty) | Editor textfield, isSecret: true | Facebook xs cookie (URL-encoded session token). Only needed if fbCookie is empty. | "42%3AaBcD..." |
proxyConfiguration | No | object | {"useApifyProxy": false} | Editor proxy | Optional override. Left at the default, the Actor pins its own sticky residential session; it does not run bare. | {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]} |
How the cookie inputs resolve
fbCookie is parsed first and wins per key, not wholesale. The Actor splits the string on ; (and newlines), reads c_user= and xs= out of it, and only falls back to the standalone c_user / xs inputs for whichever value the cookie string did not supply. So a fbCookie containing only c_user plus a separately filled xs field is a valid combination. Both values must end up non-empty or the run stops at the credentials diagnostic.
Complete example input
{"searchQueries": ["vanlife", "#streetphotography", "https://www.facebook.com/hashtag/travelphotography"],"maxItems": 25,"includeComments": true,"maxComments": 30,"commentSort": "mostRelevant","includeReplies": true,"maxRepliesPerComment": 5,"onlyCommentsNewerThan": "14 days","fbCookie": "c_user=100064381927465; xs=42%3AaBcDeFgHiJkLmN%3A2%3A1784505600%3A-1%3A-1","c_user": "","xs": "","proxyConfiguration": { "useApifyProxy": false }}
Common input pitfalls
Pasting a non-hashtag Facebook URL. The Actor recognises facebook.com/hashtag/<tag> and facebook.com/explore/tags/<tag>. Any other facebook.com URL falls back to its last path segment as the hashtag — so https://www.facebook.com/travel silently becomes the hashtag travel, which is probably not what you meant. URLs on non-Facebook hosts are dropped entirely. Supply plain hashtags when in doubt.
Expecting maxComments to survive the date filter. The comment cap is applied before onlyCommentsNewerThan. If you ask for 30 comments and 22 of them are older than your window, you get 8 — not 30 fresh ones. Widen the window, or raise maxComments, if you need a guaranteed volume of recent comments.
Reading commentSort as a global ranking. Sorting happens on the comments the Actor already pulled, not across the whole thread. Comment pagination stops as soon as maxComments raw comments are in hand, so mostRelevant ranks within that fetched window. To rank across an entire thread, set maxComments: 0 and sort the result yourself.
Duplicate hashtags. The list is de-duplicated case-insensitively, keeping the first spelling seen. ["Travel", "travel", "#TRAVEL"] is one hashtag, not three.
⬆️ Output
Every row lands in the run's default dataset and can be exported as JSON, CSV, Excel (XLSX), XML or HTML, or read through the Apify API. The schema is flat and uniform: all row types share the same key set, so a CSV export has no ragged columns and a JSON consumer never has to test for a missing key.
Route rows with the type discriminator:
type | isChild | Charged | Meaning |
|---|---|---|---|
"post" | false | Yes — one row_result event | A hashtag feed post |
"comment" | true | No | A top-level comment on a post |
"reply" | true | No | A depth-1 reply to a comment |
"diagnostic" | false | No | An explanation of why a run produced no posts |
Scraped hashtag post
Post rows carry all twelve base post fields plus the nine comment-thread columns. Note that reactionsCount and repliesCount are comment-level fields and are always 0 on a post row — a post's reaction data lives in topReactions, and its Facebook-reported comment total in commentsCount.
{"searchQuery": "vanlife","postId": "1240918374652190","user": {"name": "Nomad Build Co.","id": "100064381927465","url": "https://www.facebook.com/profile.php?id=100064381927465","avatar": "https://scontent.xx.fbcdn.net/v/t39.30808-1/473920184_avatar.jpg"},"url": "https://www.facebook.com/1240918374652190","text": "Six months in the Sprinter and the solar setup finally paid for itself. Full build list in the comments. #vanlife","topReactions": { "Like": 812, "Love": 143, "Wow": 22 },"commentsCount": 96,"sharesCount": 31,"videoViewCount": 0,"time": "2026-07-19 08:42:11","timestamp": 1784536931,"attachments": [{"deduplication_key": "1240918374652190:media","target": { "__typename": "Photo", "id": "1240918381318856" },"__typename": "StoryAttachment","style_list": ["photo", "fallback"],"styles": { "__typename": "StoryAttachmentPhotoStyleRenderer" },"media": { "__typename": "Photo", "id": "1240918381318856" },"all_subattachments": { "nodes": [] }}],"type": "post","isChild": false,"parentPostId": "","parentCommentId": "","commentId": "","reactionsCount": 0,"repliesCount": 0,"commentsFetched": 2,"comments": [{"id": "Y29tbWVudDoxMjQwOTE4Mzc0NjUyMTkwXzE4NzMyMDQ5","author": {"name": "Marta Kowalska","id": "100002938471029","url": "https://www.facebook.com/marta.kowalska.9484","avatar": "https://scontent.xx.fbcdn.net/v/t39.30808-1/298471029_avatar.jpg"},"text": "Which panels did you end up going with? The 200W flexible ones?","reactionsCount": 34,"repliesCount": 1,"date": "2026-07-19T09:36:17+00:00","timestamp": 1784540177,"url": "https://www.facebook.com/1240918374652190?comment_id=18732049","replies": [{"id": "Y29tbWVudDoxMjQwOTE4Mzc0NjUyMTkwXzE4NzMzMTE4","author": {"name": "Nomad Build Co.","id": "100064381927465","url": "https://www.facebook.com/profile.php?id=100064381927465","avatar": "https://scontent.xx.fbcdn.net/v/t39.30808-1/473920184_a.jpg"},"text": "Two 200W flexible plus a 100W rigid on the rack.","reactionsCount": 11,"repliesCount": 0,"date": "2026-07-19T10:04:52+00:00","timestamp": 1784541892,"url": "https://www.facebook.com/1240918374652190?comment_id=18733118","replies": []}]},{"id": "Y29tbWVudDoxMjQwOTE4Mzc0NjUyMTkwXzE4NzM0MDIx","author": {"name": "Diego Ferreira","id": "100008471203984","url": "https://www.facebook.com/diego.ferreira.7712","avatar": "https://scontent.xx.fbcdn.net/v/t39.30808-1/471203984_avatar.jpg"},"text": "That insulation job is cleaner than my apartment.","reactionsCount": 19,"repliesCount": 0,"date": "2026-07-19T11:18:03+00:00","timestamp": 1784546283,"url": "https://www.facebook.com/1240918374652190?comment_id=18734021","replies": []}]}
Field notes for post rows
| Field | Type | Notes |
|---|---|---|
searchQuery | string | The normalized hashtag this row came from — # stripped, URL resolved |
postId | string | Facebook's numeric post ID; the permalink in url is built from it |
user | object | name, id, url, avatar. url is a profile.php?id= permalink built from the owning profile ID |
url | string | https://www.facebook.com/<postId> |
text | string | Post caption. Pulled from story.message.text or, when absent, from the nested comet content section |
topReactions | object | Reaction name → count map, e.g. {"Like": 812, "Love": 143}. Names are Facebook's localized reaction labels, so the session's UI language determines the keys |
commentsCount | integer | Facebook's reported total comment count for the post — not the number collected. Compare it with commentsFetched |
sharesCount | integer | Share count from the post's UFI feedback summary |
videoViewCount | integer | Video views where Facebook exposes them; 0 for non-video posts |
time | string | "YYYY-MM-DD HH:MM:SS" derived from timestamp. Note this differs in format from the ISO-8601 date used inside comment objects |
timestamp | integer | Unix seconds — the reliable field for any time arithmetic |
attachments | array | Zero or more attachment objects (see below) |
commentsFetched | integer | How many top-level comments this run actually collected and embedded in comments |
comments | array | The embedded comment objects, each with its own replies array |
Posts that have neither text nor attachments are skipped, so a bare reshare stub with no caption will not appear in the dataset.
Attachment objects carry deduplication_key, target (__typename + id), __typename (always "StoryAttachment"), style_list, styles, media and all_subattachments. For video attachments the Actor issues a follow-up Reels query (FBReelsRootWithEntrypointQuery) and enriches the attachment in place, replacing styles with the full StoryAttachmentFBReelsStyleRenderer tree — playback video metadata, video owner, soundtrack info, shareable URL, first-frame thumbnail and reshare context, nested under attachments[].styles.attachment.style_infos[0].fb_shorts_story.short_form_video_context.
Scraped comment row
Comment rows reuse the post column set with the commenter in user, the comment body in text and the comment permalink in url. Two mappings are worth internalising: postId is empty on child rows and the owning post is in parentPostId; and commentsCount on a comment row mirrors repliesCount so the "engagement" column stays meaningful in the table view.
{"searchQuery": "vanlife","postId": "","user": {"name": "Marta Kowalska","id": "100002938471029","url": "https://www.facebook.com/marta.kowalska.9484","avatar": "https://scontent.xx.fbcdn.net/v/t39.30808-1/298471029_avatar.jpg"},"url": "https://www.facebook.com/1240918374652190?comment_id=18732049","text": "Which panels did you end up going with? The 200W flexible ones?","topReactions": {},"commentsCount": 1,"sharesCount": 0,"videoViewCount": 0,"time": "2026-07-19T09:36:17+00:00","timestamp": 1784540177,"attachments": [],"type": "comment","isChild": true,"parentPostId": "1240918374652190","parentCommentId": "","commentId": "Y29tbWVudDoxMjQwOTE4Mzc0NjUyMTkwXzE4NzMyMDQ5","reactionsCount": 34,"repliesCount": 1,"commentsFetched": 0,"comments": []}
Scraped reply row
A reply row is structurally identical to a comment row with type: "reply" and parentCommentId pointing at the comment it answers. Post ID, comment ID and parent comment ID together let you rebuild the tree without touching the nested arrays.
{"searchQuery": "vanlife","postId": "","user": {"name": "Nomad Build Co.","id": "100064381927465","url": "https://www.facebook.com/profile.php?id=100064381927465","avatar": "https://scontent.xx.fbcdn.net/v/t39.30808-1/473920184_avatar.jpg"},"url": "https://www.facebook.com/1240918374652190?comment_id=18733118","text": "Two 200W flexible plus a 100W rigid on the rack. Full list is pinned.","topReactions": {},"commentsCount": 0,"sharesCount": 0,"videoViewCount": 0,"time": "2026-07-19T10:04:52+00:00","timestamp": 1784541892,"attachments": [],"type": "reply","isChild": true,"parentPostId": "1240918374652190","parentCommentId": "Y29tbWVudDoxMjQwOTE4Mzc0NjUyMTkwXzE4NzMyMDQ5","commentId": "Y29tbWVudDoxMjQwOTE4Mzc0NjUyMTkwXzE4NzMzMTE4","reactionsCount": 11,"repliesCount": 0,"commentsFetched": 0,"comments": []}
Diagnostic row
When a run collects zero posts, the Actor does not raise and does not mark the run FAILED. It writes exactly one diagnostic row and finishes SUCCEEDED, so scheduled pipelines and monitoring do not see a phantom outage. This row type adds two keys that appear nowhere else: status and message.
{"searchQuery": "vanlife","postId": "","user": {},"url": "","text": "Facebook served the logged-out page — your `c_user` / `xs` cookies are expired or invalid. Log in at facebook.com and copy fresh values into the input.","topReactions": {},"commentsCount": 0,"sharesCount": 0,"videoViewCount": 0,"time": "","timestamp": 0,"attachments": [],"type": "diagnostic","isChild": false,"parentPostId": "","parentCommentId": "","commentId": "","reactionsCount": 0,"repliesCount": 0,"commentsFetched": 0,"comments": [],"status": "cookies_expired","message": "Facebook served the logged-out page — your `c_user` / `xs` cookies are expired or invalid. Log in at facebook.com and copy fresh values into the input."}
The four status values are credentials_required (no cookie supplied), cookies_expired (logged-out page served), account_checkpoint (account locked behind a security challenge) and no_results (nothing collected for another reason — usually a transient block or an unusual hashtag).
💰 Pricing: what actually gets charged
This Actor uses Apify's pay-per-event model with a single charged event, row_result.
- One
row_resultcharge per post row. That is it. - Comment rows are free. Every
type: "comment"row is pushed without a charged event. - Reply rows are free. Same —
type: "reply"rows carry no event. - Diagnostic rows are free. A run that fails to authenticate, hits a locked account, or returns nothing writes its explanation row uncharged. You are never billed for a failure.
- The
comments-<runId>mirror dataset is free. Child rows written there carry no event either.
So a run with maxItems: 25, maxComments: 30 and maxRepliesPerComment: 5 across one hashtag bills for at most 25 events, no matter how many thousands of comment and reply rows land in the dataset. Apify platform usage (compute units, proxy traffic) applies on top, as with any Actor.
To count or filter the billable rows in your own code, select on type:
posts = [r for r in items if r["type"] == "post"] # chargedcomments = [r for r in items if r["isChild"]] # free: comments + repliesissues = [r for r in items if r["type"] == "diagnostic"] # free: run explanations
In the Apify Console dataset view, the same split is a filter on the 🏷️ Row Type column. Exclude accounting rows entirely with type != "diagnostic".
How does this compare to other Facebook scrapers?
| Feature | Facebook Hashtag Search Scraper + Post Comments | Generic Facebook post scraper |
|---|---|---|
| Entity coverage in one run | Posts and comments and depth-1 replies, in the same dataset | Typically posts only, with comments as a separate Actor. scraper_one/facebook-posts-search documents a "Works Best With" flow that pipes post URLs into a separate Facebook Comments Scraper — as listed on the Apify Store, checked 2026-07-25; not measured here |
| Comment depth | Full comment pages plus depth-1 replies, capped by your own maxComments / maxRepliesPerComment | Often a fixed "top comments" sample. curious_coder/facebook-post-scraper advertises a topComments field for "the most engaging comments" — as listed on the Apify Store, checked 2026-07-25; not measured here |
| Input surface | Plain hashtags, #hashtags, and facebook.com/hashtag/ or /explore/tags/ URLs, in bulk, de-duplicated | Usually group / page / profile / search URLs. curious_coder/facebook-post-scraper lists Group, Page/Profile and Search URLs, checked 2026-07-25 |
| Output format | Flat typed JSON with a type discriminator; identical key set across post, comment and reply rows | Varies; nested comment arrays only, or a separate dataset per Actor run |
| Anti-bot handling | One pinned sticky residential exit IP per run, bounded rotation on a hard block, checkpoint and logged-out detection with an explicit status code | Generic "automatic proxy rotation" is the common claim across listings |
| Failure behaviour | Run finishes SUCCEEDED with an uncharged diagnostic row naming the exact cause | Commonly a FAILED run or an empty dataset with no explanation |
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 the entity-coverage row is the second: stitching two Actors together by post URL means two runs, two schemas and a join you have to maintain.
How many results can you scrape?
There is no hard result cap in the Actor itself beyond what you set — maxItems accepts anything from 1 to 10000 per hashtag, and the number of hashtags is unbounded. What actually limits a run is Facebook's own feed depth. The Actor's input schema states that Facebook's hashtag feed tops out around ~200 posts per hashtag; that figure comes from the schema's own description, not from a benchmark run here.
Pagination works as Facebook implements it. Page 1 uses the initial search query; every page after that uses the paginated query with the end_cursor returned by the previous page. The loop stops on the first of: reaching maxItems, has_next_page going false, an empty response, or a 60-page ceiling per hashtag. Between pages the Actor waits about 1.5 seconds plus a small random jitter.
Comments have their own ceilings. Per post, comment pagination runs until maxComments is reached, Facebook stops returning a cursor, or 60 comment pages have been fetched. Per comment, reply pagination stops at maxRepliesPerComment or 25 reply pages. Setting maxComments: 0 or maxRepliesPerComment: 0-as-unlimited removes your cap but not those page ceilings.
One coverage caveat worth knowing: Facebook's comment surface responds to logged-out requests too, but logged-out pagination is a subset that ends early. The Actor threads your c_user / xs session into the comment requests specifically to raise coverage, which is a second reason the cookie matters even though it is the hashtag feed that strictly requires it.
Integrate this Facebook hashtag scraper and automate your workflow
The Actor works with any language or tool that can send an HTTP request, because an Apify run is an HTTP call. Authenticate with your Apify API token — there is no separate service to sign up for.
REST API integration
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("<YOUR_USERNAME>/facebook-hashtag-search-scraper-post-comments").call(run_input={"searchQueries": ["vanlife", "streetphotography"],"maxItems": 25,"includeComments": True,"maxComments": 30,"commentSort": "mostRelevant","includeReplies": True,"maxRepliesPerComment": 5,"onlyCommentsNewerThan": "14 days","fbCookie": "<YOUR_FACEBOOK_COOKIE_STRING>",})for row in client.dataset(run["defaultDatasetId"]).iterate_items():if row["type"] == "post":print(row["postId"], row["commentsFetched"], row["text"][:80])elif row["isChild"]:print(" ", row["type"], row["parentPostId"], row["user"]["name"], row["reactionsCount"])elif row["type"] == "diagnostic":print("!", row["status"], row["message"])
Works in Python, Node.js, Go, Ruby, cURL. Branch on type to route each row to its own handler, and treat a diagnostic row as the run telling you what to fix.
Automation platforms (n8n, Make, LangChain)
In n8n, the Apify node runs the Actor and returns dataset items directly; put a Switch node on type to split post, comment and reply rows into separate branches, and an Item Lists node to expand a post row's comments array into one item per comment. In Make, the Apify module supports run-and-wait, so a daily hashtag sweep can feed a Google Sheets, Airtable or Slack step without any glue code. For LangChain and similar agent frameworks, wrap the Apify run endpoint as a tool — the response is already typed JSON, so nothing sits between the Actor and the model but a type check. Apify schedules handle recurring runs and webhooks fire your endpoint the moment a run finishes, which is the cleanest trigger for a warehouse load.
Is it legal to scrape Facebook hashtags and comments?
Reading publicly accessible content is broadly treated as permissible in most jurisdictions where no access control is circumvented, and this Actor reads only what Facebook's hashtag feed already serves to an ordinary logged-in session. It does not touch private groups, friends-only posts, messages or any content behind a privacy setting.
What it does return is unambiguously personal data: commenter and poster names, numeric profile IDs, profile URLs, avatars and the text people wrote. That brings GDPR, UK GDPR, CCPA and equivalent regimes into scope the moment you store it. You need a lawful basis for collection and retention, you should minimise what you keep, and you must be able to honour deletion requests for the individuals in your dataset. Aggregating comment text for sentiment analysis and retaining identified individual profiles indefinitely are very different risk positions.
Separately, your use of the cookies you supply is governed by Facebook's own Terms of Service — a contractual question between you and Meta. Consult legal counsel for commercial use cases involving bulk personal data.
❓ Frequently asked questions
Does this Facebook hashtag scraper work without a Facebook account?
No. Facebook's hashtag feed is login-gated, and a logged-out request returns a page that does not contain the search GraphQL bundle at all. You must supply c_user and xs — either separately or as one fbCookie string. Without them the run finishes SUCCEEDED with a single uncharged credentials_required diagnostic row and scrapes nothing.
How often is the scraped data updated?
Every run is a live fetch. Nothing is cached between runs — the Actor re-resolves Facebook's session tokens and persisted-query IDs from the live page on every run, then paginates the current feed. Within a single run the comment and reply query IDs are cached after first discovery, because they are identical across every post in that run; the data itself is always fetched fresh.
What happens when a post has no comments, or comments cannot be loaded?
The post row is still returned and still charged, with commentsFetched: 0 and comments: []. If the Actor cannot resolve a post's feedback_id or the comment query ID, it logs comments unavailable (no_doc_or_feedback_id) for that post and moves on rather than aborting the run. A comment fetch that throws is caught, logged as a warning, and the post is pushed regardless. In short: a comment-layer problem never costs you the post.
Can I scrape private groups or friends-only Facebook content?
No. The Actor only reads Facebook's public hashtag feed and the public comment surface of the posts it finds. Private groups, friends-only posts, restricted-audience posts and direct messages are out of scope — supplying a cookie authenticates your session, it does not grant the scraper access to anything your account can see that Facebook does not expose through these public surfaces.
How do I run it, and what does it cost?
It runs on Apify — start it from the Apify Console or through the Apify API with your Apify token. Pricing is pay-per-event with one event, row_result, charged once per post row. Comment rows, reply rows and diagnostic rows are uncharged. Current per-event pricing is shown on the Actor's Apify Store listing.
Does this work for AI agent workflows and LLM pipelines?
Yes. The Actor is callable as an HTTP endpoint by any agent framework — LangChain, LlamaIndex, a custom tool loop, or anything that can POST to the Apify API. Every row is typed JSON with stable keys, so there is no parsing step before passing results to an LLM. The type field acts as a discriminator an agent can branch on, and parentPostId / parentCommentId let a model reconstruct thread structure without re-fetching anything.
How does the Actor handle Facebook's anti-bot system?
With session stability rather than volume. Facebook ties account trust to a stable IP, and two behaviours reliably trigger its suspicious-activity checkpoints: replaying a session cookie from datacenter IPs, and hopping one session across many residential IPs. So the Actor pins a single sticky residential exit IP for the whole run and stays on it. On a genuine hard block — an empty data envelope, or a GraphQL error with code 1357004 / 1675004 or CRITICAL severity — it rotates to a new sticky residential session, bounded at three rotations, and never falls back to datacenter. It also paces requests with a delay plus jitter between feed pages, retries failed HTML and GraphQL fetches with exponential backoff, and detects Facebook's logged-out and checkpoint pages explicitly so it reports them instead of looping.
How does it compare to other Facebook scrapers on the Apify Store?
The main observable difference is that posts, comments and depth-1 replies arrive from one run in one dataset with one schema. Among the closest listings checked on the Apify Store on 2026-07-25: scraper_one/facebook-posts-search documents a keyword/hashtag post search with query, resultsCount, location, searchType, startDate and endDate, and directs users to a separate Facebook Comments Scraper for the conversation; curious_coder/facebook-post-scraper covers Groups, Pages/Profiles and Search URLs and advertises a topComments field; harshmaur/reddit-scraper — a different platform, but the strongest comment-thread listing in this comparison set — advertises complete comment threads with nested replies. All three descriptions are as published on those listings on that date; none of their claims were measured here.
Does it return data in a format LLMs can use directly?
Yes. Typed, normalized JSON with stable field names, no HTML and no selectors. Pass a row straight into an LLM context window, index text into a vector store with commentId / parentPostId / timestamp / reactionsCount as metadata, or route it through an agent tool. Nested and flat representations of the same comments are both available, so you can pick whichever shape your pipeline prefers without transforming anything.
Can I use it without managing proxies?
Yes. Leave proxyConfiguration at its default and the Actor acquires an Apify residential proxy with a pinned sticky session on its own — you do not configure anything. If you do supply a proxy configuration it is honoured, but a sticky session is still pinned on top so the exit IP stays constant across the run. If a residential proxy cannot be acquired at all, the Actor logs a warning and continues on a direct connection rather than aborting.
Why do I get fewer comments than maxComments?
Three reasons, all documented above. The onlyCommentsNewerThan window is applied after the cap, so an old thread returns fewer rows than you asked for. Facebook's own paging can end before your cap is reached. And logged-out comment pagination is a subset — the session cookie you supply is what raises that ceiling. commentsFetched on the post row always tells you exactly how many were actually collected; it is never an estimate.
What happens when Facebook changes its structure or blocks the scraper?
The Actor is maintained, and its dataset schema is the stable contract — field names, types and the type / isChild discriminators do not change on your end, so integrations keep working across updates. Persisted-query IDs, session tokens and the LSD/jazoest/spin parameters are all discovered at runtime from the live page and its JavaScript bundles rather than hard-coded, which is what lets the Actor survive most of Facebook's routine rotations without a redeploy. When a run genuinely cannot proceed, it tells you why in a status field instead of failing silently.
💬 Your feedback
Found a bug, or missing a field this Actor should return? We want to know. Open an issue on the Actor's Issues tab in the Apify Console — reports that include the exact input JSON and the hashtag you targeted are the fastest to reproduce and fix. Feature requests for additional comment or post fields are welcome too; the output schema is designed to be extended without breaking existing integrations.