# Facebook Posts Scraper (`coregent/facebook-posts-scraper`) Actor

Scrape public Facebook posts from pages and profiles, or discover posts by keyword. One normalized row per post: text, author, timestamp, media, reactions with a per-reaction breakdown, comment and share counts, and outbound links. No Facebook login, cookies or session ID needed.

- **URL**: https://apify.com/coregent/facebook-posts-scraper.md
- **Developed by:** [Delowar Munna](https://apify.com/coregent) (community)
- **Stats:** 5 total users, 2 monthly users, 94.3% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 post results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Facebook Posts Scraper

![Facebook Posts Scraper](https://raw.githubusercontent.com/coregentdevspace/facebook-posts-scraper-assets/main/thumbnail-facebook-posts-scraper.png)

Scrape public Facebook posts from pages and profiles, or discover posts by keyword. Get clean post
text, author, timestamps, media, reactions with a per-reaction breakdown, comment and share counts,
and outbound links. One global result limit keeps multi-source runs predictable, and **you never
need to provide Facebook login credentials, cookies or a session ID.**

***

### Quick start

1. Put one or more Facebook URLs in **Facebook URLs** — a post link, or a page/profile whose recent
   posts you want.
2. Set **Maximum posts**.
3. Leave **Proxy configuration** on Apify Proxy (Datacenter) — it keeps a multi-page run from being
   blocked. It will not give you deeper timeline history; see
   [Why depth is limited](#-why-depth-is-limited-without-a-login).
4. Run.

```json
{
  "startUrls": ["https://www.facebook.com/nasa"],
  "maxResults": 100,
  "sort": "latest"
}
```

Every run also writes a `RUN_SUMMARY` record to the key-value store with counts, stop reason and
diagnostics, and a `RETRY_INPUT` record you can re-run as-is if anything was blocked.

***

### Two input modes

#### Post URLs — exact, reliable, one request each

Paste post links directly. Every URL shape Facebook uses is accepted, including the long
search-engine form with the post text in the path:

```
https://www.facebook.com/NASA/posts/1617674169727953
https://www.facebook.com/NASA/posts/pfbid0p9U45uPC39zamcxn3YC…
https://www.facebook.com/NASA/posts/hello-moon-its-great-to-be-back/1496561611839210/
https://www.facebook.com/copperkettleyqr/videos/1399396352119095/
https://www.facebook.com/reel/1234567890123456
https://www.facebook.com/photo/?fbid=1617674156394621
https://www.facebook.com/permalink.php?story_fbid=…&id=…
```

#### Page and profile URLs — the current post from each

```
https://www.facebook.com/nasa          a page by handle
nasa                                   a bare handle works too
https://www.facebook.com/profile.php?id=100090722241114
https://www.facebook.com/people/Central-Pub/100090722241114/
```

Personal profiles work exactly like pages. **Facebook groups do not** — see
[What this Actor cannot do](#what-this-actor-cannot-do).

#### Keyword discovery

Put keywords in **Search queries** to find posts about a topic. Facebook publishes no logged-out
search of its own, so discovery searches the open web for indexed Facebook posts. Roughly half of
what a keyword returns is Facebook *pages* rather than individual posts; the run reports how many,
and you can paste those into **Facebook URLs** to pick up the current post from each.

Add **Locations** to turn one keyword into several searches (`"artemis mission"` + `"Australia"`).
That returns more than searching one keyword more deeply.

***

### Sample inputs

Three runs that cover the shapes people actually use. All three work in the Console and the API
unchanged.

**1 — Brand monitoring.** The current post from every page you track. This is what the Actor is best
at: each page contributes the post Facebook renders on it, so the result count scales with the
number of pages, not with how deep you ask.

```json
{
  "startUrls": [
    "https://www.facebook.com/nasa",
    "https://www.facebook.com/BurgerKing",
    "https://www.facebook.com/rolex",
    "https://www.facebook.com/starbucks"
  ],
  "maxResults": 200,
  "sort": "latest",
  "proxyConfiguration": { "useApifyProxy": true }
}
```

**2 — Post URLs, the volume path.** Permalinks are one request each, are never rate-limited, and
return the complete post. Use this shape when you want a large, reliable run.

```json
{
  "startUrls": [
    "https://www.facebook.com/NASA/posts/1617674169727953",
    "https://www.facebook.com/NASA/posts/hello-moon-its-great-to-be-back/1496561611839210/",
    "https://www.facebook.com/copperkettleyqr/videos/1399396352119095/",
    "https://www.facebook.com/photo/?fbid=1617674156394621"
  ],
  "maxResults": 500,
  "includeUnavailableRows": false,
  "proxyConfiguration": { "useApifyProxy": true }
}
```

**3 — Keyword discovery with filters.** Finds indexed posts about a topic, then narrows them.
Filtering happens after collection — a filtered-out post is never charged.

```json
{
  "searchQueries": ["artemis mission", "lunar lander"],
  "locations": ["United States", "Australia"],
  "maxResults": 300,
  "dateFrom": "2026-08-01",
  "minReactions": 50,
  "mediaTypes": ["photo", "video", "reel"],
  "sourceTypes": ["page", "post"],
  "proxyConfiguration": { "useApifyProxy": true }
}
```

For a scheduled job, add `skipPostIds` or `skipUrls` from the previous run's `RUN_SUMMARY` so each
run returns only what is new — see [Incremental runs](#incremental-runs).

***

### ⚠️ Why depth is limited without a login

This is the single most important thing to know before you set **Maximum posts**, and it is a
property of Facebook rather than of this Actor.

**A logged-out Facebook page renders exactly one post in its HTML.** Everything after that comes from
the same timeline endpoint the page's own JavaScript uses. That endpoint does answer logged-out
requests — but it is rate-limited per IP address, and **it refuses Apify's addresses on the first
call.** Measured across four runs on Apify Proxy spanning eight days: **25 timeline requests, 25
refused** — every one on the first call of a fresh session — while every page request from those
same addresses returned in full.

**So plan on one post per page URL.** Everything else about that post is complete — text, author,
timestamp, media, reactions with the per-reaction breakdown, comment and share counts, links.

| You want | Do this |
|---|---|
| **Volume** | **Paste post URLs.** One request each, never rate-limited, complete data. This is the path to use for a large run. |
| Recent posts across many pages | List many pages. Each contributes the post Facebook renders on it — so 200 pages gives you 200 current posts. |
| Deep history from one page | Not available on Apify's addresses. Your own residential proxy under **Custom proxy URLs** does get a small allowance; otherwise run on a schedule and use **Skip these post IDs** to build up over time. |

Every run reports exactly where its posts came from, in the log and in `RUN_SUMMARY.economics.feed`:

```
Posts emitted:        16
  from post URLs:     6      ← permalinks you supplied or search found
  from page HTML:     10     ← the one post each page renders
  from feed paging:   0      ← the timeline endpoint (refused on Apify addresses)
```

**Running without a proxy sends every request from one address**, which raises the chance of
Facebook blocking the run as a whole. It does not cost you timeline depth — there is none to lose on
Apify addresses either way. The run warns when you do this.

***

### Output

One row per unique post. Unknown values are `null` — never invented, never silently zero.

![Posts table view](https://raw.githubusercontent.com/coregentdevspace/facebook-posts-scraper-assets/main/facebook-posts-scraper-output-posts-table-view.png)

```json
{
  "recordType": "facebook_post",
  "postId": "1617674169727953",
  "postUrl": "https://www.facebook.com/NASA/posts/pfbid0p9U45uPC39zamcxn3YC…",
  "source": {
    "sourceType": "page",
    "sourceId": "100044561550831",
    "sourceUrl": "https://www.facebook.com/nasa",
    "inputType": "startUrl",
    "query": null,
    "queriesMatched": [],
    "location": null,
    "locationsMatched": [],
    "searchProvider": null,
    "detailSource": "facebook-page-html"
  },
  "author": {
    "id": "100044561550831",
    "name": "NASA - National Aeronautics and Space Administration",
    "url": "https://www.facebook.com/NASA",
    "username": "NASA",
    "profilePictureUrl": "https://scontent…",
    "isVerified": true
  },
  "publishedAt": "2026-08-31T21:57:23.000Z",
  "text": "Welcome to the Artemis Accords, Republic of Türkiye 🇹🇷…",
  "postType": "photo",
  "isPinned": null,
  "permalink": "https://www.facebook.com/NASA/posts/pfbid0p9U45uPC39zamcxn3YC…",
  "externalLinks": ["https://www.nasa.gov/organizations/oiir/artemis-accords/…"],
  "media": [
    {
      "type": "photo",
      "url": "https://www.facebook.com/photo/?fbid=1617674156394621&set=a.416661013162614",
      "thumbnailUrl": "https://scontent…",
      "durationSeconds": null,
      "width": 843,
      "height": 529,
      "altText": "Turkish Minister of Industry and Technology, Mehmet Fatih Kacır, 3rd from left, signs…",
      "id": "1617674156394621"
    }
  ],
  "engagement": {
    "reactions": 621,
    "comments": 33,
    "shares": 68,
    "reactionBreakdown": { "like": 490, "love": 109, "care": 10, "haha": 8, "wow": 3, "angry": 1 }
  },
  "video": null,
  "sharedPost": null,
  "scrapedAt": "2026-09-01T05:39:18.647Z",
  "unavailableReason": null
}
```

**`postType`** is one of `photo`, `video`, `reel`, `link`, `share` or `text`.

**`sharedPost`** is filled in when a post reshares another one. Its `engagement` stays the *sharer's*
own — a share with 10,000 reactions is not credited with the original's 640,000.

**`media[].altText`** is Facebook's own accessibility caption. On photo-only posts it is often the
only textual description the post carries.

#### Dataset views

Four tabs over the same rows — pick the one that matches what you are doing, or export the full
record above. Every sample below is a real row from a live run.

**Posts** — the default. Everything you normally want, in reading order.

```json
{
  "postUrl": "https://www.facebook.com/NASA/posts/pfbid0hSgbekvXSQGQJDibHXr71B5kh…",
  "publishedAt": "2026-09-08T16:27:43.000Z",
  "author": {
    "id": "100044561550831",
    "name": "NASA - National Aeronautics and Space Administration",
    "url": "https://www.facebook.com/NASA",
    "username": "NASA",
    "profilePictureUrl": "https://scontent…",
    "isVerified": true
  },
  "text": "Hey, neighbor\n\nDid you know our home galaxy — the Milky Way — has smaller galaxies that orbit around it?…",
  "postType": "photo",
  "engagement": { "reactions": 4347, "comments": 111, "shares": 444, "reactionBreakdown": { "like": 2922, "love": 1251, "wow": 94, "care": 67, "haha": 11, "sad": 2 } },
  "media": [{ "type": "photo", "url": "https://www.facebook.com/photo/?fbid=1624398355722201&set=a.416661013162614", "thumbnailUrl": "https://scontent…", "width": 640, "height": 509, "altText": "Blue and orange stars shine through a wispy bubble of gas…", "id": "1624398355722201" }],
  "externalLinks": [],
  "source": { "sourceType": "page", "sourceId": "100044561550831", "sourceUrl": "https://www.facebook.com/nasa", "inputType": "startUrl", "detailSource": "facebook-page-html" },
  "postId": "1624398375722199",
  "scrapedAt": "2026-09-09T04:43:15.393Z"
}
```

**Engagement** — spreadsheet-friendly. Drops media and source so the numbers line up in a CSV, and
keeps `text` so each row is still identifiable.

```json
{
  "postUrl": "https://www.facebook.com/Starbucks/posts/pfbid02PmrZNwEN2arNsnNxYzbT9c4…",
  "publishedAt": "2026-09-08T13:31:37.000Z",
  "author": { "id": "100064487329582", "name": "Starbucks", "username": "Starbucks", "isVerified": true },
  "postType": "text",
  "engagement": {
    "reactions": 5633,
    "comments": 165,
    "shares": 695,
    "reactionBreakdown": { "like": 3662, "haha": 1707, "love": 228, "care": 23, "sad": 7, "wow": 5, "angry": 1 }
  },
  "text": "me pretending one coffee is enough to recover from a 3-day weekend"
}
```

**Media** — posts that carry attachments, with the `video` block alongside. `video.views` and
`video.transcript` are always `null` on this surface; see
[What this Actor cannot do](#what-this-actor-cannot-do).

```json
{
  "postUrl": "https://www.facebook.com/reel/2557680831370435/",
  "publishedAt": "2026-09-08T23:43:10.000Z",
  "author": { "id": "100064916943021", "name": "The Cheesecake Factory", "username": "thecheesecakefactory", "isVerified": true },
  "postType": "reel",
  "media": [
    {
      "type": "video",
      "url": "https://www.facebook.com/reel/2557680831370435/",
      "thumbnailUrl": "https://scontent…",
      "durationSeconds": 12,
      "width": null,
      "height": null,
      "altText": null,
      "id": "2557680831370435"
    }
  ],
  "video": { "durationSeconds": 12, "views": null, "transcript": null },
  "externalLinks": []
}
```

**Unavailable** — empty by default. It only fills when you switch on **Include unavailable posts as
empty rows**; otherwise these are reported in the log, `RUN_SUMMARY.notAvailableByReason` and
`RETRY_INPUT` instead of cluttering your dataset. Shape:

```json
{
  "postUrl": "https://www.facebook.com/SomePage/posts/1234567890123456",
  "postId": "1234567890123456",
  "unavailableReason": "login-wall",
  "source": { "sourceType": "post", "inputType": "startUrl", "detailSource": null },
  "scrapedAt": "2026-09-09T04:43:15.393Z"
}
```

`unavailableReason` is one of `login-wall`, `soft-block`, `rate-limited`, `blocked`, `not-found`,
`group-not-readable-logged-out`, `timeout`, `proxy-error` or `request-failed`. The first four mean
the post exists and a later run usually gets it — those are the ones written into `RETRY_INPUT`.
`not-found` means deleted or not public, and re-running will not recover it. **Unavailable rows are
never charged, whether or not you display them.**

***

### What this Actor cannot do

Stated plainly, because each of these was tested rather than assumed, and a scraper that quietly
returns nothing is worse than one that says why.

| Not available | Why |
|---|---|
| **Comment text** | Logged-out Facebook publishes the comment *count* but no comment bodies — the permalink page carries `can_viewer_comment: false`, a comments-disabled notice and zero comment records. Comment counts **are** returned. |
| **Video transcripts / captions** | Every video attachment served logged out carries an empty captions list and no captions URL. |
| **Video view counts** | Not published on this surface. `video.views` is permanently `null` rather than guessed from reactions. |
| **Facebook groups** | A logged-out request for any group returns an empty shell with no posts at all — public groups included. Group URLs are rejected with that reason rather than accepted and silently returning nothing. |
| **Pinned-post flag** | The timeline Facebook serves logged-out explicitly omits pinned posts, so `isPinned` is `null` rather than a guess. |
| **Deep history from one page** | Facebook's timeline endpoint refuses Apify's addresses outright — measured, 25 of 25 across four runs. See [Why depth is limited](#-why-depth-is-limited-without-a-login). |
| **Private, friends-only or gated content** | Out of scope by design. This Actor never asks for credentials and cannot see anything a logged-out visitor cannot. |

The only way to get comments, transcripts or deep history is a logged-in session. This Actor does
not ask for your Facebook account, and will not.

***

### Filters

All filters run **after** posts are collected, because Facebook offers no way to search by
engagement or media type. A filtered-out post is never charged, so narrowing costs you nothing.

- **Minimum reactions / comments / shares** — posts where Facebook published no figure are **kept**.
  An unknown count is not a low one.
- **Media types** — `photo`, `video`, `reel`, `link`, `share`, `text`. A reel also matches `video`.
- **Posted on or after / before** — both bounds inclusive. A plain date as `dateTo` covers the whole
  of that day. Posts with no timestamp are dropped when a date bound is set, and counted separately
  as `dateUnknown` so you can tell that apart from an over-narrow range.
- **Only / exclude these authors** — matched on name, handle or numeric ID. If a post matches both
  lists it is excluded.
- **Only these source types** — `page`, `profile`, `post`.

***

### Incremental runs

For a scheduled job that should only return what is new:

- **Skip these post IDs** / **Skip these post URLs** — anything already collected.
- **Posted on or after** — plus **Stop a timeline after N posts older than your start date**, which
  stops paging a page once it has gone past your window instead of fetching history you filtered out.

`RUN_SUMMARY` gives you the IDs to carry forward; `RETRY_INPUT` gives you a ready-to-run input
containing only what was blocked or rate-limited — never posts that are simply gone, because
re-running those spends a request every time to confirm what is already known.

***

### Pricing

Pay per result. You are charged **once per unique post returned with data**, and never for:

- duplicates — a post found by two searches, or supplied directly *and* read from a timeline, is
  charged once;
- posts removed by your filters;
- posts on your skip lists;
- posts that were deleted, not public, blocked or rate-limited.

By default, posts that could not be read are left out of the dataset entirely rather than returned
as blank rows. The run log, the run status, `RUN_SUMMARY` and `RETRY_INPUT` all report them, and a
CSV full of empty rows imports junk into whatever you load it into. Switch on **Include unavailable
posts as empty rows** if you want them anyway — they are free either way.

There is no start fee, no subscription and no monthly rental — you pay only for posts you actually
receive, so a run that returns nothing costs you nothing.

This README deliberately quotes no figures. Live prices are on the Actor's Pricing tab, which is the
only place that cannot go stale.

***

### 🚦 Proxy policy

Use **Apify Datacenter** proxy — it is the default, and it is what keeps a multi-page run from being
blocked as a whole. It does NOT unlock timeline depth: Facebook's feed endpoint refuses Apify
addresses on the first call, measured. Depth needs a different class of address, which means your own
provider under **Custom proxy URLs**.

**Apify Residential proxy is not supported.** The run fails at startup if `apifyProxyGroups`
includes `RESIDENTIAL`. Two reasons: residential bandwidth is billed to the Actor developer rather
than to your run, at several times what the results are priced at; and it buys nothing here, because
Facebook gates on the request fingerprint rather than the IP, and a per-address call budget is not
raised by a more expensive address.

If you genuinely need residential routing, supply your own provider under **Custom proxy URLs** —
that traffic goes through your account and is honoured in full:

```
http://user:pass@proxy.iproyal.com:12321
http://user:pass@proxy.brightdata.com:22225
http://user:pass@proxy.oxylabs.io:7777
```

***

### API example

```bash
curl -X POST "https://api.apify.com/v2/acts/coregent~facebook-posts-scraper/runs?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
        "startUrls": [
          "https://www.facebook.com/nasa",
          "https://www.facebook.com/NASA/posts/1498937668268271"
        ],
        "searchQueries": ["artemis mission"],
        "maxResults": 200,
        "dateFrom": "2026-08-01",
        "minReactions": 10,
        "mediaTypes": ["photo", "video"],
        "proxyConfiguration": { "useApifyProxy": true }
      }'
```

The Console and the API take the same input. `maxResults` is global and deterministic, and you never
need to understand a provider cursor.

***

### Troubleshooting

**"I only got one post per page."**
That is the expected result on Apify's addresses — Facebook's timeline endpoint refuses them, so
each page contributes the single post it renders. Switching Apify Proxy on will not change it. For
volume, supply post URLs directly, or list more pages. See
[Why depth is limited](#-why-depth-is-limited-without-a-login).

**"My keyword search returned pages, not posts."**
Expected — a web search for posts turns up profile pages too. The run reports how many. Put those
page URLs in **Facebook URLs** to collect their timelines.

**"Some posts came back unavailable."**
Check `RUN_SUMMARY.notAvailableByReason`. `login-wall` and `soft-block` mean Facebook withheld a
post that exists — those are in `RETRY_INPUT`, and a later run usually gets them. `not-found` means
the post is deleted or not public, and re-running will not recover it.

**"The run failed saying there was nothing to scrape."**
You supplied only keywords and keyword discovery is not configured on this Actor, or the search
found no posts. Supply Facebook URLs directly, or broaden the keywords.

**"Reaction counts changed between runs."**
They are live figures. A popular post can gain thousands of reactions in an hour.

***

### Privacy and platform disclaimer

This Actor processes **publicly accessible** Facebook data only — what any logged-out visitor can
see. It never requests, stores or uses Facebook credentials, cookies, session IDs, access tokens or
browser profiles, and it cannot reach private, friends-only, group or otherwise gated content.

You are responsible for complying with applicable law, Meta's terms, and your own privacy and
data-protection obligations, including how you store and use any personal data in the results.

# Actor input Schema

## `startUrls` (type: `array`):

Public Facebook POST URLs, or PAGE / PROFILE URLs. A post URL returns that one post, complete — this is the reliable path, and the one to use for volume. A page or profile URL returns the single post Facebook renders on the page itself: reading further back through a timeline needs Facebook's feed endpoint, which refuses Apify's addresses outright (measured), so in the default configuration expect ONE post per page. See the Actor's README before planning a large run. Accepts any URL form (/posts/, /videos/, /reel/, /photo/, /watch/, /permalink.php), a bare page handle, an @handle, or a numeric page ID. Facebook GROUPS are not supported: a logged-out request for a group returns an empty page with no posts at all.

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

Keywords, phrases or hashtags to find public posts about. Discovery searches the open web for indexed Facebook posts — Facebook itself publishes no logged-out search. Roughly half of what a keyword returns is Facebook pages rather than individual posts; those are counted and reported, and you can paste them into 'Facebook URLs' to collect their timelines. Requires a search provider key to be configured on the Actor.

## `maxResults` (type: `integer`):

GLOBAL cap on posts returned across every URL and every query — not per source. You are charged only for the posts you actually receive.

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

Only return posts published on or after this date (inclusive). A plain date such as 2026-08-01 is read as UTC midnight. Timelines are newest-first, so this also lets the Actor stop paging a page once it has gone past your date instead of fetching history you filtered out.

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

Only return posts published on or before this date (inclusive). A plain date such as 2026-08-31 covers the whole of that day.

## `sort` (type: `string`):

How the posts collected are ordered before the maximum is applied. 'Newest first' matches the timeline. 'Most reactions first' sorts the posts this run collected — Facebook publishes no logged-out 'top' ranking, so this is an ordering of your results, not a platform popularity ranking.

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

Drop posts with fewer reactions than this. Posts where Facebook published no reaction count are KEPT — an unknown count is not a low one.

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

Drop posts with fewer comments than this. Posts with no published comment count are kept.

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

Drop posts with fewer shares than this. Posts with no published share count are kept.

## `mediaTypes` (type: `array`):

Keep only posts of these kinds. Leave empty for all. A reel also counts as a video. A shared post is one that reshares another post — its own text and media are usually empty, and the original it points at is reported in the row's sharedPost field.

## `authorInclude` (type: `array`):

Keep only posts whose author name, handle or numeric ID matches one of these (case-insensitive, partial matches allowed). Leave empty for all authors.

## `authorExclude` (type: `array`):

Drop posts by these authors. Matched the same way as the include list. If a post matches both lists it is excluded — a deny list is treated as the stronger instruction.

## `sourceTypes` (type: `array`):

Keep only posts that came from these kinds of source. Leave empty for all.

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

Optional place names appended to each search query, so one keyword becomes several searches. Only affects keyword discovery; it does nothing to URLs you supply. Adding locations returns more posts than searching one keyword more deeply.

## `maxResultsPerSource` (type: `integer`):

A fairness ceiling so one very active page cannot consume the whole global limit. 0 means no per-source ceiling. This is never the primary control — 'Maximum posts' is.

## `stopAfterConsecutiveOldPosts` (type: `integer`):

Timelines are newest-first, so once this many posts in a row fall before 'Posted on or after', the rest are older still and are not worth fetching. Only applies when a start date is set. 0 disables the early stop.

## `skipPostIds` (type: `array`):

Numeric post IDs to leave out — for scheduled runs that should only return what is new since last time.

## `skipUrls` (type: `array`):

Post URLs to leave out. Matched on the post's identifier, so any URL form of the same post works.

## `includeUnavailableRows` (type: `boolean`):

Off by default. When on, posts that could not be read are returned as empty rows carrying an 'unavailableReason' instead of being left out. They are never charged either way. The run log, the run's status message, RUN\_SUMMARY and the RETRY\_INPUT record already report what was missed, so this is only useful if you want the gaps in the dataset itself.

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

Apify Proxy (Datacenter) is strongly recommended and on by default. Facebook allows only a couple of timeline requests per IP address, so without a proxy a multi-page run collects roughly one post per page. APIFY RESIDENTIAL IS NOT SUPPORTED and the run will fail if you select it: residential bandwidth is billed to the Actor rather than to your run, it costs several times what the results are priced at, and it returns identical data because Facebook gates on the request fingerprint, not the IP. If you need residential routing, supply your own provider under Custom proxy URLs — that traffic goes through your account and is honoured in full.

## Actor input object example

```json
{
  "startUrls": [
    "https://www.facebook.com/nasa"
  ],
  "searchQueries": [],
  "maxResults": 100,
  "dateFrom": "",
  "dateTo": "",
  "sort": "latest",
  "minReactions": 0,
  "minComments": 0,
  "minShares": 0,
  "mediaTypes": [],
  "authorInclude": [],
  "authorExclude": [],
  "sourceTypes": [],
  "locations": [],
  "maxResultsPerSource": 0,
  "stopAfterConsecutiveOldPosts": 3,
  "skipPostIds": [],
  "skipUrls": [],
  "includeUnavailableRows": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `engagement` (type: `string`):

No description

## `media` (type: `string`):

No description

## `runSummary` (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 = {
    "startUrls": [
        "https://www.facebook.com/nasa"
    ],
    "searchQueries": [],
    "maxResults": 100,
    "sort": "latest",
    "minReactions": 0,
    "minComments": 0,
    "minShares": 0,
    "mediaTypes": [],
    "authorInclude": [],
    "authorExclude": [],
    "sourceTypes": [],
    "locations": [],
    "maxResultsPerSource": 0,
    "stopAfterConsecutiveOldPosts": 3,
    "skipPostIds": [],
    "skipUrls": [],
    "includeUnavailableRows": false,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("coregent/facebook-posts-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 = {
    "startUrls": ["https://www.facebook.com/nasa"],
    "searchQueries": [],
    "maxResults": 100,
    "sort": "latest",
    "minReactions": 0,
    "minComments": 0,
    "minShares": 0,
    "mediaTypes": [],
    "authorInclude": [],
    "authorExclude": [],
    "sourceTypes": [],
    "locations": [],
    "maxResultsPerSource": 0,
    "stopAfterConsecutiveOldPosts": 3,
    "skipPostIds": [],
    "skipUrls": [],
    "includeUnavailableRows": False,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("coregent/facebook-posts-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 '{
  "startUrls": [
    "https://www.facebook.com/nasa"
  ],
  "searchQueries": [],
  "maxResults": 100,
  "sort": "latest",
  "minReactions": 0,
  "minComments": 0,
  "minShares": 0,
  "mediaTypes": [],
  "authorInclude": [],
  "authorExclude": [],
  "sourceTypes": [],
  "locations": [],
  "maxResultsPerSource": 0,
  "stopAfterConsecutiveOldPosts": 3,
  "skipPostIds": [],
  "skipUrls": [],
  "includeUnavailableRows": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call coregent/facebook-posts-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,coregent/facebook-posts-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/FrSfi4jeErCwkcyHS/builds/5s0GDrGuIZYldfDzf/openapi.json
