# Facebook Post Search & Keyword Scraper API (`overbifrost/facebook-post-search-scraper`) Actor

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.

- **URL**: https://apify.com/overbifrost/facebook-post-search-scraper.md
- **Developed by:** [Lasse](https://apify.com/overbifrost) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.99 / 1,000 post delivereds

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 Post Search & Keyword Scraper API

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:

```json
{
  "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 `onlyNewPosts` and 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.csv` file.
- **Review results visually.** Open `REPORT.html` for 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.

```json
{
  "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:

```json
{
  "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](https://docs.apify.com/api/v2/dataset-items-get).

### 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:

1. Read query `status`, `stopReason` and `coverageGapReasons` in `RUN_SUMMARY`.
2. Check strict matching, date policy, filters and previously seen-post counts.
3. 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.

```javascript
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);
```

```python
import os
from apify_client import ApifyClient

client = 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().items
print(items)
```

These examples read one Dataset page; use client pagination for larger exports. The [Apify API](https://docs.apify.com/api/v2) 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](https://apify.com/overbifrost/facebook-post-search-scraper/pricing); 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.
- `TOP` uses index relevance. `RECENT` is retained only for API compatibility and reports `UNSUPPORTED`; 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":
true` to the JSON input. If a query fails or is blocked, the Actor attempts
to save `DISCOVERY_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.

# Actor input Schema

## `queries` (type: `array`):

Add one keyword, hashtag, unfiltered Facebook posts-search link or individual post link per item. Search links reuse their text through Google’s index; direct post links are fetched directly.

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

Maximum posts saved per search after matching and filters. Fewer may be available. Increase the scan limit under Search depth if filtering or monitoring skips too many candidates.

## `maxUniquePosts` (type: `integer`):

Maximum unique posts saved across all inputs. A post found by several searches counts once. Increase both result limits for more posts from one search.

## `hashtags` (type: `array`):

Optional additional hashtag searches, with or without #. Together with keywords, at most 100 search inputs.

## `dateFrom` (type: `string`):

Keep observed publication dates on or after this value. Use a date, ISO timestamp or relative value such as 7 days. Relative dates resolve once at run start; checks use UTC and do not expand search coverage.

## `dateTo` (type: `string`):

Keep observed publication dates on or before this value. A date-only value includes the whole UTC day. Accepts ISO timestamps and relative values such as 1 day. Leave empty for no upper bound.

## `unknownDate` (type: `string`):

Choose what happens when a post's publication date is unavailable. Keeping it can include posts outside your date range. Used only when a date filter is set.

## `matchMode` (type: `string`):

Related results keep Google-ranked discoveries. Stricter modes require collected post text to match. All terms / Any term support quoted phrases, OR alternatives and -exclusions. Direct post links need no keyword match.

## `filters` (type: `object`):

Optional checks on observed post data. Empty selections keep all types; positive minimums require an observed count. Unknown language or tagged place does not pass those filters.

## `includeTopComments` (type: `boolean`):

Collect up to the selected number of top-level comments already exposed in the public post document. Does not fetch a complete thread or make additional comment requests.

## `maxTopCommentsPerPost` (type: `integer`):

Maximum visible top-level comments per post when enabled. Fewer or no comments may be exposed. Default 10.

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

Full keeps nested post data. Flat uses scalar spreadsheet columns and preserves nested details in JSON text cells; use the Flat dataset view. HTML results keep the same readable format.

## `writeCsv` (type: `boolean`):

Write a UTF-8 CSV file in the run storage. Safe for spreadsheet formulas, with a 32 MiB artifact bound. The summary reports any truncation; the Dataset retains the saved post rows.

## `onlyNewPosts` (type: `boolean`):

Remember saved posts for this search configuration across runs. Previously saved posts are skipped without a post charge. Uses bounded persistent history; does not discover posts missing from the search index.

## `monitorName` (type: `string`):

Keep the same profile for repeated runs. Choose another name for independent monitoring history. The default profile is default.

## `monitorRetentionDays` (type: `integer`):

History retention when monitoring is enabled. Default 90 days. History also has a fixed capacity of 51,200 search/identity hashes; expired or evicted posts can appear again.

## `previousDatasetId` (type: `string`):

Choose a previous Dataset to skip posts it already contains. That Dataset is read only. The previous run is not selected automatically.

## `excludePostIds` (type: `array`):

Skip specific posts by adding their Facebook post IDs or post links, one per item.

## `maxScannedPostsPerQuery` (type: `integer`):

Optional wider scan window when filters or monitoring discard posts. By default it equals Posts per search. Saved posts still respect the result limits. Document, request, byte and time bounds also apply.

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

Number of searches acquired in parallel, from 1 to 10. Higher values may finish multi-query runs sooner but consume the shared work budget faster. The small prefilled run uses 1; API default is 3.

## `minDelayMs` (type: `integer`):

Minimum time between starting Facebook post-document requests, shared across searches. Default 0 ms. Maximum defaults to the same value. Cached documents add no wait.

## `maxDelayMs` (type: `integer`):

Upper end of the interval between Facebook post-document request starts. Must be at least the minimum; at most 10,000 ms. Leave empty to use the minimum.

## `maxSerpPagesPerQuery` (type: `integer`):

Hard bound on Google index result pages fetched per query on the discovery leg. Default 5.

## `maxDocumentsPerQuery` (type: `integer`):

Hard bound on public logged-out Facebook document fetches per query. Default 24.

## `maxTotalRequests` (type: `integer`):

Shared run bound for admitted SERP, goto and Facebook document attempts, including failed attempts. A denied request is never sent. Platform usage costs remain separate from delivered-post billing.

## `maxTotalResponseMiB` (type: `integer`):

Shared decoded response bound. One bounded final response may cross the limit; later requests stop. This measures decoded response bodies, not billed transfer bytes or dollars.

## `proxyStrategy` (type: `string`):

JSON/API only: AUTO uses Apify datacenter for Facebook documents. Other values: NONE, APIFY_DATACENTER, APIFY_RESIDENTIAL. Residential traffic is explicit opt-in.

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

JSON/API only: TOP uses Google index relevance. RECENT is accepted for compatibility, falls back to index order and is reported UNSUPPORTED. Newest-first search is not available.

## `discoveryQueryVariant` (type: `string`):

JSON/API diagnostic setting: BROAD_V1 is the default; POST_URL_BIASED_V1 restricts individual-post URL families. Normal runs do not need to change this.

## `discoveryTransportVariant` (type: `string`):

JSON/API diagnostic setting: SERP_RICH_CONTROL is the default. SERP_BASIC_GBV1 and DATACENTER_BASIC_GBV1_WITH_SERP_FALLBACK are experimental and do not expand Facebook coverage.

## `optimizationComparison` (type: `boolean`):

Adds the comparison summary and refuses PPE billing before acquisition. Use only for an unpublished, unmonetized owner acceptance run; platform compute/proxy costs still apply.

## `saveDiscoveryDiagnostics` (type: `boolean`):

JSON/API troubleshooting only. When enabled and a query fails or is blocked, save up to four redacted Google response excerpts in DISCOVERY_DEBUG.json (512 KiB total). Removes scripts, form values, opaque links and tracking parameters. Public result text and the supplied search query may remain. No extra HTTP request or post charge. Disabled by default.

## Actor input object example

```json
{
  "queries": [
    "James Webb Olhar Digital"
  ],
  "maxPostsPerQuery": 3,
  "maxUniquePosts": 3,
  "unknownDate": "KEEP_FLAGGED",
  "matchMode": "SOURCE_RESULT",
  "includeTopComments": false,
  "writeCsv": false,
  "onlyNewPosts": false,
  "maxConcurrency": 1,
  "maxSerpPagesPerQuery": 1,
  "maxDocumentsPerQuery": 6,
  "maxTotalRequests": 48,
  "maxTotalResponseMiB": 24,
  "proxyStrategy": "AUTO",
  "sortBy": "TOP",
  "discoveryQueryVariant": "BROAD_V1",
  "discoveryTransportVariant": "SERP_RICH_CONTROL",
  "optimizationComparison": false,
  "saveDiscoveryDiagnostics": false
}
```

# Actor output Schema

## `htmlReport` (type: `string`):

Readable post cards, publication times, engagement and query coverage. Shows up to 100 saved posts; the Dataset is the full export.

## `results` (type: `string`):

One saved post per row. FULL preserves nested evidence; FLAT uses scalar columns and JSON text cells.

## `runSummary` (type: `string`):

Per-query coverage states, dedupe/filter counters, acquisition telemetry and billing state.

## `chargeJournal` (type: `string`):

Auditable per-post post-delivered charging decisions recorded after result persistence.

## `csvExport` (type: `string`):

Optional posts.csv UTF-8 artifact. See run summary for truncation or storage failure.

## `topComments` (type: `string`):

Visible top-level comments when enabled. A subset of comments, with no additional post charge.

## `searchSummary` (type: `string`):

One row per search, including failed, partial and empty searches.

# 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 = {
    "queries": [
        "James Webb Olhar Digital"
    ],
    "maxPostsPerQuery": 3,
    "maxUniquePosts": 3,
    "matchMode": "SOURCE_RESULT",
    "maxConcurrency": 1,
    "maxSerpPagesPerQuery": 1,
    "maxDocumentsPerQuery": 6,
    "maxTotalRequests": 48,
    "maxTotalResponseMiB": 24,
    "discoveryQueryVariant": "BROAD_V1",
    "discoveryTransportVariant": "SERP_RICH_CONTROL",
    "optimizationComparison": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("overbifrost/facebook-post-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 = {
    "queries": ["James Webb Olhar Digital"],
    "maxPostsPerQuery": 3,
    "maxUniquePosts": 3,
    "matchMode": "SOURCE_RESULT",
    "maxConcurrency": 1,
    "maxSerpPagesPerQuery": 1,
    "maxDocumentsPerQuery": 6,
    "maxTotalRequests": 48,
    "maxTotalResponseMiB": 24,
    "discoveryQueryVariant": "BROAD_V1",
    "discoveryTransportVariant": "SERP_RICH_CONTROL",
    "optimizationComparison": False,
}

# Run the Actor and wait for it to finish
run = client.actor("overbifrost/facebook-post-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 '{
  "queries": [
    "James Webb Olhar Digital"
  ],
  "maxPostsPerQuery": 3,
  "maxUniquePosts": 3,
  "matchMode": "SOURCE_RESULT",
  "maxConcurrency": 1,
  "maxSerpPagesPerQuery": 1,
  "maxDocumentsPerQuery": 6,
  "maxTotalRequests": 48,
  "maxTotalResponseMiB": 24,
  "discoveryQueryVariant": "BROAD_V1",
  "discoveryTransportVariant": "SERP_RICH_CONTROL",
  "optimizationComparison": false
}' |
apify call overbifrost/facebook-post-search-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,overbifrost/facebook-post-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/MgPLDM1cpjSmZAtPm/builds/mWcNF09rT2IQagryK/openapi.json
