# Facebook Search Scraper: Keyword Posts, No Login (`automation_craft/facebook-search-scraper`) Actor

Facebook search scraper for public posts by keyword, phrase or #hashtag, no login: top or most recent, date window, tagged place, post type and source filters, deep month by month search. Rows: text, author, reactions by type, comments, shares, video views and media. JSON, CSV, API; pay per post.

- **URL**: https://apify.com/automation_craft/facebook-search-scraper.md
- **Developed by:** [Automation Craft](https://apify.com/automation_craft) (community)
- **Categories:** Social media, Marketing, News
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.90 / 1,000 posts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### Facebook Search Scraper: Keyword Posts, No Login

**Facebook search scraper** for public posts, with no login, no cookies and no Facebook account: give keywords, phrases, #hashtags or a pasted Facebook post search link and get the posts Facebook's own search finds for each of them, in its Top order or most recent first, inside a date window and at a tagged place if you like. Every row carries the post's text, author, time, reactions by type, comment and share counts, video views, photos and videos with their files, link previews, shared posts, the query that found it, its rank, and whether the text really holds your words. Pay per post, from $1.90 per 1,000.

Built for social listening and brand mentions, news and topic research, campaign and hashtag tracking, lead finding, academic studies and AI data sets, and for scheduled runs in n8n, Make or Zapier. Rows carry what Facebook shows a visitor who is not logged in; use them under the rules that apply to you.

#### At a glance

- **Search by words**: up to 100 queries a run (keywords, phrases, #hashtags, or `facebook.com/search/posts/?q=...` links, whose most recent, date and location filters are read too), each searched on its own and named on every row.
- **Facebook's two orders**: Top posts (Facebook's relevance ranking) or Most recent first.
- **Date window and place**: posts on or after and on or before a day (UTC, or "7 days", "2 months"), sent as Facebook's own filter and checked again on every post; posts tagged at a place, given by Facebook location id or by its full name ("New York, New York").
- **Deep search** (on by default): when a query's results end before your post number, the search goes on month by month backwards, each month a search of its own, so you get more posts than the plain search gives.
- **Filters**: post type (text, photo, album, video, reel, link, share, event, listing), who posted it (a page, a business page, a person, a group post), minimum reactions, comments or shares, only posts that contain the query, words to keep and words to leave out. Posts a filter leaves out are free.
- **Each post billed once per run**, however many of your queries find it. Every search gets a free coverage row that says what was read and why it ended.
- **Personal data off by default**: a person author's id, profile link and picture, the ids and links of tagged people and the `phones` and `emails` lists stay empty unless you turn them on. Names and texts are delivered as Facebook shows them, and each post's own link too, except a person's post link that holds the person's profile name or id.

### Quick start

1. Open the Actor and type your queries into **Search queries**, one per line (the form starts with `climate change`).
2. Set **Posts per query** (the form starts at 10; 20 when the field is left out; up to 5,000) and pick an **Order**. Add a date window, a tagged location or filters if you like.
3. Run, then download the rows from the Output tab (view **Posts**) as JSON, CSV or Excel, or read them through the API.
4. For a daily feed of new mentions: set **Order** to Most recent first and **Posted on or after** to `1 day`, and schedule the run with Apify's scheduler.

### What you get

One `post` row per post (`type: "post"`). The fill rates count the 3,043 post rows (2,024 distinct posts, on 53 queries such as climate change, nike, pizza, coffee, yoga and tesla, in Top and Most recent order, with and without windows, places and filters) of the 58 runs of the platform test matrix of 2026-10-23 (two passes, build 0.1.1); 12 rows of the compact form and 20 rows with personal data on are included.

| Field | What it holds | Filled |
|---|---|---|
| `postId`, `url`, `createdAt`, `createdAtUnix` | The post's id, its own link as Facebook serves it, and its creation time (UTC). With personal data off a person's post link is empty when it holds the person's profile name or id (`facebook.com/<name>/posts/...`); group, photo, video and reel links stay | `postId`, `createdAt` 100%; `url` 100% of page posts (`createdAtUnix` 99.6%: not in compact rows) |
| `query`, `searchRank`, `searchPage`, `sortBy` | The query that found the post, its place in Facebook's results (1 = first), the results page it came on, the order | 100% (`searchPage`, `sortBy` 99.6%) |
| `searchWindow`, `searchLocation` | The date window and the location id the search was sent with | on every row of a search that sets them |
| `matchesQuery`, `matchedWords` | Whether the text (or shared text, link, listing or event title) holds every word of the query as a whole word, and which words it holds | 100%; true on 2,537 rows (83.4%), at least one word on 88.2% |
| `text` | The post's text in full, exactly as Facebook serves it | 99.3% (2,963 of 2,984 posts that are not shares) |
| `title` | The post's header line when it has one ("X is at Y", "X is with Y") | 9.5% |
| `postType` | `text`, `photo`, `album`, `video`, `reel`, `link`, `share`, `event`, `listing` or `unavailable_attachment` | 100% |
| `authorName`, `authorType` | The author's name and kind (`page` or `person`) | 100% (2,438 pages, 605 persons) |
| `authorId`, `authorUrl`, `authorProfilePictureUrl` | The author's id, link and picture: a page's always, a person's with personal data on | 2,433 of 2,433 page posts in full rows; empty for persons with personal data off |
| `authorPageId`, `authorIsBusinessPage` | A page's classic page id (the one the Ad Library and the Graph API know) and whether its business page is active | 2,433 of 2,438 page posts (not in compact rows) |
| `groupId`, `groupName`, `groupUrl`, `isGroupPost` | The public group a post was made in | 537 group posts (17.6%), every one named |
| `reactionCount`, `reactions` | All reactions, and each type (`like`, `love`, `care`, `haha`, `wow`, `sad`, `angry`) | 100%; the breakdown on 2,816 of 2,827 posts with a reaction, adding up to the total on all 2,816 |
| `commentCount`, `shareCount`, `engagementCount` | Comments and shares as Facebook counts them, and the three counts added | 100% (`engagementCount` 99.6%) |
| `videoViewCount`, `videoDurationSeconds` | On videos and reels | 347 of 347 video and reel posts (duration on 346) |
| `media`, `mediaCount`, `mediaListed`, `additionalMediaCount`, `albumComplete` | Photos and videos: `type`, `id`, `imageUrl`, `videoUrl` (HD) and `videoUrlSd`, `width`, `height`, `durationSeconds`, `altText` (Facebook's own picture description), `captionsUrl`; how many items the post holds and how many the search lists | 2,740 of 2,740 photo, album, video and reel posts; `imageUrl` and size on 6,123 of 6,123 photos, `altText` on 6,100; `videoUrl` on 346 of 358 videos; 503 of 1,132 albums hold more items than the search lists (the **Complete albums** add-on reads the rest from the post's photo set page) |
| `link` | A link preview: `url`, `title`, `description`, `domain`, `imageUrl` | 149 of 149 link posts |
| `sharedPost` | A share's original post: `postId`, `postUrl`, `authorName`, `authorType`, `text`, `createdAt` | 58 of 59 shares |
| `listing` | A sale post's listing: `listingId`, `title`, `priceText`, `price`, `currency`, `isSold`, `isOnMarketplace`, `locationText`, `marketplaceUrl` | 7 of 7 listing posts |
| `event` | An event post's event: `name`, `when`, `place`, `url`, `canceled` | where present (none among the posts measured) |
| `hashtags`, `mentions`, `taggedPlace`, `taggedProfiles` | Hashtags in the text; pages and people the text links to; the place and the people or pages the header tags (a person's id and link with personal data on) | hashtags 40.7%, mentions 3.7%, tagged place 5.7%, tagged profiles 2.7% (where the post has them) |
| `audience`, `isAiLabelled` | The audience Facebook shows ("Public"), and whether Facebook marks the post as made or edited with AI | 99.6%; AI label on 210 posts |
| `phones`, `emails` | Phone numbers and e-mail addresses written in the post, as lists (personal data on) | empty with personal data off |
| `storyId`, `feedbackId`, `attachmentKinds`, `subjectKey`, `searchKey`, `albumExpanded`, `scrapedAt` | Facebook's own story and feedback ids (a person author's story id with personal data on only), the attachment styles, the search the row belongs to as given and as one search (a place's name and id are one search), whether the Complete albums add-on read the post's photo set, the time of the run | page posts 100% |

**Compact rows** (**Row form** set to Compact, for AI agents and sheets) carry one level only: `type`, `postId`, `url`, `query`, `searchRank`, `matchesQuery`, `createdAt`, `text`, `postType`, `authorName`, `authorType`, `authorUrl`, `groupName`, `reactionCount`, `commentCount`, `shareCount`, `videoViewCount`, `imageUrl`, `videoUrl`, `linkUrl`, `sharedPostUrl`, `taggedPlace`, `searchLocationId`, `searchLocationInput`, `subjectKey`, `searchKey`, `scrapedAt`.

#### Comment rows (add-on)

With **Comments per post** above 0, one `comment` row per comment of each delivered post (`type: "comment"`, the Comment event), oldest first, up to 1,000 a post, replies too with **Include replies**: `commentId`, `postId`, `postUrl`, `query`, `parentCommentId`, `threadCommentId`, `depth`, `text`, `language`, `createdAt`, `authorName`, `authorIsPostAuthor`, `authorBadges`, `reactionCount`, `reactions`, `replyCount`, `attachments`, `isEdited`, `mentions`, `commentUrl` (a person commenter's `authorId` and `authorUrl` with personal data on).

#### Coverage, status and summary rows (free)

- **Coverage** (`type: "coverage"`), one per search: `query`, `sortBy`, `window`, `location`, `pagesRead`, `monthWindowsWalked`, `postsRead`, `postsDelivered`, `postsCharged`, `repeated`, `deliveredByAnotherSearch`, `beyondCap`, `filtered` (by window, type, source, counts, query and keywords), `reachedEndOfResults`, `newestPostAt`, `oldestPostAt`, `stopReason` (`max_posts_per_query`, `end_of_results`, `max_pages_per_query`, `max_posts`, `charge_limit`, `location_not_found` and others), `complete`, and `resumeEntry` (this search's part of the run's resume token, below; `resumed` and `deliveredInEarlierRun` on a search resumed with a token).
- **Status** (`type: "status"`): `invalid` (an input that cannot be used as given: nothing is fetched, only the start fee is charged), `values_skipped` (query values that cannot be used, beside usable ones), `location_not_found` (a place name that names no single place, with Facebook's suggestions; its searches are not run), `lookup_failed`, `skipped` (searches not started because the run stopped), `place_changed` (a place name of a resume token that now names another place: that search is not read), `addon_unavailable`, `comments_incomplete`.
- **Summary** (`type: "summary"`), the last row: every counter, the requests made, the billing the platform confirmed, and `resumeToken` when a search stopped before Facebook's end.

#### Example post row

The first result of the prefill search (`climate change`, Top order) on 2026-10-23, a page's post; the two image links are shortened here:

```json
{
  "type": "post",
  "postId": "1509521447706382",
  "url": "https://www.facebook.com/NBCNews/posts/pfbid02Y3Cjk5s3LLiu8Q6LU4iJCbkP6aNf9VQAfWXZahSoyE5KwAczgjMqkg8Ac7KQmcXul",
  "query": "climate change",
  "sortBy": "top",
  "searchLocation": null,
  "searchRank": 1,
  "searchPage": 1,
  "searchWindow": null,
  "matchesQuery": true,
  "matchedWords": ["climate", "change"],
  "createdAt": "2026-10-22T17:00:05.000Z",
  "createdAtUnix": 1792688405,
  "text": "Losing more ground in the fight against climate change, humanity is heading for a hotter world than experts projected last year, and the U.S. is a big reason for that, a new U.N. report says.",
  "title": null,
  "hashtags": [],
  "mentions": [],
  "postType": "link",
  "authorName": "NBC News",
  "authorId": "100059456233501",
  "authorUrl": "https://www.facebook.com/NBCNews",
  "authorType": "page",
  "authorPageId": "155869377766434",
  "authorIsBusinessPage": false,
  "authorProfilePictureUrl": "https://scontent-lax3-2.xx.fbcdn.net/v/t39.30808-1/402884174_736334248358443_4969975308185333169_n.jpg?...",
  "groupId": null,
  "groupName": null,
  "groupUrl": null,
  "isGroupPost": false,
  "taggedPlace": null,
  "taggedProfiles": [],
  "audience": "Public",
  "isAiLabelled": false,
  "reactionCount": 9100,
  "reactions": { "haha": 6778, "sad": 1000, "like": 841, "angry": 410, "wow": 38, "care": 24, "love": 9 },
  "commentCount": 3012,
  "shareCount": 466,
  "engagementCount": 12578,
  "videoViewCount": null,
  "videoDurationSeconds": null,
  "media": [],
  "mediaCount": 0,
  "mediaListed": 0,
  "additionalMediaCount": 0,
  "albumComplete": true,
  "albumExpanded": false,
  "link": {
    "url": "https://www.nbcnews.com/news/us-news/us-big-reason-world-losing-ground-fight-climate-change-un-report-says-rcna604944?cid=sm_npd_nn_fb_ma&utm_campaign=trueanthem&utm_medium=social&utm_source=facebook",
    "title": "U.S. is a big reason the world is losing ground in the fight against climate change, U.N. report says",
    "description": null,
    "domain": "nbcnews.com",
    "imageUrl": "https://external-lax3-2.xx.fbcdn.net/emg1/v/t13/11557256023761094427?..."
  },
  "event": null,
  "sharedPost": null,
  "listing": null,
  "phones": null,
  "emails": null,
  "attachmentKinds": ["Share"],
  "storyId": "UzpfSTEwMDA1OTQ1NjIzMzUwMToxNTA5NTIxNDQ3NzA2MzgyOjE1MDk1MjE0NDc3MDYzODI=",
  "feedbackId": "ZmVlZGJhY2s6MTUwOTUyMTQ0NzcwNjM4Mg==",
  "subjectKey": "s:cd2940051ce5db57",
  "scrapedAt": "2026-10-23T08:39:10.171Z"
}
```

#### How fast and how deep

Measured on Apify runs of 2026-10-23 (256 MB, the Apify datacenter pool): the prefill (10 posts) in 7 seconds; 1,000 posts of one query (nike) in 475 and 530 seconds, each delivered once; 150 posts of pizza recipe, whose plain search ends at about 74, in 74 and 109 seconds with Deep search walking two months back; 3 queries of 10 posts each in 6 and 18 seconds with 3 searches at once; 50 queries of one post each in 146 and 175 seconds. Facebook answers about 5 posts a results page (4.7 to 5.5 on the final build's walks). Where a plain search ends depends on the query and on Facebook: climate change ended at 91 to 93 posts on four walks, nike at 184 to 320.

### How much does it cost to scrape Facebook search?

Pay per event: you pay for the posts delivered (and the add-ons you turn on), never for a repeat, a post a filter leaves out, a failed call, a place name that names no place or an input that cannot be used.

| Event | Free | Bronze | Silver | Gold |
|---|---|---|---|---|
| Post (`post`), per 1,000 posts | $2.66 | $2.43 | $2.20 | $1.90 |
| Comment (`comment`), per 1,000 comments, add-on | $0.27 | $0.24 | $0.22 | $0.19 |
| Complete album (`complete_album`), per 1,000 albums, add-on | $0.42 | $0.38 | $0.35 | $0.30 |
| Actor start (`apify-actor-start`), once per run | $0.0065 | $0.0065 | $0.0065 | $0.0065 |

Platinum and Diamond plans pay the Gold price. There is no discount on the Actor start event. The Store pricing card shows these same prices per 1,000 events: "$2.66 / 1,000" on the Post row means one post costs about a quarter of a cent on the Free plan ($0.00266), and "$1.90 / 1,000" means one post costs about a fifth of a cent on the Gold plan ($0.0019).

- **Post** is charged once per post per run: a post that two of your queries find is delivered and charged once. Posts left out by the date window, the post type, the source, the minimum counts, the query match or your words are not charged and get no row.
- **Comment** is charged per comment or reply row, only with **Comments per post** above 0.
- **Complete album** is charged only with **Complete albums** on, and only when the post's photo set added photos or videos the search result did not list.

Worked examples, start fee included:

| Run | Free | Bronze | Silver | Gold |
|---|---|---|---|---|
| The prefill: 10 posts of one query | $0.0331 | $0.0308 | $0.0285 | $0.0255 |
| 1,000 posts | $2.6665 | $2.4365 | $2.2065 | $1.9065 |
| A daily scheduled run of 20 queries x 20 posts (400 posts a run) | $1.0705 a day | $0.9785 a day | $0.8865 a day | $0.7665 a day |
| The same daily run for 30 days | $32.115 | $29.355 | $26.595 | $22.995 |
| 100 posts with 20 comments each (2,000 comment rows) | $0.8125 | $0.7295 | $0.6665 | $0.5765 |

Every run reads its searches fresh: a post a scheduled run delivered yesterday is delivered and charged again today when the search finds it again. Narrow a daily run with **Posted on or after** (for example `1 day`) and Most recent first to keep repeats down, and dedupe on `postId` in your own sheet or workflow.

#### The work allowance

Work that earns nothing (searches whose posts every filter leaves out, refused calls, long stretches of repeats) is bounded by an allowance in real cost that grows with every event the run bills. When it is spent the run stops: the search it was reading ends with `work_allowance` in its coverage row, the searches not started get one free `skipped` row, and every row already delivered is kept. The **Complete albums** add-on reads from a small allowance of its own, so album reads that fail never stop the search they serve; when that allowance has no room the add-on stops for the rest of the run with a free `addon_unavailable` row while the posts keep coming.

### Input

| Field | What it does | Default |
|---|---|---|
| `searchQueries` | Up to 100 queries, one per line: a keyword or phrase, a #hashtag, or a Facebook post search link (`facebook.com/search/posts/?q=coffee`; its most recent, date and location filters are read too). A query in double quotes must stand as that exact phrase for the query match. A value written as a link that is not a Facebook post search link (another site, `fb.watch/...`, `mailto:`, `tel:`) is skipped free and named in a `values_skipped` row, never searched as words. Also read: `query`, `queries`, `searchTerms`. | none (the form starts with `climate change`) |
| `sortBy` | `top` (Facebook's relevance ranking) or `recent` (newest first). Also read: `searchType` (`latest` and `newest` read as `recent`). | `top`, or the order a pasted link names |
| `maxPostsPerQuery` | The most posts delivered per query (and per location), 1 to 5,000. Also read: `resultsCount`, `maxResults`, `resultsLimit`. | 20 (the form starts at 10) |
| `maxPosts` | A cap of post rows for the whole run, across queries. Also read: `maxItems`. | no cap |
| `startDate`, `endDate` | The date window, UTC: `YYYY-MM-DD`, `today`, `yesterday`, or a time back such as `7 days`, `3 weeks`, `2 months`, `1 year`. Sent as Facebook's date filter and checked on every post. Also read: `postedAfter`, `dateFrom`, `postedBefore`, `dateTo`. | no window (the end is today) |
| `locations` | Up to 20 places, one per line: a Facebook location id (`108424279189115` is New York, New York) or a place's full name as Facebook writes it (`London, United Kingdom`). Each query is searched once per location, at most 300 searches a run. Also read: `location`, `locationUid`, `locationId`. | none |
| `deepSearch` | When a query's results end before its post number, go on month by month backwards (back to the start date, or 24 months), each month a new search; a deep walk ends after 3 months in a row that bring no new post. | on |
| `postTypes` | Only these kinds: `text`, `photo`, `album`, `video`, `reel`, `link`, `share`, `event`, `listing`, `unavailable_attachment`. | every kind |
| `postSources` | Only posts by `page`, `business_page`, `person` or `group` (a post made in a public group). | every source |
| `minReactions`, `minComments`, `minShares` | Only posts with at least this many. | none |
| `mustContainQuery` | Keep only posts whose text holds every word of the query as a whole word (case and accents aside). | off |
| `includeKeywords`, `excludeKeywords` | Up to 50 words or phrases to keep (any of them) and to leave out (any of them), as whole words, case aside. Also read: `keywordFilter`. | none |
| `maxPagesPerQuery` | The most result pages read per search (about 5 posts a page), 1 to 1,000. | no page limit |
| `maxCommentsPerPost`, `includeReplies` | Comments of each delivered post, oldest first, up to this many (0 to 1,000), each as its own row (Comment event); replies too. | 0 (off), off |
| `includeAllPhotos` | The photos and videos of a post that its search result does not list, read from the post's photo set page (Complete album event; `albumComplete` says whether every item was read). Needs full rows. Also read: `scrapePhotos`. | off |
| `outputFormat` | `full` or `compact` (one level rows). Also read: `compact` (true or false). | `full` |
| `includePersonalData` | The personal data switch (below). | off |
| `maxConcurrency` | How many searches are read at the same time, 1 to 5, each on its own exit. Prices do not change. | 1 |
| `requestDelayMs` | The pause between two calls of one search, 300 to 10,000 ms. | 400 |
| `maxRunSeconds` | Stop starting new work after this many seconds (60 to 86,400) and end with the coverage rows and the summary. | the run's own timeout |
| `proxyConfiguration` | Apify Proxy: the datacenter pool (default), the `RESIDENTIAL` group with a two letter country, your own proxies (`http(s)://user:password@host:port` links, each new exit takes the next one; never shown in logs or rows), or no proxy (the run's own address). | Apify datacenter pool |
| `resumeToken` | The `resumeToken` of an earlier run's summary row, to go on with its searches where they stopped (below). | none |

#### Resume tokens

When a search of a run stops before Facebook's end (its post number, its page limit, the charge limit, the run deadline, a refusal), the run's summary row carries a `resumeToken`. Run the same searches again, given the same way (the same words, order, date window, and the same place ids or place names), with that token in `resumeToken`: each search starts where it stopped, a search that run read to its end is not read again, and each search skips, free, the posts IT delivered in that run. Posts it read but did not deliver (past its post number or the charge limit) are delivered now. A post another search of the earlier run delivered can come again, as in any two separate runs, and a search the input adds beside the token starts fresh. The token is yours to keep: nothing is stored on Apify between runs, and the token of the resumed run carries the posts of both runs, search by search. A token with a search that is not in the input refuses the run with one free row; a relative window (`7 days`) names other dates on another day, so resume with the dates of the first run. With personal data off, a token holds no person's id.

```json
{ "searchQueries": ["climate change"], "maxPostsPerQuery": 100, "resumeToken": "fbr1.eyJ2..." }
```

A value sent as `null` or `""` counts as left out. Two keys of one meaning sent together, a setting name the Actor does not know, or a value it cannot use as given refuse the input with one free `invalid` row (only the start fee is charged).

Examples:

```json
{ "searchQueries": ["climate change", "#climateaction"], "maxPostsPerQuery": 50 }
```

```json
{
  "searchQueries": ["electric scooter"],
  "sortBy": "recent",
  "startDate": "7 days",
  "postSources": ["page", "group"],
  "minReactions": 10,
  "mustContainQuery": true
}
```

```json
{
  "searchQueries": ["pizza"],
  "locations": ["New York, New York", "106078429431815"],
  "maxPostsPerQuery": 100,
  "excludeKeywords": ["delivery"],
  "maxCommentsPerPost": 20
}
```

### Personal data

Off by default. Rows then carry the names of post authors and commenters as Facebook shows them (they are part of the content) and each post's and comment's own link, and leave empty the fields that name or reach a private person: a person author's id, profile link and picture (`authorId`, `authorUrl`, `authorProfilePictureUrl`), a person author's `storyId`, a person's post link (and its photo, video and comment links, and a shared person's post link) when it holds the person's profile name or id, the ids and links of people tagged or mentioned and of commenters, a listing's seller, the `phones` and `emails` lists, and a link field that points to a person's profile or is a contact link. A person in Facebook's professional mode is a person here (only an author Facebook flags as a Page is a page), and a post whose author Facebook does not name has its links judged as a person's. Texts are never masked: what someone wrote in a post or comment is delivered exactly as Facebook serves it. A page's id and link are not personal data and are always delivered. With **Include personal data** (`includePersonalData: true`) those fields are filled; you are responsible for a lawful use of personal data (GDPR included).

### What this Actor does NOT do

- No login, no cookies and no Facebook account: it reads what Facebook's search shows a visitor who is not logged in.
- No private or friends only posts, and no posts of private groups: only public posts are reachable logged out.
- No other search tabs: people, pages, groups, videos, places and Marketplace results are not read, only the posts tab. Video search is a separate listing ([Facebook Video Search Scraper](https://apify.com/automation_craft/facebook-video-search-scraper)), and hashtag feeds a separate listing we plan.
- Facebook decides the ranking and how many posts a query has: results end where Facebook's search ends (Deep search goes past that end with month windows, and still ends where those months end).
- No monitor or "new posts only" mode across runs (owner decision); every run reads its searches fresh; schedule runs with Apify's scheduler and dedupe on `postId` in your own sheet or workflow. Inside one run each post is delivered once. An input with `memoryName`, `monitorStoreName`, `resetMemory` or `onlyNewPosts` is refused with a free row that says so.
- No Google results mode: the Actor reads Facebook's own search, never a search engine's results.
- Facebook's search is fuzzy: it also answers posts that do not hold every word of your query. Each row says whether it does (`matchesQuery`), and **Only posts that contain the query** keeps only those.
- A date window is sent as Facebook's filter, which is soft: posts outside the window that Facebook still answers are left out in code and not charged, so a narrow window can end with fewer posts than asked for.
- An album lists the items the search result shows; `mediaCount` says how many it holds, and **Complete albums** reads the rest from the post's photo set page (one page of the set; `albumComplete` says whether every item was read).
- No speech is turned into text and no text is read from images (Facebook's own alt text and caption links are delivered).
- Counts (reactions, comments, shares, views) are the ones Facebook shows at the moment of the run.
- No language model filter: the Actor makes no call to an outside model; filter by your own words.

### API examples

Run and get the rows (replace `<APIFY_TOKEN>`):

```bash
curl -X POST "https://api.apify.com/v2/acts/automation_craft~facebook-search-scraper/run-sync-get-dataset-items?token=<APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"searchQueries":["climate change"],"maxPostsPerQuery":20}'
```

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<APIFY_TOKEN>' });
const run = await client.actor('automation_craft/facebook-search-scraper').call({ searchQueries: ['#climateaction'], sortBy: 'recent', startDate: '7 days', maxPostsPerQuery: 50 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.filter((r) => r.type === 'post'));
```

```python
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("automation_craft/facebook-search-scraper").call(run_input={"searchQueries": ["pizza"], "locations": ["New York, New York"], "maxPostsPerQuery": 30})
posts = [r for r in client.dataset(run["defaultDatasetId"]).iterate_items() if r["type"] == "post"]
```

### FAQ

#### Can I search Facebook posts by keyword without logging in?

Yes. This Actor runs Facebook's own post search as a visitor who is not logged in: no account, no cookies and no login. It returns the public posts Facebook's search shows for your words; posts shared only with friends or a group's members are not reachable this way.

#### Can I search Facebook for a hashtag?

Yes. Put the hashtag with its # in **Search queries** (for example `#climateaction`), one query per line. Each row lists the hashtags in its text (`hashtags`), and **Only posts that contain the query** keeps only the posts whose text holds it as a whole word.

#### How do I get Facebook posts from a date range, or only the most recent ones?

Set **Posted on or after** and **Posted on or before** (`YYYY-MM-DD`, or a time back such as `7 days`); the window is sent as Facebook's own date filter and checked again on every post, so posts outside it are left out and not charged. For the newest posts first, set **Order** to Most recent first.

#### Why does a Facebook search stop after a few hundred posts?

Facebook ends each search where its own results end, and it decides the ranking: in our tests climate change ended at 91 to 93 posts and pizza recipe at 74. With **Deep search** on (the default) the Actor then searches month by month backwards with Facebook's date filter, which reaches posts the plain search did not show; pizza recipe gave 150 posts that way.

#### How do I export Facebook search results to CSV or Excel?

Run the Actor, open the Output tab and download the **Posts** view as CSV or Excel: one row per post with the query, rank, time, author, text and counts as columns. The same rows are available as JSON through the API, and **Row form** set to Compact gives one level rows for sheets.

#### Why does this Actor run with limited permissions?

It runs with the least privilege Apify offers: it reads and writes only its own run's dataset and key-value store. It cannot see your other Actors, datasets or account data.

### More data tools by Automation Craft

- [Facebook Posts Scraper - Page Posts, No Login](https://apify.com/automation_craft/facebook-posts-scraper): the posts of public pages you name
- [Facebook Post Scraper: Reactions and Shares by URL](https://apify.com/automation_craft/facebook-post-scraper): posts from links you paste
- [Facebook Groups Scraper: Posts of Public Groups](https://apify.com/automation_craft/facebook-groups-scraper): the feed of public groups
- [Facebook Comments Scraper: Replies, No Login](https://apify.com/automation_craft/facebook-comments-scraper): every comment of public posts
- [Facebook Page ID Finder: Names to Page IDs](https://apify.com/automation_craft/facebook-page-id-finder)
- [Facebook Ads Library Scraper - Ad Library & Spy](https://apify.com/automation_craft/facebook-ads-library-scraper)
- [Facebook Marketplace Scraper with Sold Tracker](https://apify.com/automation_craft/facebook-marketplace-scraper)
- [Threads Search Scraper: Keywords, Hashtags, Dates](https://apify.com/automation_craft/threads-search-scraper)
- [Instagram Posts Scraper: Profiles, Reels, No Login](https://apify.com/automation_craft/instagram-posts-scraper)

### Changelog

- 0.1 (2026-10-24): a search that reached Facebook's end with a post passed over in a race between two searches is no longer marked complete (a restarted run or a resume token reads it again); mentions and tags after an emoji keep their whole name; hashtags and the query match keep whole words in scripts with vowel signs (Hindi, Bengali, Thai, Tamil); a post stored twice by a retry counts once toward its search's number after a restart; inputs written with `query`, `queries` or `searchTerms` are no longer refused by the form; photo set pages whose parts arrive out of order keep each item's picture, and the Complete albums texts say the add-on reads the post's photo set page; the resume token and personal data field texts say exactly what is skipped and what is emptied; a resume token keeps the dates and months of the run that wrote it when it is used on a later day.
- 0.1 (2026-10-23): first build: public Facebook posts by keyword, phrase, #hashtag or pasted post search link, logged out; Top or Most recent order, a date window, tagged locations by id or name, post type, source, count and word filters, deep search by month windows, comments and complete albums as add-ons, personal data off by default.

# Changelog

This Actor's version history is a separate document: https://apify.com/automation_craft/facebook-search-scraper/changelog.md

# Actor input Schema

## `searchQueries` (type: `array`):

What to search Facebook for, one query per line (up to 100): a keyword or phrase (climate change), a #hashtag, or a Facebook post search link (facebook.com/search/posts/?q=coffee; its most recent, date and location filters are read too). Any other link (another site, a short link, mailto: or tel:) is skipped free and named in a row, never searched as words. Each query is searched on its own and every row names it. Facebook's search is fuzzy: each row says whether it holds every word (matchesQuery), and "Only posts that contain the query" keeps only those. Also read from query, queries and searchTerms.

## `sortBy` (type: `string`):

Facebook's own two orders: top (its relevance ranking, posts of any age) or recent (the newest public posts first). Left empty: top, or the order a pasted search link names. Also read from searchType (latest and newest read as recent).

## `maxPostsPerQuery` (type: `integer`):

The most new posts delivered per query (and per location). Left empty: 20. A query whose results end earlier is walked month by month when "Deep search" is on. Also read from resultsCount, maxResults and resultsLimit.

## `maxPosts` (type: `integer`):

The most posts delivered in the whole run, across queries. Left empty: no cap beyond the per query number and your charge limit. Also read from maxItems.

## `startDate` (type: `string`):

The first day of a date window (UTC): YYYY-MM-DD, today, yesterday, or a time back from the run's start such as 7 days, 3 weeks, 2 months, 1 year. Sent to Facebook as its own date filter and checked again on every post (posts outside the window are left out and not charged). Left empty: no start. Also read from postedAfter and dateFrom.

## `endDate` (type: `string`):

The last day of the date window (UTC), in the same forms as the start. Left empty: today. Also read from postedBefore and dateTo.

## `locations` (type: `array`):

Only posts tagged at a place, one per line: a Facebook location id (108424279189115 is New York, New York) or a place's full name as Facebook writes it (New York, New York; London, United Kingdom), looked up in Facebook's own place list. A name that names no single place gets a free row with Facebook's suggestions and its searches are not run. Each query is searched once per location. Up to 20. Also read from location, locationUid and locationId.

## `deepSearch` (type: `boolean`):

When a query's results end before its post number, go on month by month backwards (this month, the month before, back to the start date or 24 months), each month a new search of its own: Facebook's date filter ranks other posts first, so a month finds posts the plain search did not. Repeats are free.

## `postTypes` (type: `array`):

Only these kinds of post (any of them). Left empty: every kind. Posts of other kinds are left out and not charged. Also read from postType.

## `postSources` (type: `array`):

Only posts by these sources (any of them): a page, a business page (a page with a business account), a person, or a post made in a public group. Left empty: every source. Also read from postSource.

## `minReactions` (type: `integer`):

Only posts with at least this many reactions (posts with fewer, or with no count shown, are left out and not charged).

## `minComments` (type: `integer`):

Only posts with at least this many comments.

## `minShares` (type: `integer`):

Only posts with at least this many shares.

## `mustContainQuery` (type: `boolean`):

Keep only posts whose text (or shared post text, or link, listing or event title) holds every word of the query as a whole word, case and accents (é, ü, ñ) aside; a query in double quotes must stand as that exact phrase. Off: Facebook's own matches are delivered and each row says whether it holds the query (matchesQuery, matchedWords).

## `includeKeywords` (type: `array`):

Only posts whose text holds at least one of these words or phrases (whole words, case aside). Up to 50. Also read from keywordFilter.

## `excludeKeywords` (type: `array`):

Posts whose text holds any of these words or phrases are left out and not charged (the same matching rule). Up to 50.

## `maxPagesPerQuery` (type: `integer`):

The most result pages read per search (about 5 posts a page). Left empty: no page limit (the posts per search cap and Facebook's own end decide).

## `maxCommentsPerPost` (type: `integer`):

Read the comments of each delivered post, oldest first, up to this many, each as its own row (the Comment event). 0 or empty: no comments are read.

## `includeReplies` (type: `boolean`):

With comments on: read the replies of each comment too (they count toward the number per post).

## `includeAllPhotos` (type: `boolean`):

For a post whose search result lists only its first photos, read the post's photo set page and add the photos and videos it holds that the search result did not list (the Complete album event, charged only when the set added items; albumComplete says whether every item of the post was read). Also read from scrapePhotos.

## `outputFormat` (type: `string`):

full: every field; compact: the main fields of a post one level deep (text, author, counts, first image or video, link). Left empty: full. Also read from compact (true or false).

## `includePersonalData` (type: `boolean`):

Off (the default): the fields that name or reach a private person are empty: a person author's id, profile link, picture and story id, the ids and links of people tagged or mentioned and of commenters, a listing's seller, and the phones and emails lists read from a text; a person's post link (and its photo, video and comment links) is kept only in a form without the person's profile name or id (a group post, photo, video or reel link), else empty. The names of authors, commenters and people tagged or mentioned, and every text, are delivered as Facebook shows them, either way. On: those fields are filled; you are responsible for a lawful use of personal data (GDPR included).

## `maxConcurrency` (type: `integer`):

How many searches are read at the same time, each on its own exit. Left empty: 1 (one after another).

## `requestDelayMs` (type: `integer`):

The pause between two calls of one search, in milliseconds. Left empty: 400.

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

Stop starting new work after this many seconds and end with the coverage rows and the summary (what was delivered stays delivered and charged). Left empty: the run's own timeout.

## `proxyConfiguration` (type: `object`):

Apify Proxy, the datacenter pool by default; choose the RESIDENTIAL group with a country for exits of that country, your own proxies (http(s) links with their port; each new exit takes the next one), or no proxy for the run's own address. Also read from proxy.

## `resumeToken` (type: `string`):

Go on with the searches an earlier run stopped before their end (its post number, its page limit, the charge limit, the run deadline, a refusal): paste the resumeToken of that run's summary row here and run the same searches again, given the same way (the same words, order, date window, and the same place ids or place names). Each search starts where it stopped, searches that run read to the end are not read again, and each search skips, free, the posts it delivered in that run. A post another search of that run delivered can come again and is charged, as in any two separate runs. Left empty: every search starts at its first page.

## Actor input object example

```json
{
  "searchQueries": [
    "climate change"
  ],
  "maxPostsPerQuery": 10,
  "deepSearch": true,
  "mustContainQuery": false,
  "includeReplies": false,
  "outputFormat": "full",
  "includePersonalData": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "searchQueries": [
        "climate change"
    ],
    "maxPostsPerQuery": 10,
    "outputFormat": "full",
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation_craft/facebook-search-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "searchQueries": ["climate change"],
    "maxPostsPerQuery": 10,
    "outputFormat": "full",
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("automation_craft/facebook-search-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "searchQueries": [
    "climate change"
  ],
  "maxPostsPerQuery": 10,
  "outputFormat": "full",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call automation_craft/facebook-search-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automation_craft/facebook-search-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/5Lc05XAJMEXIGKEK3/builds/AAhnkCzbWQeUsewMP/openapi.json
