๐ Facebook Hashtag Search Scraper
Pricing
from $4.99 / 1,000 results
๐ Facebook Hashtag Search Scraper
Pricing
from $4.99 / 1,000 results
Rating
0.0
(0)
Developer
Scraper Engine
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
1
Monthly active users
6 days ago
Last modified
Categories
Share
Facebook Hashtag Search Scraper โ Posts, Reactions and Reels Data
Facebook Hashtag Search Scraper turns any Facebook hashtag into a structured post dataset: post text, author name and profile, the full reaction breakdown by type, comment and share counts, video view counts, UTC timestamps and every attachment โ including enriched Reels metadata for video posts. One typed JSON row per post, pushed live as it is collected, exportable to CSV, Excel, XML or JSON. Paste a hashtag, add your Facebook session cookies, press Start.
โ ๏ธ Read the cookie requirement below before your first run. Facebook serves hashtag search results only to a logged-in session, so this Actor needs your own c_user and xs cookies. Without both, the run finishes cleanly and writes a single unbilled record telling you what to fix โ it does not return posts.
What is Facebook Hashtag Search Scraper?
Facebook Hashtag Search Scraper is an Apify Actor that reads Facebook's own hashtag-search GraphQL responses and shapes each result into a flat post record. It accepts plain hashtags, #hashtags and full hashtag URLs, processes as many as you supply in one run, and writes each post to the dataset the moment it is extracted.
A Facebook login is required โ not an Apify-side account, but your own browser session, supplied as the c_user and xs cookies. Facebook's hashtag feed returns nothing to an anonymous request, which is a platform behaviour, not an Actor limitation.
It is built for social listening and trend teams tracking what gets posted under a tag, growth marketers benchmarking engagement in a niche, and developers piping hashtag posts into a dashboard or an LLM pipeline.
What Facebook hashtag post data is publicly available to scrape?
Facebook's hashtag feed shows each matching post exactly as a logged-in visitor sees it in search: the caption, the author, the aggregate engagement counts and the attached media. What it never exposes is the individual people behind those counts, or anything from a non-public audience.
| Data Category | Returned by this Actor | Not exposed by the hashtag feed |
|---|---|---|
| Post caption text, post ID, canonical post URL | โ Yes | โ |
| Author display name, numeric ID, profile URL, avatar image | โ Yes | โ |
| Reaction breakdown by reaction name, comment count, share count | โ Yes | โ |
| Video view count and full Reels / FB Shorts media metadata | โ Yes | โ |
| Post creation time โ UTC string plus Unix seconds | โ Yes | โ |
| Attachments โ photos, albums, videos, share cards, life events | โ Yes | โ |
| Comment text and comment threads | โ | Aggregate count only in this feed |
| Who reacted, who shared โ individual reactor and sharer lists | โ | Not in the feed payload |
| Friends-only posts, closed-group posts, private profiles | โ | Audience-restricted |
| Author email, phone or other contact details | โ | Never in a post payload |
Facebook Hashtag Search Scraper only returns publicly visible posts โ what any logged-in visitor sees on the hashtag page. Nothing from a private profile, a friends-only audience or a closed group.
๐ช You must supply your own Facebook session cookies
This is the one prerequisite, and it is worth stating plainly rather than burying it in the input table.
Facebook's hashtag search endpoint only answers an authenticated session. The Actor bootstraps every run by loading https://www.facebook.com/hashtag/<tag> with your cookies attached, then reads the dynamic GraphQL parameters out of that page. On a logged-out response Facebook does not even ship the search query bundle, so there is nothing to paginate.
Supply two cookie values from a browser where you are already logged in:
c_userโ your numeric Facebook user IDxsโ your session token, stored as a secret input so it is masked in the Console and run history
Get both from DevTools โ Application โ Cookies โ https://www.facebook.com, or from a cookie-export extension such as Cookie-Editor.
Neither field is marked required in the schema, deliberately: a run without them finishes SUCCEEDED and writes one unbilled record with status set to credentials_required, instead of failing. Two related states are detected and reported the same way โ cookies_expired when Facebook serves the logged-out page, and account_checkpoint when Facebook has locked the account behind a security check. A checkpoint cannot be worked around by changing IP, so the Actor stops on it immediately rather than retrying. Use cookies from a healthy account, and expect that any account whose session is replayed by automation carries a real risk of being challenged or locked by Facebook.
What data can I extract with Facebook Hashtag Search Scraper?
Every row carries four groups of data: which hashtag produced it, who posted it, how the post performed, and what media is attached.
| Field Name | Description |
|---|---|
searchQuery | The normalised hashtag that produced this post, without the # |
postId | Facebook post ID |
url | Canonical post URL, built as https://www.facebook.com/<postId> |
text | Post caption / message text, "" when the post has no text |
user | Author object: name, id, url, avatar |
topReactions | Reaction counts keyed by Facebook's own reaction name, {} when none |
commentsCount | Total comments on the post, 0 when unavailable |
sharesCount | Total shares, 0 when unavailable |
videoViewCount | Video views for video and Reels posts, 0 otherwise |
time | Creation time as YYYY-MM-DD HH:MM:SS in UTC, "" when no timestamp |
timestamp | Creation time as Unix seconds, 0 when unavailable |
attachments | Array of attachment objects โ photos, albums, videos, shares |
status | Diagnostic rows only: credentials_required, cookies_expired, account_checkpoint or no_results |
message | Diagnostic rows only: the human-readable explanation of that status |
Post, author and engagement fields
searchQuery, postId, url and text identify the post. searchQuery is the cleaned tag, so #Travel, travel and https://www.facebook.com/hashtag/travel all land in the dataset as travel.
user is a nested object with four keys: name and id from the post's owning profile, url built as https://www.facebook.com/profile.php?id=<id>, and avatar taken from the author's profile picture in the story payload. All four keys are always present; any of them can be an empty string when Facebook omits the owning profile, and in that case user.url still comes back as the bare profile.php?id= form.
topReactions is a map, not a fixed list โ the keys are Facebook's localized reaction names, taken from localized_name in the response. An English session returns Like, Love, Care, Wow, Haha, Sad, Angry; a session set to another language returns those names translated. If you aggregate across accounts, key on the session language or normalise the labels yourself. commentsCount, sharesCount and videoViewCount are plain integers, and videoViewCount is read from either the story or the UFI feedback block, whichever carries it.
time and timestamp are the same instant twice. time is rendered in UTC on purpose, so the string does not shift with the container's time zone; timestamp is Unix seconds.
Attachment and Reels fields
attachments is an array. Each entry has the same eight-key shape:
| Attachment key | Description |
|---|---|
deduplication_key | Facebook's own dedupe key for the attachment, "" when absent |
target | { "__typename": <media type>, "id": <media id> } โ the fastest way to tell a Video from a Photo |
__typename | Always the literal StoryAttachment |
style_list | Facebook's renderer style tokens, e.g. ["photo", "fallback"] or ["fb_shorts_creation", "video", "fallback"], derived from the renderer when Facebook does not send them |
styles | The full renderer object as Facebook returns it |
media | The media object as Facebook returns it |
all_subattachments | { "nodes": [...] } โ album children, { "nodes": [] } when there are none |
Video attachments get a second pass. For every attachment whose target.__typename is Video, the Actor issues a separate Reels detail query and rewrites that attachment's style_list, styles and media with the full FB Shorts payload. After enrichment, styles.attachment.style_infos[0].fb_shorts_story carries post_id, creation_time, message, feedback, privacy_scope, tracking, encrypted_tracking, sponsored_data, branded_content_post_info, save_info, to and legal_reporting_uri, plus a short_form_video_context object holding playback_video, video, video_owner, shareable_url, video_owner_type, soundtrack_info, track_title, music_album_art_uri, is_original_audio_on_facebook, first_frame_thumbnail, remix_info, is_passive_content and fb_shorts_reshare_context. playback_video itself carries the player-level fields โ width, height, length_in_second, permalink_url, captions_url, is_looping, is_spherical, muted_segments, drm_info, audio_settings, preferred_thumbnail, videoDeliveryLegacyFields and the rest of the delivery block. These are passed through with Facebook's own key names rather than renamed, so a value you find in a browser response is at the same path here.
Two behaviours to plan for. A post is only written when it has either text or at least one attachment โ a story with neither is skipped rather than emitted as an empty row. And status / message are omitted entirely from post rows rather than set to null, so their presence is a reliable marker of a diagnostic record.
๐ค Add-on: Need additional Facebook data?
Hashtag search is one entry point into Facebook. Facebook Posts Scraper pulls posts from a specific page or profile timeline rather than a tag, and Facebook Group Post Scraper does the same inside groups you can access. Facebook Video Search Scraper is the better fit when video is the unit you care about, and Instagram Hashtag Scraper covers the same hashtag-research job on the other Meta surface.
Why not build this yourself?
Meta's Graph API publishes no endpoint that returns a Facebook hashtag feed to a third-party app โ check Meta's current Graph API reference before assuming otherwise, and note that the hashtag-search endpoints it does document belong to Instagram, not Facebook. So the alternative to this Actor is your own scraper, and the maintenance load is the real cost.
Facebook's hashtag results come from persisted GraphQL queries, and every request needs values that only exist inside the live page: an fb_dtsg token, an lsd token, jazoest, the haste session, the spin parameters, and โ the awkward part โ the numeric doc_id of three separate operations (SearchCometResultsInitialResultsQuery, SearchCometResultsPaginatedResultsQuery, FBReelsRootWithEntrypointQuery). Those IDs change with Facebook's build. This Actor extracts them from the hashtag page HTML, and when they are not there it fans out across the page's static.xx.fbcdn.net JavaScript bundles to find them, with a dedicated fallback path through /reel/ for the Reels query. It also has to unwrap Facebook's for (;;); anti-CSRF prefix and split the multi-object streamed response before any JSON parsing happens.
The second cost is the session. Facebook ties account trust to a stable IP, so a naive residential proxy URL โ which rotates its exit IP per request โ scatters one authenticated session across dozens of addresses and reliably triggers a checkpoint lock. Getting this right means pinning a sticky session, keeping it for the whole run, rotating only on a genuine hard block, and telling a transient block apart from an expired cookie and from a locked account. That is the logic that keeps a run producing rows instead of producing an account you can no longer log into.
How to use Facebook Hashtag Search Scraper
Facebook Hashtag Search Scraper runs on Apify. Start it from the Apify Console, or call it through the Apify API.
- Open Facebook Hashtag Search Scraper on Apify and click Try for free
- Enter one or more hashtags or hashtag URLs in Hashtags or Hashtag URLs (
searchQueries) โ this is the only schema-required input - Paste your
c_userandxsFacebook cookies โ required in practice, see the cookie section above - Set Max Posts Per Hashtag (
maxItems) โ this is per hashtag, not a run total - Leave Proxy Configuration at its default unless you have a specific reason to override it
- Click Start, watch posts stream into the Output tab live, then export as JSON, CSV, Excel or XML
The log prints one line per saved post with the author, reaction total, comment, share and view counts, so you can see whether a tag is worth a bigger run before committing to one.
How to scale to bulk hashtag extraction
searchQueries is a list โ add as many hashtags as you like and each is processed in turn into the same dataset. Plain tags, #tags and full facebook.com/hashtag/... or facebook.com/explore/tags/... URLs can be mixed freely; every entry is normalised to a bare slug and duplicates are dropped case-insensitively, so Travel, #travel and the travel hashtag URL together produce one hashtag, not three.
maxItems applies per hashtag, so ten hashtags at maxItems: 50 is a target of 500 rows, not 50. A hashtag that yields nothing does not stop the run โ the loop moves to the next one. Expired cookies and a locked account do stop it, because neither can be fixed by continuing.
What can you do with Facebook hashtag data?
- ๐ A social listening analyst tracking a campaign tag runs it daily and diffs
postIdsets between runs to surface only new posts, ranking them bytopReactionstotals andsharesCount. - ๐ฏ A growth marketer benchmarking a niche pulls several competitor tags in one run and compares median
commentsCountandvideoViewCountper tag to decide where the engaged audience actually is. - ๐ฌ A short-form video researcher filters rows where
attachments[].target.__typenameisVideo, then readsplayback_video.length_in_secondandsoundtrack_infofrom the enriched Reels block to study what length and audio pattern travels. - ๐งญ A brand-safety reviewer monitoring a branded tag scans
textanduser.namefor impersonation and off-brand usage, usingurlto jump straight to the post that needs a report. - ๐ค An AI engineer building a trend agent indexes
text,searchQueryandtimeinto a vector store and keepstopReactions,commentsCountandsharesCountas metadata filters, so the agent answers "what is resonating under this tag this week" against live posts rather than a stale export.
Every one of these is callable from an agent framework over the Apify API, since the Actor is a standard HTTP-triggered run.
How does Facebook Hashtag Search Scraper handle rate limits and blocking?
No CAPTCHA solving is involved, and no browser is launched. The Actor speaks to https://www.facebook.com/api/graphql/ over plain HTTP with your session cookies and a full desktop Chrome header set, which is what the endpoint expects.
The networking model is a sticky residential exit IP, held for the whole run. Facebook ties account trust to a stable IP, so hopping addresses mid-session is what triggers its "suspicious activity" locks. Requests therefore go out through one Apify Residential session by default, and only rotate to a fresh single IP on a genuine hard block โ never back to a datacenter address. Rotation is bounded at three new sessions; after that the request gives up rather than burning the account. If you configure your own proxy, it is honoured and a sticky session is still pinned so the exit IP stays constant.
A response counts as blocked when it is not valid JSON, carries no data object, or returns Facebook error code 1357004 or 1675004 or a CRITICAL severity. Each page request retries up to six times across that escalation, and the initial session bootstrap gets its own six attempts. Requests time out at 30 seconds; a timeout is treated as a block. Pages are fetched sequentially with a delay of roughly 1.5 seconds plus jitter between them, and Reels enrichment runs at a concurrency of 8.
Failures never fail the run. A hashtag that cannot be collected is logged and skipped, an expired cookie or a checkpoint stops early with a named reason, and if the run ends with zero posts a single unbilled diagnostic row explains why.
โฌ๏ธ Input
| Parameter | Required | Type | Description | Example Value |
|---|---|---|---|---|
searchQueries | โ Yes | array | One or more hashtags (football, #football) or full Facebook hashtag URLs (https://www.facebook.com/hashtag/football). Bulk input supported; entries are normalised and deduplicated. Default ["football"]. | ["football", "travel"] |
maxItems | No | integer | Maximum posts to collect per hashtag. Minimum 1, maximum 10000. Default 10. | 50 |
c_user | No | string | Your Facebook c_user cookie โ the numeric user ID. Not schema-required, but required in practice to collect any posts. | "100012345678901" |
xs | No | string | Your Facebook xs cookie โ the URL-encoded session token. Stored as a secret. Required in practice alongside c_user. | "46%3AAbCdEf..." |
proxyConfiguration | No | object | Apify Proxy settings. Default {"useApifyProxy": false}. See the note below โ the default does not mean "no proxy". | {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]} |
Three honest notes on how these behave at runtime:
proxyConfigurationdefault is misleading. The field's default is{"useApifyProxy": false}and its schema description still describes an oldnone โ datacenter โ residentialfallback chain. The current implementation does not do that. When you leave the field alone, the Actor goes straight to a sticky Apify Residential session and stays on it, because the platform's direct egress is a datacenter IP that Facebook blocks for authenticated sessions. Escalation never returns to datacenter either: a configured non-residential proxy jumps to residential on the first hard block, and residential rotates to a fresh sticky IP up to three times. Treat the default as "let the Actor pick residential", not as "run without a proxy" โ and budget residential proxy usage accordingly.maxItemsis a target, not a promise, and it is per hashtag. The schema accepts up to 10000, but see the ceiling note in the Output section. The value is not clamped in code, so an out-of-range value sent via the API is bounded only by Facebook's own end-of-feed and the Actor's internal pagination cap.searchQuerieshas an undocumented alias and a different runtime fallback. The Actor also reads aurlskey ifsearchQueriesis absent, which is handy when piping output from another Actor. And if nothing resolves to a usable hashtag, it falls back totravelโ not to the schema defaultfootball.
Example input
{"searchQueries": ["football","#travelphotography","https://www.facebook.com/hashtag/streetfood"],"maxItems": 50,"c_user": "100012345678901","xs": "46%3AAbCdEfGhIjKlMn%3A2%3A1753400000%3A-1%3A9999","proxyConfiguration": { "useApifyProxy": false }}
โฌ๏ธ Output
Every post is one dataset row with the same twelve keys in the same order, run after run. Missing values arrive as typed empty defaults โ "" for strings, {} for the reaction map, [] for attachments, 0 for counts โ never as a missing key, so downstream code needs no presence checks. Rows are pushed live as each post is extracted, so a run you stop early still leaves you everything collected up to that point. Export as JSON, CSV, Excel or XML, or read the dataset through the Apify API.
Billing is per row: each post row is pushed with the row_result charged event, one event per post. Diagnostic rows are pushed with no charged event at all โ the credentials_required, cookies_expired, account_checkpoint and no_results records are free. They are also easy to exclude, because status is absent from real post rows:
posts = [row for row in items if "status" not in row]
โ ๏ธ Per-hashtag ceiling. Facebook's hashtag feed stops paginating long before maxItems' 10000 maximum, and the Actor also caps itself at 60 result pages per hashtag. This Actor's own changelog records an observed ceiling of roughly 200 posts per hashtag. Plan around collecting a few hundred posts per tag and running more tags, rather than expecting thousands from one.
Example output
A post row with an enriched Reels attachment (the Facebook-passthrough blocks are abbreviated with โฆ, but every key name shown is the real one):
{"searchQuery": "streetfood","postId": "1017535753998725","user": {"name": "Bangkok Bites","id": "100072269304011","url": "https://www.facebook.com/profile.php?id=100072269304011","avatar": "https://scontent-lhr8-1.xx.fbcdn.net/v/t39.30808-1/429972849_122130234884145259_n.jpg"},"url": "https://www.facebook.com/1017535753998725","text": "Boat noodles at 6am, three stalls deep into Talat Noi. #streetfood #bangkok","topReactions": {"Like": 2694,"Love": 891,"Care": 142,"Wow": 63,"Haha": 31,"Sad": 4,"Angry": 1},"commentsCount": 158,"sharesCount": 74,"videoViewCount": 143521,"time": "2026-07-19 04:11:22","timestamp": 1784434282,"attachments": [{"deduplication_key": "0119ac5697c575b62f508b681459152f","target": {"__typename": "Video","id": "2193122174769647"},"__typename": "StoryAttachment","style_list": ["fb_shorts_creation", "video", "fallback"],"styles": {"__typename": "StoryAttachmentFBReelsStyleRenderer","__isStoryAttachmentStyleRendererUnion": "StoryAttachmentFBReelsStyleRenderer","is_prod_eligible": true,"attachment": {"style_list": ["fb_shorts_creation", "video", "fallback"],"style_infos": [{"__typename": "FBShortsShareAttachmentStyleInfo","fb_shorts_story": {"post_id": "1017535753998725","creation_time": 1784434282,"message": { "text": "Boat noodles at 6am, three stalls deep into Talat Noi." },"short_form_video_context": {"playback_video": {"id": "2193122174769647","width": 1080,"height": 1920,"length_in_second": 47,"permalink_url": "https://www.facebook.com/reel/2193122174769647/","is_looping": true,"is_spherical": false,"captions_url": null,"muted_segments": [],"videoDeliveryLegacyFields": {"dash_manifest_url": null,"browser_native_hd_url": null,"browser_native_sd_url": null,"dash_manifest_xml_string": null,"id": "2193122174769647"}},"video": {"__typename": "Video","id": "2193122174769647","playable_duration_in_ms": 47120,"owner": { "id": "100072269304011", "name": "Bangkok Bites" }},"video_owner": { "id": "100072269304011", "name": "Bangkok Bites" },"shareable_url": "https://www.facebook.com/reel/2193122174769647/","track_title": "Original audio","is_original_audio_on_facebook": true,"is_passive_content": false,"fb_shorts_reshare_context": { "is_reshare": false, "reshare_creator": null },"first_frame_thumbnail": "https://scontent-lhr8-1.xx.fbcdn.net/v/t15.5256-10/frame0.jpg"},"feedback": { "โฆ": "Facebook feedback block, passed through" },"privacy_scope": { "โฆ": "as returned" }},"enable_horizontal_ufi": false,"enable_comet_reels_unification_for_ifr": false},{ "__typename": "FeedStandardAttachmentStyleInfo" },{ "__typename": "FutureOfFeedAttachmentStyleInfo" }]}},"media": {"__typename": "Video","allow_unencrypted_playback": true,"__isNode": "Video","id": "2193122174769647"},"all_subattachments": { "nodes": [] }}]}
An unbilled diagnostic row, written when a run collects nothing. It carries the same twelve keys with empty defaults, plus status and message:
{"searchQuery": "streetfood","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 `c_user` and `xs` cookie values into the input.","topReactions": {},"commentsCount": 0,"sharesCount": 0,"videoViewCount": 0,"time": "","timestamp": 0,"attachments": [],"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 `c_user` and `xs` cookie values into the input."}
How does it work?
Each hashtag starts with one page load. The Actor requests https://www.facebook.com/hashtag/<tag> through a sticky residential IP with your session cookies attached, and reads the dynamic parameters Facebook embeds there โ the fb_dtsg and lsd tokens, jazoest, the haste session and spin values, the initial query variables, and the numeric doc_id for the initial, paginated and Reels queries. When a doc_id is not in the HTML, it scans the page's JavaScript bundles until it finds one.
With those in hand it calls Facebook's GraphQL endpoint directly, page by page, following the feed's own end_cursor and stopping when has_next_page goes false, your maxItems target is met, or the internal page cap is reached. Each response is stripped of Facebook's for (;;); prefix, parsed, and walked for the story behind every result. Video attachments then get a second Reels query to fill in full short-form metadata.
Because it reads Facebook's structured GraphQL payload rather than rendered HTML, a Facebook front-end redesign does not change your field names โ the twelve output keys stay the same across runs. Only publicly visible hashtag-feed posts are collected.
Integrations
Facebook Hashtag Search Scraper is an Apify Actor, so it works with anything that can call the Apify API or consume a dataset.
Calling Facebook Hashtag Search Scraper from Python
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("<YOUR_USERNAME>/facebook-hashtag-search-scraper-pay-per-events").call(run_input={"searchQueries": ["streetfood", "#travelphotography"],"maxItems": 50,"c_user": "<YOUR_C_USER_COOKIE>","xs": "<YOUR_XS_COOKIE>",})for row in client.dataset(run["defaultDatasetId"]).iterate_items():if "status" in row:print("diagnostic:", row["status"], row["message"])continueprint(row["searchQuery"], row["user"]["name"], row["commentsCount"], row["url"])
Works in Go, Ruby, Node.js, cURL โ any language that can make an HTTP request. Branch on the presence of status to keep diagnostic rows out of your post pipeline.
Scheduling and webhooks
Because cookies expire, hashtag monitoring works best on a schedule you can see failing. Use an Apify schedule to run the same searchQueries daily, and attach an Actor webhook on run success to POST the dataset to your endpoint. A run whose dataset contains a single row with status set to cookies_expired is your signal to refresh the xs value โ worth alerting on explicitly.
No-code tools (n8n, Make, LangChain)
In n8n, use the Apify node โ or an HTTP Request node pointed at the Apify run endpoint with your token โ and pass the same JSON input shown above; an IF node on status splits real posts from diagnostics. In Make, the Apify module supports run-and-wait, so a daily tag sweep can feed a Google Sheets or Airtable step directly. In LangChain, wrap the run endpoint as a tool and hand the typed rows to the agent as-is; no parsing step is needed.
Is it legal to scrape Facebook hashtag posts?
Scraping publicly visible posts is broadly treated as permissible where the content is accessible without circumventing an access restriction, and this Actor returns only public posts from Facebook's hashtag feed โ nothing from a private profile, a friends-only audience or a closed group. But this output is personal data, and that changes your obligations rather than removing them.
The fields that carry personal data are specific and worth naming: user.name, user.id, user.url and user.avatar identify a real person, text is content they authored and can itself contain names and other personal details, postId and url link straight back to them, and the enriched Reels blocks carry further identity objects under short_form_video_context.video_owner, short_form_video_context.video.owner and fb_shorts_reshare_context.reshare_creator. Under GDPR you need a lawful basis before you store or process any of it โ legitimate interests is the usual route for research and brand monitoring, and it requires a documented balancing test, a retention limit, and a way to honour deletion and objection requests. CCPA/CPRA treats the same fields as personal information about California residents.
Data minimisation is the practical control here, and this output makes it easy: if you only need engagement patterns, drop user, text and the enriched styles blob at ingest and keep the counts, the timestamp and the hashtag. Aggregate rather than storing rows where you can.
Separately, you are supplying your own Facebook session cookies, which is governed by Facebook's Terms of Service โ read them and decide for yourself. Consult legal counsel if your use case involves bulk storage of personal data, profiling of identified individuals, or transferring these rows outside your jurisdiction.
โ Frequently asked questions
What Facebook hashtag fields does Facebook Hashtag Search Scraper return?
The five most used are text, user, topReactions, commentsCount and url. Twelve keys are returned on every post row โ plus status and message on unbilled diagnostic rows. See the data fields table above for all of them, including the attachment and Reels shapes.
Does Facebook Hashtag Search Scraper require a Facebook account or login?
Yes โ your own session, supplied as the c_user and xs cookies. Facebook's hashtag search endpoint returns nothing to an anonymous request, so this is unavoidable rather than a design choice. Neither field is marked required in the schema, but without both the run writes one unbilled credentials_required record and collects no posts. The xs field is stored as a secret, so it is masked in the Console and run history.
How many posts can I extract per hashtag?
maxItems accepts 1 to 10000 and applies per hashtag, but Facebook's feed ends the pagination well before that, and the Actor caps itself at 60 result pages per hashtag. This Actor's changelog records an observed ceiling of roughly 200 posts per hashtag. For volume, add more hashtags to searchQueries rather than raising maxItems.
What happens if a hashtag returns zero results?
It depends on whether the whole run came back empty. If some hashtags produced posts and one did not, that hashtag is simply absent from the dataset โ the log notes it and the run continues to the next tag, with no row and no charge for it. If the entire run collected nothing, the Actor writes exactly one unbilled diagnostic row and still finishes SUCCEEDED, with status set to the reason: no_results for a transient block or an unusual tag, credentials_required when cookies were missing, cookies_expired when Facebook served the logged-out page, or account_checkpoint when the account is locked. In code, filter with [row for row in items if "status" not in row] and branch on row["status"] for the rest โ status never appears on a real post row.
Can I scrape multiple Facebook hashtags at once?
Yes. searchQueries is a list and each entry is processed in turn into the same dataset. Plain tags, #tags and hashtag URLs can be mixed; everything is normalised to a bare slug and deduplicated case-insensitively. Remember that maxItems is per hashtag, so the run total is roughly maxItems ร number of hashtags.
Does Facebook Hashtag Search Scraper work with Claude, ChatGPT and other AI agent tools?
Yes. It is callable as a standard HTTP endpoint through the Apify API, so LangChain, CrewAI, n8n or a hand-written tool definition can invoke it and receive typed JSON with no parsing step. Give the agent the same run input shown above and have it skip rows where status is present.
How does Facebook Hashtag Search Scraper compare to other Facebook hashtag scrapers?
Checked on the Apify Store on 25 July 2026: easyapi/facebook-hashtag-search-scraper is the most used of the three and returns a very similar post shape โ postId, user, text, topReactions, commentsCount, sharesCount, videoViewCount, time, timestamp, attachments โ from a single searchQuery string plus maxItems; bulk hashtag input and cookie authentication are not documented in its README. datapilot/facebook-hashtag-search-scraper documents the same field set from a single hashtag input, and its own README describes the current implementation as generating sample posts to demonstrate the data structure, with real scraping left as an integration step. scrapium/facebook-hashtag-search-scraper documents the closest feature set โ bulk searchQueries, c_user / xs cookies, Reels enrichment โ and describes a none โ datacenter โ residential proxy chain. What this Actor documents that the others do not: bulk input with hashtag-URL normalisation and deduplication, the sticky-residential session model with named checkpoint and expired-cookie detection, unbilled diagnostic rows with a documented status vocabulary, and the per-hashtag ceiling stated up front.
Does Facebook Hashtag Search Scraper return data in a format LLMs can use directly?
Yes. Every row is typed, normalized JSON with the same field names on every run. No HTML parsing, no selectors. Pass a row straight into an LLM context window, index it into a vector store, or hand it to an agent tool. If context size matters, drop the attachments blob โ the enriched Reels payload is by far the largest part of a video row.
What happens when Facebook changes its layout or anti-bot system?
The scraper is maintained, and because it reads Facebook's GraphQL payload rather than rendered markup, a visual redesign generally does not affect it. The exposure is Facebook's build-specific doc_id values and token names; those are extracted from the live page and its JavaScript bundles on every run rather than hardcoded, which is what lets a new Facebook build work without a code change. Your field names and types stay the same on your end.
Can I use Facebook Hashtag Search Scraper without managing proxies or browser infrastructure?
Yes. There is no browser to run and no proxy account to create โ the Actor uses Apify Residential with a pinned sticky session, holds one exit IP for the whole run, and rotates to a fresh one only on a hard block. It does not solve CAPTCHAs, because the flow it uses does not present one; what it does handle is block detection, proxy escalation, retry limits and telling a transient block apart from an authentication problem. The one thing you manage yourself is your Facebook cookies, which expire and need refreshing.
Which Facebook hashtag fields work best for AI training data and RAG indexing?
For RAG indexing: text is the high-information field and chunks cleanly on its own, with searchQuery and user.name as useful metadata for filtering and time for recency ranking. For training data and analytics: topReactions, commentsCount, sharesCount, videoViewCount and timestamp are the most structurally consistent fields across records, since they always arrive with typed defaults rather than going missing. All values are returned as typed primitives, arrays or plain nested objects, so no normalization pass is needed before indexing.
๐ Related scrapers
| Scraper Name | What it extracts |
|---|---|
| Facebook Posts Scraper | Posts from a specific Facebook page or profile timeline |
| Facebook Group Post Scraper | Posts and engagement from inside Facebook groups |
| Facebook Video Search Scraper | Facebook video search results with video metadata |
| Facebook Pages Recent Posts Scraper | The latest posts across a list of Facebook pages |
| Instagram Hashtag Scraper | The same hashtag-research job on Instagram |
| Twitter Trends Scraper | Trending topics for timely content and tag planning |
๐ฌ Your feedback
Found a bug, or need a field that is in Facebook's hashtag payload but not in the output? Open an issue on the Actor's Issues tab and it will be looked at. Reports that include the exact searchQueries value and the run ID are the fastest to reproduce and fix.