Facebook Post Search & Keyword Scraper API
Pricing
from $0.99 / 1,000 post delivereds
Facebook Post Search & Keyword Scraper API
Search public Facebook posts by keyword, phrase, or hashtag. Export globally deduplicated posts with text, authors, dates, engagement, media, and match evidence. Use multiple queries in one run, verify literal matches, and distinguish confirmed no-results from blocked or partial searches.
Pricing
from $0.99 / 1,000 post delivereds
Rating
0.0
(0)
Developer
Lasse
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
an hour ago
Last modified
Categories
Share
Find public Facebook posts by keyword, phrase or hashtag, or extract details from individual post links. Get deduplicated posts with text, publication dates, authors, engagement and media when Facebook exposes them. Add visible top comments, spreadsheet output and repeat-run deduplication when you need them.
No Facebook account, cookies or Facebook API key required. Keyword searches use Google's web index to discover Facebook post links. Post details come from the public Facebook document, not Google snippets. Direct post links skip Google discovery.
Start with a small search
Paste this complete input for a bounded first run:
{"queries": ["James Webb Olhar Digital"],"maxPostsPerQuery": 3,"maxUniquePosts": 3,"maxSerpPagesPerQuery": 1,"maxDocumentsPerQuery": 6,"maxTotalRequests": 48,"maxTotalResponseMiB": 24,"maxConcurrency": 1}
Replace the example with your own topic. The Form is prefilled with the same small search and work limits. Result limits are maximums, not promises: filters, unavailable public documents and index coverage can reduce the result count. Source response times vary; a run can spend time waiting for Google before posts appear.
For a larger search, increase Posts per search, Total post limit and, if necessary, the document and page bounds. JSON/API defaults are 20 posts per search, 500 in total, 5 index pages and 24 Facebook documents per search; these differ from the smaller Form prefill.
What you can do
- Combine searches. Up to 100 inputs across keywords, links and extra hashtags. A post found by several searches becomes one row with all contributing queries.
- Require literal matches. Verify words, phrases, hashtags, OR alternatives and exclusions against collected post text.
- Filter observed posts. Use publication dates, content and author types, language, engagement minimums, tagged-place IDs and observed reshares.
- Monitor without returning the same posts each run. Turn on
onlyNewPostsand reuse a monitoring profile. - Collect visible top comments. Save comments already exposed with the target post, inline and in a separate table.
- Choose nested or spreadsheet output. Use full JSON, flat scalar columns or an optional
posts.csvfile. - Review results visually. Open
REPORT.htmlfor post cards, dates, metrics, media, comment excerpts and search coverage. - Understand incomplete runs. Coverage distinguishes completed, empty, partial, blocked and failed acquisition.
Keywords, hashtags and Facebook links
Use Keywords or Facebook links (queries) for any combination of:
| Input | Behavior |
|---|---|
James Webb | Discover indexed Facebook post links for the topic |
#ClimateTech | Discover indexed posts for the hashtag |
https://www.facebook.com/search/posts/?q=James%20Webb | Extract the search text and use index discovery |
https://www.facebook.com/hashtag/climatetech | Extract the hashtag and use index discovery |
| An individual Facebook post, video, Watch or Reel link | Fetch that post directly, with no Google search |
hashtags adds separate hashtag searches, with or without #. At least one queries entry is required; keywords and extra hashtags together must not exceed 100 entries. Query entries are limited to 300 characters each. Repeated searches are merged while preserving their input positions.
Search links reuse their text, not Facebook's native feed or filters. Unsupported embedded filters, profile/collection links, comment targets and non-Facebook links fail validation before acquisition. Direct post links establish the target, so keyword verification and search ranking do not apply to them. Date and post filters still apply.
Keyword matching
The default keeps relevant index discoveries. Select a stricter mode when the collected text must satisfy your query:
matchMode | Rule |
|---|---|
SOURCE_RESULT | Keep source-ranked results; no literal text verification |
CONTAINS | Query text appears as a normalized substring |
EXACT_PHRASE | Query words appear consecutively and in order |
WHOLE_WORD | Every query word appears as a complete word |
HASHTAG | The requested hashtag is observed; # is optional |
ALL_TERMS | All term groups match; supports phrases, OR alternatives and exclusions |
ANY_TERM | At least one positive group matches; exclusions still apply |
For example, ALL_TERMS with "electric vehicle" battery OR charging -jobs requires the phrase “electric vehicle”, either “battery” or “charging”, and excludes “jobs”. Matching is case- and diacritic-insensitive. Expressions have at most 64 tokens. Parenthesized or nested Boolean expressions are not part of the local grammar.
Strict modes reject posts without a verified contributing keyword query before saving or charging. A post may verify for one query and fail another; matchedQueries preserves that distinction. A supplied direct link remains eligible without a keyword match. Missing text cannot establish a strict match. Google snippets never become post text.
Dates and filters
dateFrom and dateTo accept a real YYYY-MM-DD, an ISO timestamp or a relative value such as 7 days, 24 hours or 1 month. Relative values are resolved once when the run starts. Offsetless timestamps use UTC; a date-only upper bound includes that entire UTC day.
{"queries": ["electric vehicles"],"dateFrom": "7 days","unknownDate": "DROP","matchMode": "ALL_TERMS","maxPostsPerQuery": 10,"maxScannedPostsPerQuery": 50,"maxUniquePosts": 10,"filters": {"postTypes": ["VIDEO", "REEL"],"minReactions": 10}}
Filters check the acquired sample locally; they do not turn the index into a complete historical or live Facebook feed. unknownDate defaults to KEEP_FLAGGED: unknown publication dates stay in the results with a note. Use DROP when only posts with observed dates inside the requested range are acceptable.
filters field | Accepted values / behavior |
|---|---|
sourceTypes | PAGE, GROUP, PROFILE, EVENT, UNKNOWN |
postTypes | TEXT, LINK, PHOTO, ALBUM, VIDEO, REEL, SHARE, UNKNOWN |
authorTypes | PAGE, PROFILE, UNKNOWN; source-provided identity |
languages | Lowercase two-letter codes, for example ["en", "es"]; detected from text |
minReactions, minComments, minShares | Minimum observed count; an unknown count does not meet a positive minimum |
excludeReshares | Exclude explicitly observed Facebook reshares; unknown share status stays flagged |
taggedLocationIds | Explicit Facebook place IDs observed on posts; unknown/nonmatching places are excluded |
mediaOnly, videoOnly, linkOnly | Additional JSON/API switches for media, videos/Reels or external links |
Language detection abstains on short or ambiguous text. Unknown languages are excluded when a language filter is selected. Tagged-place filtering is not a native location search and does not infer a post's location from its author's address or text.
Search depth matters with filters. maxScannedPostsPerQuery controls how many raw posts may be inspected; it defaults to maxPostsPerQuery. Increase it when filtering or monitoring discards candidates. Facebook document, index page, request, response-byte and time limits still apply. A larger scan limit alone does not raise those limits. Posts shared by multiple queries must fit every accepted contributing query's result ceiling, so overlapping searches can return fewer posts.
Repeat-run monitoring
Turn on Only posts not returned before (onlyNewPosts) and keep the same monitorName and search/filter configuration:
{"queries": ["James Webb Olhar Digital"],"onlyNewPosts": true,"monitorName": "space-news","maxPostsPerQuery": 3,"maxScannedPostsPerQuery": 12,"maxUniquePosts": 3,"maxSerpPagesPerQuery": 1,"maxDocumentsPerQuery": 12,"maxTotalRequests": 48,"maxTotalResponseMiB": 24,"maxConcurrency": 1}
A subsequent run suppresses posts already saved for the same search configuration without another post event. isNew means new to that monitoring configuration, not recently published. A different query, meaningful filter or profile has independent history. A post previously seen under one query can be new under another.
History retains identity/search hashes and timestamps, not post text. The default retention is 90 days (monitorRetentionDays: 1–365), with capacity for 51,200 hashes. One post can use several hashes. Expired or capacity-evicted entries may reappear. Concurrent runs using the same profile are refused before acquisition; use separate profiles for independent jobs. Storage or lease failures stop delivery and are reported explicitly.
For manual exclusions, use excludePostIds with post IDs/permalinks, or select a read-only previousDatasetId. These work independently of persistent monitoring. Monitoring revisits the bounded index sample; it cannot guarantee discovery of every newly published mention.
Posts, comments and exports
The default Dataset contains one row per unique saved post. outputFormat: "FULL" is the default; "FLAT" uses scalar columns and JSON text cells for nested details. The Flat view suits flat rows. The HTML preview uses the same readable cards for either format.
| Data | Fields / behavior |
|---|---|
| Post | postId, canonicalUrl, sourceUrl, text, publishedAt, capturedAt, postType |
| Author and source | Public ID, name, URL, author picture, business-page status and Page/Group/Profile/Event context when exposed |
| Engagement | Reactions, reaction types, comments, shares and views; totalEngagement is available only when reactions, comments and shares are all known |
| Media and links | Images, video URLs/duration, thumbnails, available dimensions/captions/quality variants, video title, link attachments and external links |
| Additional details | Hashtags, mentions, collaborators, tagged place, detected language and a separate original sharedPost when observed |
| Comments | Optional topComments with text, public author details, observed date, reactions and reply count |
| Evidence | matchedQueries, verifiedMatch, requested/applied scope, acquisition provenance, canonical identity and limitations |
Unavailable data stays null, empty or omitted according to the field contract. Zero is kept only when observed. Media links can expire. A known post may expose text but no metrics, or media but no publication date.
Set includeTopComments: true to collect up to maxTopCommentsPerPost visible top-level comments (default 10, maximum 50). This reads comments already exposed with the proved target post; it does not request a full thread, replies or comment pagination. Comments remain inside the post and are also saved in the Top comments Dataset. They add no post-delivered event.
The Search summary Dataset has one row per query, including partial, failed and empty searches. Default post views are Posts, Engagement, Media & links, Matching & coverage, Flat and Authors. Views do not duplicate or unwind post rows.
The run's default Key-value store contains:
| Record | Contents |
|---|---|
REPORT.html | Readable HTML preview, up to 100 posts and 4,000 text characters per post, within 2 MiB |
RUN_SUMMARY | Coverage, counts, filters, monitoring, export/table outcomes, acquisition and billing diagnostics |
CHARGE_JOURNAL | Per-post persistence and charge decisions |
posts.csv | Optional UTF-8 CSV when writeCsv: true; scalar columns, quoted cells and spreadsheet-formula protection |
The CSV file is capped at 32 MiB and never cuts a row. RUN_SUMMARY reports truncation or a failed optional artifact write, including a safe failure category and HTTP status when available; the Dataset retains saved post rows. The HTML preview includes empty/incomplete-run coverage when its write succeeds. For complete nested data, read the Dataset items as JSON through the Apify API. Standard Dataset export formats are documented by Apify.
Coverage and troubleshooting
RUN_SUMMARY.queryCoverage records the result of each bounded acquisition:
| Status | Meaning |
|---|---|
COMPLETE | The requested bounded acquisition completed |
NO_RESULTS_CONFIRMED | A clean bounded acquisition returned no source results |
PARTIAL | Limits or acquisition gaps left the requested work incomplete; there may be zero saved posts |
BLOCKED | Access was blocked, challenged or rate-limited |
FAILED | Acquisition failed; inspect stopReason |
BUDGET_STOPPED | Further work could not be admitted within a budget |
UNSUPPORTED | A requested capability, such as native recency ordering, is unavailable |
COMPLETE never means every matching Facebook post was found. NO_RESULTS_CONFIRMED applies to the attempted public source window, not all of Facebook. A completed search can also save no posts because matching, filters or previous-result exclusions removed them.
Unresolved redirects, missing/ambiguous target documents and parser truncation create explicit coverage gaps. If no post is delivered and no query completes, the Actor fails after saving diagnostics. Missing optional media, comments or dates do not themselves mean the search failed.
If a run returns fewer posts than expected:
- Read query
status,stopReasonandcoverageGapReasonsinRUN_SUMMARY. - Check strict matching, date policy, filters and previously seen-post counts.
- Check scan, document, page, request and byte bounds before increasing only the result limit.
Transport retries preserve the query terms. The standard discovery route allows one 180-second attempt and one recovery of up to 60 seconds per page, within the shared deadline and downstream reserve. After an unusable result layout or a page without post targets, that remaining recovery requests a simpler Google layout; post-target recovery also narrows the search to post URL families. This adds no third attempt. Response times vary; an unchanged input is not a guarantee that external services will respond on every run. Keep optimizationComparison: false for monetized runs; true requires unbilled development and fails before acquisition under PPE.
Limits and advanced controls
| Input | API default | Bound / purpose |
|---|---|---|
maxPostsPerQuery | 20 | 1–500 saved posts per search |
maxUniquePosts | 500 | 1–20,000 saved unique posts in total |
maxScannedPostsPerQuery | Same as posts per search | 1–500 inspected raw posts per search |
maxDocumentsPerQuery | 24 | 1–50 Facebook document fetches per search |
maxSerpPagesPerQuery | 5 | 1–10 index pages per search |
maxConcurrency | 3 | 1–10 parallel searches; Form prefill is 1 |
minDelayMs, maxDelayMs | 0 | 0–10,000 ms between post-document request starts; maximum defaults to minimum |
maxTotalRequests | 400 | 1–400 shared admitted HTTP attempts, including failures |
maxTotalResponseMiB | 256 | 1–256 MiB of decoded responses; at most one bounded final-response overshoot |
proxyStrategy | AUTO | Facebook documents use datacenter; residential requires explicit APIFY_RESIDENTIAL |
Pacing spaces post-document requests across parallel searches; it does not shorten Google retry windows. Result caps do not remove work bounds, and 0 does not mean unlimited.
JSON/API-only diagnostic options remain available: discoveryQueryVariant defaults to BROAD_V1; discoveryTransportVariant defaults to SERP_RICH_CONTROL. Basic rendering and datacenter discovery variants are experimental, offer no reliability guarantee and do not expand Facebook coverage. They are unnecessary for normal runs. proxyStrategy affects the document leg, not Google's SERP route.
API and integrations
Use Actor ID overbifrost/facebook-post-search-scraper with an Apify token. A Facebook token is not needed.
import { ApifyClient } from "apify-client";const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor("overbifrost/facebook-post-search-scraper").call({queries: ["James Webb Olhar Digital"],maxPostsPerQuery: 3,maxUniquePosts: 3,includeTopComments: true,writeCsv: true});const { items } = await client.dataset(run.defaultDatasetId).listItems();console.log(items);
import osfrom apify_client import ApifyClientclient = ApifyClient(os.environ["APIFY_TOKEN"])run = client.actor("overbifrost/facebook-post-search-scraper").call(run_input={"queries": ["James Webb Olhar Digital"],"maxPostsPerQuery": 3,"maxUniquePosts": 3,"includeTopComments": True,"writeCsv": True,})items = client.dataset(run["defaultDatasetId"]).list_items().itemsprint(items)
These examples read one Dataset page; use client pagination for larger exports. The Apify API also supports asynchronous runs and Dataset reads. Schedules, webhooks and HTTP integrations can reuse the same input. Keep the same monitoring profile for recurring runs with onlyNewPosts. Pin a tested build when you need to control when updates reach a workflow.
Pricing
The Actor uses the post-delivered event for unique persisted posts. Duplicate query hits, rejected candidates and auxiliary comment/summary rows do not add post events. Previously saved monitoring matches are skipped before another post event.
Posts are saved before a charge attempt. A failed save does not create a post charge; an uncertain charge stops further billable delivery. The charge journal audits event decisions, not publisher revenue. Current rates, plan discounts and any Actor Start event are listed on the Actor's Pricing page; that page is authoritative.
Source boundaries
- Public logged-out content only. Private/restricted posts, customer sessions and full comment threads are unsupported.
- Keyword/hashtag discovery is index-bounded. Unindexed or newly published posts may be absent; this is not a full Facebook archive.
TOPuses index relevance.RECENTis retained only for API compatibility and reportsUNSUPPORTED; native newest-first search is unavailable.- Native location search is unavailable. Explicit observed tagged-place filtering is a separate local check.
- Observed fields and external source availability can change. Missing fields remain unknown, and incomplete acquisition is reported.
This Actor is not affiliated with Meta or Facebook. For a reproducible issue report, include the run ID, build, input and RUN_SUMMARY; remove tokens and signed private links before sharing publicly.
Troubleshooting a failed keyword search
RUN_SUMMARY distinguishes failed acquisition from a completed search with
no new posts. For a bounded diagnostic run, add
"saveDiscoveryDiagnostics": trueDISCOVERY_DEBUG.json in the run's key-value store. The summary reports
its status. This adds no search request or post-delivery event.
The file contains up to four redacted Google response excerpts (512 KiB total), request context and safe fingerprints. Scripts, form values and opaque or tracking link parameters are removed; public result text and your search query can remain. Keep the file private. It is not raw HTML, a complete response or post data. Disable the flag after troubleshooting.