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

Scrape recent public posts from any Facebook Page: text, exact post time, permalink, photos and videos, reaction, comment and share counts, plus page identity. no login needed from you, no browser.

- **URL**: https://apify.com/dami\_studio/facebook-posts-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Social media, Marketing, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.65 / 1,000 post scrapeds

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

Paste public Facebook Page URLs and get their recent posts back as clean, flat rows: the full post text, the exact time it was posted, its permalink, every photo and video attached to it, and the reaction, comment and share counts — plus who the Page is. No login, no cookies, no Facebook account, no browser, and nothing for you to configure.

- Up to 50 Pages in a single run, one row per post.
- Reaction counts broken down by type (Like, Love, Wow, Care, Haha, Sad, Angry), not just a single total.
- Photos, videos, albums and Reels come back with their ids, permalinks, thumbnails and alt text.
- Date filtering is included at no extra charge — it is a field on the input, not a paid add-on.
- Runs with empty input return a labelled sample row, free, so you can see the shape first.
- A Page that is blocked, private or misspelled produces an uncharged diagnostic row, never a failed run.

### Price

**$0.65 per 1,000 posts** ($0.00065 each), plus a **$0.0015 start fee per run** (billed per gigabyte of run memory, so exactly that on the default 1 GB).

It is the same rate on every plan, free or paid. There are no volume tiers, no minimum spend, no subscription and no add-on fees. What you read here is what you pay on day one and on day four hundred.

One run tops out at 1,000 posts — 50 Pages at 20 posts each — so larger totals are several runs and pay the start fee once per run.

| Posts | Runs | Total cost |
|---|---|---|
| 100 | 1 | $0.0665 |
| 1,000 | 1 | $0.6515 |
| 10,000 | 10 | $6.515 |
| 100,000 | 100 | $65.15 |

#### What is actually charged

- **One `post-scraped` event per post row written to the dataset.** Nothing else is metered per row.
- **Free:** the sample row an empty run returns, and every diagnostic row — a blocked target, a dead URL, a search that matched nothing. Those rows all carry `"charged": false`.
- Posts dropped by your date filter are never charged — the filter runs before billing, and using it costs nothing extra.
- Duplicate posts inside a single run are dropped before they are charged.
- A Page that returns nothing produces one free diagnostic row and no post charges.
- A run that finds nothing costs the start fee and nothing more.
- Rows never leave the dataset without a charge, and are never charged without a row. The billed event is the named `post-scraped`. Nothing is priced against the built-in `apify-default-dataset-item`, which bills every row pushed to the dataset — sample and diagnostic rows included.

### Input

```json
{
  "startUrls": [
    "https://www.facebook.com/NASA",
    "https://www.facebook.com/BBCNews"
  ],
  "resultsLimit": 20,
  "onlyPostsNewerThan": "30 days"
}
```

| Field | What it does |
|---|---|
| `startUrls` | Public Facebook Page URLs, one per line. A bare handle (`NASA`), a `/pg/` URL, a `/p/` URL and a `profile.php?id=` URL all work. Up to 50 Pages per run. |
| `resultsLimit` | How many recent posts to take from each Page, newest first. Default 20, which is also the ceiling — see Limitations. Keep it low while testing; you pay per post. |
| `onlyPostsNewerThan` | Optional. Drop posts older than this. Accepts `2026-08-01`, a full ISO timestamp, an epoch, or a relative window such as `7 days`. Free. |
| `onlyPostsOlderThan` | Optional. Drop posts newer than this. Same formats. Free. |
| `proxyUrls` | Leave empty. Fill it in only if you want the traffic to leave through proxy servers you already pay for, as `http://user:pass@host:port`. |
| `sessionCookies` | Leave empty. Runs are logged out by default and that is enough for public Pages. Supply your own `c_user`/`xs` cookie only if you want a Page that walls logged-out visitors retried on your own account, under your own rate limit. |

Run it with **empty input** and you get one clearly labelled sample row, free, so you can see the output shape before you spend anything.

### Output

One row per post. A real row from a real run:

```json
{
  "ok": true,
  "charged": true,
  "recordType": "post",
  "inputUrl": "https://www.facebook.com/NASA",
  "pageName": "NASA - National Aeronautics and Space Administration",
  "pageId": "100044561550831",
  "pageUrl": "https://www.facebook.com/NASA/",
  "pageIsVerified": true,
  "pageFollowers": null,
  "pageLikes": 28696238,
  "pageProfilePicture": "https://lookaside.fbsbx.com/lookaside/crawler/media/?media_id=100044561550831",
  "postId": "1602283071267063",
  "postUrl": "https://www.facebook.com/NASA/posts/pfbid0zJ6mq7LDUr5ZxqeqrrPDCWnv87PJXe1qg9YoKpQNyULieuLXMdJjXJ2DEUAcTwfyl",
  "text": "Our photographers were on hand in Maine and Spain to capture the Aug. 12 solar eclipse. Check out a few of their photos here and see the rest on Flickr: https://www.flickr.com/photos/nasahqphoto/",
  "textLength": 195,
  "time": "2026-08-13T18:17:19.000Z",
  "timestamp": 1786645039,
  "reactionsCount": 25128,
  "topReactions": [
    {
      "type": "Like",
      "count": 17443
    },
    {
      "type": "Love",
      "count": 6977
    },
    {
      "type": "Wow",
      "count": 347
    },
    {
      "type": "Care",
      "count": 327
    }
  ],
  "commentsCount": 337,
  "sharesCount": 2729,
  "postType": "photo",
  "media": [
    {
      "type": "photo",
      "id": "1602282937933743",
      "url": "https://www.facebook.com/photo.php?fbid=1602282937933743&set=a.416661013162614&type=3",
      "thumbnailUrl": "https://lookaside.fbsbx.com/lookaside/crawler/media/?media_id=1602282937933743",
      "altText": "In this eclipse photo, the Moon is passing fully in front of the Sun, creating a perfectly black disc surrounded by the Sun's shining yellow corona.",
      "style": "Album"
    }
  ],
  "mediaCount": 4,
  "isReel": false,
  "isSponsored": false,
  "linkUrl": null,
  "linkTitle": null,
  "scrapedAt": "2026-08-15T22:31:07.182Z"
}
```

#### Field notes

- `inputUrl` — the Page URL you asked for, so you can group rows by Page after merging several runs.
- `postUrl` — the post permalink. Reels come back as `/reel/…`, videos as `/videos/…`, ordinary posts as `/posts/pfbid…`.
- `time` — the exact posting time in ISO 8601 UTC, taken from the post itself rather than a "2 days ago" label. `timestamp` is the same moment in epoch seconds.
- `reactionsCount` — total reactions of all types at read time. `topReactions` breaks the same total down by reaction type.
- `commentsCount` — the public comment count. The comments themselves are not returned — this Actor scrapes posts.
- `sharesCount` — shares at read time. Null when Facebook does not publish a share count for that post.
- `postType` — one of `video`, `photo`, `link` or `status`, derived from what is actually attached.
- `media` — an array. Photos and videos carry an id, a permalink, a thumbnail and, when Facebook supplies one, the alt text written for screen readers. Videos also carry `durationMs`.
- `isSponsored` — true when the post is a paid placement rather than an organic one.
- `pageFollowers` — null on many Pages — Facebook only publishes a follower count on some of them. `pageLikes` is the more widely available figure.

Every real row carries `"charged": true`. Sample rows carry `"_sample": true` and diagnostic rows carry `"_diagnostic": true` with an `errorCode` you can filter on, and neither is ever billed.

### How it works

- It reads the public, logged-out version of the Page that Facebook publishes for indexing, and lifts the posts out of the structured data already inside that page. No headless browser, no rendering, no scrolling, no account and no cookies.
- One request per Page delivers the whole batch of recent posts, which is why the price can be this low: the cost of a run barely moves with the number of posts you take from a Page.
- The response is read only as far as the posts, then cut off — the rest of a Facebook page is player state this Actor has no use for.
- Traffic leaves through a large pool of rotating addresses. Facebook throttles per address, so a refusal is answered by moving to a fresh one and asking again, up to eight times per Page, with the request identity varied each time.
- Nothing is charged until a post row is safely written to your dataset.

### What people use it for

- Brand and market monitoring — pull the recent posts of twenty Pages every morning and diff on `postId` to see only what is new.
- Social media reporting — reaction, comment and share counts land as plain numbers, so engagement rates are a spreadsheet formula rather than a data-cleaning project.
- Content research — read what a whole category of Pages posted this week, sorted by `reactionsCount`, before deciding what to publish yourself.
- Feeding a dashboard or a warehouse: rows are flat and typed, so they load without a transformation step.
- Archiving a Page you own or follow, including the permalinks and media URLs, so the record survives a deleted post.
- Lightweight sentiment and topic analysis — `text` is the full post message, not a truncated preview, so it goes straight into an NLP pipeline.

### Reading the output

Every run writes three kinds of row, and they are easy to tell apart:

- **Real rows** carry `"charged": true` and `"recordType": "post"`. These are the rows you paid for, one billed event each.
- **The sample row** carries `"_sample": true` and `"charged": false`. There is exactly one, it only appears when the input had no Page URLs, and it exists so you can look at the output shape before you spend anything.
- **Diagnostic rows** carry `"_diagnostic": true`, `"charged": false` and an `errorCode` you can switch on: `NOT_FOUND` when there is no public Page at that URL, `BLOCKED` when Facebook served a login interstitial from every address tried, `NO_RESULTS` when the Page rendered but published no posts, `BAD_INPUT` when a URL could not be read as a Page, `TIME_BUDGET` when the run ran out of time before reaching a Page, and `NETWORK` when Facebook could not be reached. Each carries the `inputUrl` it belongs to and a plain-English explanation.

If you only want the data, filter on `charged == true`. The count of those rows always equals the number of events you were billed for, so the dataset is its own invoice.

### Limitations

- The public Page render carries the 20 most recent posts, and that is the hard ceiling per Page per run. This Actor goes wide rather than deep: 50 Pages of 20 posts in one run, not 1,000 posts from one Page. If you need deep history, this is not the tool.
- Because of that ceiling, the date filters narrow the recent posts a Page publishes — they cannot reach further back than those posts. Asking for last year on a Page that posts daily will correctly return nothing.
- Public Pages only. Personal profiles with a private timeline, Groups, Events and anything behind a login are out of scope and produce an uncharged `NOT_FOUND` or `BLOCKED` row.
- Some Pages are age-restricted or country-restricted and show a logged-out visitor no timeline at all. Those return an uncharged diagnostic row rather than posts, and no amount of retrying changes it.
- Comments themselves are not returned, only the comment count. The same goes for the identities of people who reacted.
- Reaction, comment and share counts are a snapshot at read time and keep moving on a live post.
- Pinned posts are excluded by Facebook from the public timeline render, so a Page whose newest content is pinned may show it lower down or not at all.
- `sharesCount` and `pageFollowers` are null on some Pages because Facebook does not publish them there. Nulls are honest, not missing data.
- Facebook throttles per address. Under heavy parallel load a Page can come back as `BLOCKED`; re-running a few minutes later normally clears it, and nothing is charged for the attempt.
- The text is returned as posted. It is not translated, and emoji and line breaks are preserved exactly.

### Questions

**Do I need a Facebook account or cookies?**

No. Runs are logged out and read only what Facebook shows the public web, which is also why private content is out of reach. There is an optional `sessionCookies` field for callers who want a Page that walls logged-out visitors retried on their own account, but it stays off unless you fill it in.

**Can I get more than 20 posts from one Page?**

Not from this Actor. Facebook publishes the 20 most recent posts to a logged-out visitor and nothing further without an account, and this Actor will not pretend otherwise. Running it on a schedule and deduplicating on `postId` builds deep history over time, one cheap run at a time.

**What happens if a Page is blocked or does not exist?**

You get one uncharged diagnostic row for that Page explaining which of the two it was, and the run carries on to your other Pages. You are never billed for a Page that returned nothing.

**Does the date filter cost extra?**

No. It is a plain input field, it runs before anything is billed, and posts it removes are never charged. Filtering to last week is strictly cheaper than not filtering.

**Will the run fail if something goes wrong?**

No. A blocked, empty or broken target produces an uncharged diagnostic row and the run still finishes as succeeded. A failed run would still bill you the start fee, which would mean paying to be told something went wrong.

**Can I run this on a schedule?**

Yes, and it is the intended way to use it. Nothing in the run holds state between runs. Schedule the same Page list daily and deduplicate on `postId` to see only new posts.

**How do I get exactly the rows I paid for?**

Filter the dataset on `"charged": true`. Sample and diagnostic rows are always `false`, and the number of charged rows always equals the number of billed events.

**Do I need to supply a proxy?**

No. The run brings its own egress and its cost is already inside the price you see. The `proxyUrls` field exists only for callers who specifically want traffic to leave through servers they own.

# Actor input Schema

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

Public Facebook Page URLs, one per line. A plain handle (NASA) or a profile.php?id= URL works too. Up to 50 Pages per run.

## `resultsLimit` (type: `integer`):

How many recent posts to return for each Page, newest first. The public Page render carries the 20 most recent posts, so 20 is both the default and the ceiling. Keep it low while you are testing - you pay per post.

## `onlyPostsNewerThan` (type: `string`):

Optional. Drop posts older than this. Accepts a date (2026-08-01), a full timestamp, or a relative window such as "7 days". Filtered-out posts are never charged. It filters within the recent posts a Page exposes publicly - it does not reach further back in time.

## `onlyPostsOlderThan` (type: `string`):

Optional. Drop posts newer than this. Same formats as above. Filtered-out posts are never charged.

## `proxyUrls` (type: `array`):

Leave this empty. By default the run rotates a large pool of addresses that cost you nothing per gigabyte. Fill it in only if you specifically want the traffic to leave through proxy servers you already pay for, in the form http://user:pass@host:port.

## `sessionCookies` (type: `array`):

Leave this empty unless you need it. Runs are logged out by default and that is enough for public Facebook content. Facebook shows some things only to a signed-in account, and it limits how fast any one account may read; supplying your own cookie uses your account and your own rate limit, shared with nobody. In Chrome: open facebook.com while signed in, press F12, then Application > Cookies > https://www.facebook.com, and paste the values as "c\_user=<value>; xs=<value>". One line per account. Treat these like a password: anyone with them can act as that account, and Facebook may sign the session out or restrict the account for automated use.

## Actor input object example

```json
{
  "startUrls": [
    "https://www.facebook.com/NASA",
    "https://www.facebook.com/BBCNews"
  ],
  "resultsLimit": 20
}
```

# Actor output Schema

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

Every row in the default dataset: inputUrl, pageName, pageId, pageUrl, pageIsVerified, pageFollowers, pageLikes, pageProfilePicture, postId, postUrl, text, textLength, time, timestamp, reactionsCount, topReactions, commentsCount, sharesCount, postType, media, mediaCount, isReel, isSponsored, linkUrl, linkTitle. An empty, blocked or unmatched run returns a single uncharged row explaining what happened instead.

# 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",
        "https://www.facebook.com/BBCNews"
    ],
    "resultsLimit": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/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",
        "https://www.facebook.com/BBCNews",
    ],
    "resultsLimit": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/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",
    "https://www.facebook.com/BBCNews"
  ],
  "resultsLimit": 20
}' |
apify call dami_studio/facebook-posts-scraper --silent --output-dataset

```

## MCP server setup

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