# Facebook Posts Scraper — Page Timeline Export (`devilscrapes/facebook-posts-scraper`) Actor

Export public Facebook Page timeline posts — text, permalink, timestamp, author, reaction/share/comment counts, and photo/video attachments. No login, no cookies, no Facebook API access token. Works from a Page vanity name, numeric ID, or full URL.

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

## Pricing

Pay per event

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?

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

<div align="center">
  <img src=".actor/icon.svg" width="160" alt="Devil Scrapes mark" />

## Facebook Posts Scraper

**$3.02 / 1 000 posts**  ·  pay only for results  ·  no credit card to try

*The devil's in the data — we go get it.* 😈

Export public Facebook Page timeline posts — text, permalink, timestamp, author, reaction/share/comment counts, and photo/video attachments. No login, no cookies, no access token.

</div>

***

Point this Actor at any public Facebook Page (vanity name, numeric ID, or full URL) and it walks the timeline forward from the current cursor, returning one clean row per post. Facebook has no public feed API for this, so we talk to the same internal GraphQL endpoint the Page itself loads, handle the persisted-query id Meta rotates on every deploy, and hand you back structured JSON instead of a browser session to babysit. Pricing is per post landed — a run that finds nothing costs you only the flat start fee.

### 🎯 What this scrapes

Public Facebook **Page** timelines — the same posts you'd see scrolling a Page's own feed while logged out. For each `page_identifiers` entry we:

- Resolve the Page from a vanity name (`nasa`), a numeric Page ID, or a full `facebook.com/...` URL.
- Walk the timeline forward from the newest post, page by page, until we hit `max_posts_per_page` or the Page runs out of posts.
- Emit one row per post: body text, permalink, author, timestamp, and engagement counts.

Groups, personal profiles, Reels, Marketplace listings, and Events are out of scope — see Limitations below.

### 🔥 Features

- 🛡️ **We rotate browser fingerprints** via `curl-cffi` impersonation so Facebook's servers see a real browser's TLS handshake, not a bare Python client.
- 🔁 **We retry with exponential backoff** on `408`/`429`/`503` and network errors — up to 5 attempts per request, honouring `Retry-After` when Facebook sends it.
- 🌐 **We route every request through Apify Proxy** and keep sessions isolated per Page, so one blocked identifier never poisons the rest of the run.
- 🔧 **We self-heal the persisted-query id.** Meta rotates the internal `doc_id` this endpoint depends on with every deploy; when a refetch comes back stale, we re-derive the current id straight from the Page's own JS bundles, cache it, and retry — no manual patch, no waiting on us to ship a fix.
- 🧱 **We isolate failures per Page.** A Page that's private, deleted, or behind a login wall doesn't take down the rest of your batch — it's logged and reported on its own, and the run keeps going.
- 🧊 **We keep the dataset clean** — Pydantic-validated rows, ISO-8601 timestamps, stable post IDs, structured attachments instead of raw HTML.
- 💰 **You pay only for results that land.** No posts, no per-post charge — only the small `actor-start` warm-up fee applies.

### 🧭 Use cases

- **Competitive content tracking** — monitor how often a competitor's Page posts, what they post, and how it performs (reactions/shares/comments) over time.
- **Brand and PR monitoring** — pull a Page's own public posts into your analytics stack instead of screenshotting a feed.
- **Media/research datasets** — build a corpus of public statements from official Pages (news outlets, government agencies, brands) for text analysis.
- **Feed archiving** — keep a structured, queryable record of a Page's public timeline instead of relying on Facebook's own search.

### ⚙️ How to use it

1. Click **Try for free** (or run via API/CLI) and open the input form.
2. Enter one or more `page_identifiers` — vanity names, numeric Page IDs, or full URLs. Mixed forms are fine in the same run.
3. Set `max_posts_per_page` if you want fewer or more posts than the default `40` per Page.
4. Leave `proxy_configuration` on its default unless you have a reason to override it.
5. Run it. Each `page_identifiers` entry is processed in turn; results land in the dataset as they're scraped, so you can start exporting before the run finishes.

### 📥 Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `page_identifiers` | array of string | — | Required. Vanity name, numeric Page ID, or full URL. Mixed forms OK. |
| `max_posts_per_page` | integer | `40` | Stop paginating a Page once this many posts are collected (1–1000). |
| `posts_per_request` | integer | `8` | GraphQL refetch page size (1–20). Larger values don't reliably return more posts per request — Facebook's response size is driven by the feed, not this count. |
| `proxy_configuration` | object | residential (`RESIDENTIAL`) | Apify Proxy configuration. Datacenter groups get Facebook's own rate limiting; override only if you know what you're doing. |

```json
{
  "page_identifiers": ["nasa", "nike"],
  "max_posts_per_page": 10,
  "posts_per_request": 8,
  "proxy_configuration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"]
  }
}
```

### 📤 Output

| Field | Type | Notes |
|---|---|---|
| `page_identifier` | string | Echo of the input identifier this post was found under. |
| `post_id` | string | Stable post id. |
| `text` | string | null | Post body text. |
| `permalink` | string | `pfbid` permalink URL. |
| `creation_time` | string (ISO-8601) | Post creation time. |
| `author_name` / `author_id` / `author_url` | string | Post author. |
| `is_cross_post` | boolean | `true` when the author differs from the requested Page's own id. |
| `reaction_count` / `share_count` / `comment_count` | integer | Engagement counts. |
| `attachments` | array | Photo/Video attachments: `{type, attachment_id, url}`. |

```json
{
  "page_identifier": "nasa",
  "post_id": "1629528961875807",
  "text": "Welcome to the Artemis Accords, Djibouti...",
  "permalink": "https://facebook.com/NASA/posts/pfbid02JgLRZAPvjgGRymFsFcdzYXYdDqg3knu3X2PAsYbgFcjyMtCQw3XWRQZ8VbbUT6N5l",
  "creation_time": "2026-09-06T00:00:00+00:00",
  "author_name": "NASA",
  "author_id": "100044561550831",
  "author_url": "https://facebook.com/NASA",
  "is_cross_post": false,
  "reaction_count": 393,
  "share_count": 21,
  "comment_count": 47,
  "attachments": [{ "type": "Photo", "attachment_id": "1629528958542474", "url": null }]
}
```

### 💰 Pricing

Pay-per-event. No subscription, no seat fee — you're billed for a run's warm-up and for every post that actually lands in your dataset.

| Event | Price | Trigger |
|---|---|---|
| `actor-start` | $0.20 | Once per run, the first time any Page's timeline is successfully read (whether that yields posts or a genuine zero-post result). |
| `post-scraped` | $0.003 | Once per post written to the dataset. |

At the default settings that works out to **$3.20 per 1,000 posts** ($0.20 start + 1,000 × $0.003). A run against a Page with no new posts still costs the $0.20 start fee and nothing more — you're never billed per post for posts that weren't there.

### 🚧 Limitations

- **Public Page timelines only.** No login, no cookies beyond the anonymous bootstrap request, and no account of ours is used to reach anything.
- **No groups, personal profiles, Reels, Marketplace, or Events.** A profile or group URL will fail to resolve rather than return the wrong content.
- **No comment-thread expansion.** You get the `comment_count` total, not the individual comments.
- **No historical date-range seeking.** The Actor walks forward from the Page's current timeline cursor; it doesn't jump to an arbitrary past date.
- **Depth is bounded by `max_posts_per_page`.** Facebook's own pagination limits, not this Actor, set the practical ceiling on how far back a single run can walk.
- If Facebook's servers can't be reached at all, or a Page is private/deleted/behind a login wall, that identifier is reported as failed rather than as an empty (but successful) result — the two are never conflated.
- **`posts_per_request` is not a real filter.** Recon shows Facebook's response size doesn't scale with it — 8 is the measured effective value regardless of what's set. It's kept as an input for forward compatibility, not because it currently changes behaviour.

### ❓ FAQ

**Does this need a Facebook account or access token?**
No. It reads the same public timeline anyone can see logged out.

**Can it scrape groups or personal profiles?**
No — Page timelines only. See Limitations.

**Why did one of my `page_identifiers` come back with zero rows?**
Either the Page genuinely has no posts to return right now (a real, successful zero), or the identifier didn't resolve at all (private, deleted, or a login wall) — the run's status message tells you which, per identifier.

**What proxy should I use?**
Residential, which is the default — Facebook's own application-level rate limiting hits shared datacenter IP pools within the first request or two. Leave `proxy_configuration` as-is unless you have a specific reason to override it.

**How far back can I go?**
As far as `max_posts_per_page` lets the Actor walk forward through the timeline in one run. There's no date-range jump — see Limitations.

### 🙋 Your feedback

Found a Page this doesn't handle right, or want a field we don't capture yet? Message **DevilScrapes** on Apify or open an issue from the Actor's Store page — we read every one and ship fixes fast.

# Changelog

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

# Actor input Schema

## `page_identifiers` (type: `array`):

Public Facebook Pages to scrape — vanity name (<code>nasa</code>), numeric Page ID, or full URL. Mixed forms are fine.

## `max_posts_per_page` (type: `integer`):

Stop paginating a Page once this many posts have been collected.

## `posts_per_request` (type: `integer`):

GraphQL refetch page size (<code>count</code>). Recon shows response size does not scale with this value — 8 is the measured default, not a real lever.

## `proxy_configuration` (type: `object`):

Apify Proxy. <strong>Defaults to Residential</strong> — cloud-evidenced 2026-09-17: the shared <code>BUYPROXIES94952</code> datacenter pool gets Facebook's application-level <code>Rate limit exceeded</code> (code 1675004) on the very first refetch POST, while Residential clears cleanly through the identical code path.

## Actor input object example

```json
{
  "page_identifiers": [
    "nasa",
    "nike"
  ],
  "max_posts_per_page": 40,
  "posts_per_request": 8,
  "proxy_configuration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `datasetItems` (type: `string`):

Full set of scraped posts — one item per post. Streamed JSON via the Apify dataset API.

## `datasetItemsCsv` (type: `string`):

Same data, CSV export. Drop straight into a spreadsheet or warehouse loader.

## `datasetView` (type: `string`):

Open the run's dataset in the Console with the Overview view applied.

# 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 = {
    "page_identifiers": [
        "nasa",
        "nike"
    ],
    "max_posts_per_page": 40,
    "posts_per_request": 8,
    "proxy_configuration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("devilscrapes/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 = {
    "page_identifiers": [
        "nasa",
        "nike",
    ],
    "max_posts_per_page": 40,
    "posts_per_request": 8,
    "proxy_configuration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("devilscrapes/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 '{
  "page_identifiers": [
    "nasa",
    "nike"
  ],
  "max_posts_per_page": 40,
  "posts_per_request": 8,
  "proxy_configuration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call devilscrapes/facebook-posts-scraper --silent --output-dataset

```

## MCP server setup

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