# Facebook Page Posts Scraper (`simple.actors/facebook-page-posts`) Actor

Scrape the latest posts from any public Facebook page: full text, date, reactions broken down by type, comments, shares, video views, photo and video URLs, hashtags, links and top comments. 52 fields per post. No login, no cookies, no browser. Unofficial.

- **URL**: https://apify.com/simple.actors/facebook-page-posts.md
- **Developed by:** [Simple Actors](https://apify.com/simple.actors) (community)
- **Categories:** Social media
- **Stats:** 1 total users, 1 monthly users, 96.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 posts

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

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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 Page Posts Scraper

Give it a public Facebook page URL and get back its **latest posts** — full
text, publish date, reactions, comments, shares, photos and video, newest first.
52 fields per post.

**$1 per 1,000 posts**, plus $0.003 per run — with Apify platform usage
included. A typical run reads a page's latest 8 posts for **$0.011**, and a run
that legitimately finds nothing costs only the run fee.

Built for the common case: the last handful of posts. One run is **two
requests** and finishes in seconds.

No login, no cookies, no account of yours involved. It reads the same post data
Facebook already server-renders for logged-out visitors, so the login box you see
on the page is never in the way.

*Unofficial. Not affiliated with, endorsed by, or sponsored by Facebook or Meta.*

### What you can do with it

- **Archive a page's posts** — keep a dated, linkable record of what an
  organisation published, in a format you can search.
- **Watch a page for new posts** — run it on a schedule and diff on `postId` to
  catch anything new.
- **Follow announcements** — city governments, utilities, schools and clubs post
  outage notices, closures and alerts to Facebook first.
- **Build a text dataset** — full post bodies for search, tagging or analysis.

### How to use it

Paste one or more page URLs:

```json
{
  "startUrls": [{ "url": "https://www.facebook.com/CoJCTN" }]
}
```

That is the whole input. The actor loads the page and makes a **single** posts
request, and returns however many posts that request gives back — usually about
8\. There is no count to tune and nothing to page through.

`maxPosts` is optional and only ever trims: set `5` if five is all you want.
Raising it fetches nothing extra, because there is no second request.

Vanity URLs (`facebook.com/CoJCTN`), numeric ones
(`facebook.com/profile.php?id=100066882550448`) and `m.facebook.com` links all
work — they get normalised for you.

### What you get

One dataset item per post — the full text, when it was posted, how it performed,
and every photo or video it carries:

```json
{
  "facebookUrl": "https://www.facebook.com/CoJCTN",
  "postId": "1369546808618058",
  "storyId": "UzpfSTEwMDA2Njg4MjU1MDQ0ODoxMzY5NTQ2ODA4NjE4MD…",
  "url": "https://www.facebook.com/reel/946342075161908/",
  "postType": "video",
  "title": "Assistant City Manager Nick Geis provides information about the City's Stormwater Division and recent rain events in this video",
  "text": "Assistant City Manager Nick Geis provides information about the City's Stormwater Division and recent rain events in this video. T…",
  "hashtags": [],
  "links": [],
  "time": "2026-08-12T20:58:11.000Z",
  "timestamp": 1786568291,
  "timeCreated": "2026-08-12T20:58:11.000Z",
  "timestampCreated": 1786568291,
  "likes": 101,
  "topReactions": [
    {
      "type": "Like",
      "count": 93
    },
    {
      "type": "Love",
      "count": 4
    },
    {
      "type": "Haha",
      "count": 2
    },
    {
      "type": "Wow",
      "count": 1
    },
    {
      "type": "Angry",
      "count": 1
    }
  ],
  "topReactionsCount": 5,
  "reactionLikeCount": 93,
  "reactionLoveCount": 4,
  "reactionHahaCount": 2,
  "reactionWowCount": 1,
  "reactionSadCount": 0,
  "reactionAngryCount": 1,
  "reactionCareCount": 0,
  "comments": 14,
  "shares": 10,
  "viewsCount": 4027,
  "videoPostViewCount": 4027,
  "liveViewerCount": null,
  "isVideo": true,
  "isShare": false,
  "sharedPost": null,
  "paidPartnership": false,
  "media": [
    {
      "url": "https://www.facebook.com/reel/946342075161908/",
      "sdUrl": "https://video.fmad8-1.fna.fbcdn.net/o1/v/t2/f2/m412/AQPql-…",
      "hdUrl": "https://video.fmad8-1.fna.fbcdn.net/o1/v/t2/f2/m366/AQMeSx…",
      "thumbnailUrl": "https://scontent.fmad7-1.fna.fbcdn.net/v/t15.5256-10/77312…",
      "durationMs": 83797,
      "isLive": false,
      "captionsUrl": "https://scontent.fmad7-1.fna.fbcdn.net/v/t39.2093-6/773744…",
      "publishedAt": "2026-08-12T20:57:35.000Z",
      "id": "946342075161908"
    }
  ],
  "imageCount": 0,
  "videoCount": 1,
  "images": [],
  "videos": [
    {
      "url": "https://www.facebook.com/reel/946342075161908/",
      "sdUrl": "https://video.fmad8-1.fna.fbcdn.net/o1/v/t2/f2/m412/AQPql-…",
      "hdUrl": "https://video.fmad8-1.fna.fbcdn.net/o1/v/t2/f2/m366/AQMeSx…",
      "thumbnailUrl": "https://scontent.fmad7-1.fna.fbcdn.net/v/t15.5256-10/77312…",
      "durationMs": 83797,
      "isLive": false,
      "captionsUrl": "https://scontent.fmad7-1.fna.fbcdn.net/v/t39.2093-6/773744…",
      "publishedAt": "2026-08-12T20:57:35.000Z",
      "id": "946342075161908"
    }
  ],
  "link": "http://johnsoncitytn.org/",
  "textReferences": [
    {
      "type": "ExternalUrl",
      "id": "NjQyMTgzOTU5MjA4MTA3Omh0dHBcYS8vam9obnNvbmNpdHl0bi5vcmcvOjpEZWZhdWx0Ojo6MTM2OTU0NjgwODYxODA1ODo=",
      "url": "https://l.facebook.com/l.php?u=http%3A%2F%2Fjohnsoncitytn.org%2F&h=AUBHhU73r3VsNDED12CG04aEHyH2l9n-I8a-pvaVp_R-qUALOBrliucOL3dbyBm5_miUUKNE08QGK0Kietp3S-ngPvikwPVCBLV_wDmBrF_OlSEMBtANW9eg79D3_y4kOZU9dG8bG_wphctPgRp5asZVt2Ib2zVP&s=1",
      "externalUrl": "http://johnsoncitytn.org/",
      "mobileUrl": "https://lm.facebook.com/l.php?u=http%3A%2F%2Fjohnsoncitytn.org%2F&h=AUDb75f2i4MOL0iRZzTtSvA_Kk_mq4NvS2OXjLo1sRG-nFzZhQmis6_xkA1ZlNJ8RE0V1KYVBa5W13d9MDWyp3ykghtSdiYlGw3ThgZJfgLO6lKNZFQ2_ckD3JusbfKCQjRbwqIo-J-MYpq6qFvqCsabwNxN7KBn&s=1",
      "offset": 167,
      "length": 17
    }
  ],
  "collaborators": [],
  "actionLink": {
    "type": "MMEMessengerActionLink",
    "url": null
  },
  "previewTitle": null,
  "previewDescription": null,
  "previewSource": null,
  "previewTarget": null,
  "topComments": [
    {
      "author": "Tom McCormack",
      "authorId": "pfbid02vuXJVG7EBSwLbibmGki4uAxXWEoUksYp7HCXxDs…",
      "text": "There are other parts of Johnson City that need to be protected from flooding. Are you looking at protecting those areas also?",
      "createdAt": "2026-08-12T23:16:13.000Z"
    }
  ],
  "feedbackId": "ZmVlZGJhY2s6MTM2OTU0NjgwODYxODA1OA==",
  "pageName": "Johnson City, TN - City Government",
  "pageId": "100066882550448",
  "user": {
    "id": "100066882550448",
    "name": "Johnson City, TN - City Government",
    "profileUrl": "https://www.facebook.com/100066882550448",
    "profilePic": "https://scontent.fmad8-1.fna.fbcdn.net/v/t39.30808-1/46072…"
  },
  "pageProfilePicture": "https://scontent.fmad8-1.fna.fbcdn.net/v/t39.30808-1/46072…",
  "scrapedAt": "2026-08-19T08:38:43.421Z"
}
```

Field names follow the vocabulary the established Facebook post scrapers use —
`time`/`timestamp`, `likes`/`comments`/`shares`, `media`, `textReferences` — so
you can point existing code at this actor without rewriting how you read the
results. The fields it adds on top (`postType`, `hashtags`, `imageCount`, and
the `images`/`videos` split alongside the combined `media`) keep their own
names.

`postType` is one of `video`, `photo`, `link` or `text`. A post that shares a
URL is a `link` post and carries the preview card in `previewTitle`,
`previewSource` and `previewDescription`, with the real destination in `link` —
unwrapped from the `l.facebook.com` redirect Facebook rewrites outbound links
into.

`textReferences` lists what Facebook marks up inside the text — hashtags,
mentions, events and external links — each with its type, ID, URL and character
offset, so you can find them in `text` without re-parsing it. Reactions come
both as `topReactions` and as one column per type (`reactionLikeCount`,
`reactionLoveCount`, `reactionHahaCount`, `reactionWowCount`, `reactionSadCount`,
`reactionAngryCount`, `reactionCareCount`) for loading straight into a
spreadsheet or a table. Photo posts carry
`images` with dimensions and the Facebook photo page; video posts carry `videos`
with separate SD and HD URLs, a thumbnail, duration and a captions track when
one exists.

The dataset ships four views: **Posts** (what it says and how it did),
**Engagement** (reactions, comments, shares and views for ranking or trend
work), **Media** (posts with photos or video and their URLs), and **Posts with
full text**.

Set `includeRaw` to attach Facebook's untouched post object under `raw` when you
need a detail that has no named field.

#### Things worth knowing about the numbers

- **`topComments` is not the comment thread.** Facebook ships a couple of
  comments alongside the feed and this actor passes those through, so a post
  with `commentCount: 14` will still usually carry zero or one entry here. Use
  `commentCount` for the real total.
- **Every post has a posted time.** `time` and `timestamp` are always
  present — a post without one fails the run rather than arriving with a null,
  so you can sort and bucket on it without guarding. A share also carries the
  original's own time in `timeCreated`, and a video carries its upload
  time in `videos[].publishedAt`; both can be earlier than the post itself.
- **A shared post reports zero engagement.** When `isShare` is true, the
  reactions, comments and shares belong to the original story, not to the share,
  and Facebook returns the sharer's own counters as empty. `sharedPost` names the
  original and links to it — read the engagement there.
- **Video URLs expire.** `sdUrl` and `hdUrl` are signed and time-limited, good
  for hours rather than days. Download what you need soon after the run; the
  permalink in `url` keeps working.

#### About `title`

A Facebook post has no title of its own — it is just a body of text. `title`
here is Facebook's own SEO headline for the post, which is what search engines
and link previews show. For the occasional post Facebook generates no headline
for, `title` falls back to the post's opening line — and for a photo posted with
no caption at all, to Facebook's own description of the picture, so the field is
never blank. Either way it is capped at
120 characters, so its length does not depend on which source it came from.
`text` is always the complete, untruncated body.

#### When a page cannot be read

A URL that cannot be read does not fail the run and does not disappear — it
comes back as its own row, so one bad page never costs you the pages that
worked:

```json
{
  "facebookUrl": "https://www.facebook.com/somepage",
  "url": "https://www.facebook.com/somepage",
  "error": "not_available",
  "errorDescription": "… is not publicly visible — Facebook reports \"This content isn't available\". The page may be private, restricted, or removed; only public pages can be read without a login.",
  "scrapedAt": "2026-08-19T16:02:11.004Z"
}
```

`error` is one of `not_available` (private, restricted or deleted),
`not_found`, or `read_failed` (Facebook refused the read, worth retrying).
Rows carrying an `error` are not charged as posts.

### What an empty result means

An empty dataset means the page genuinely has no public posts — not that
something went wrong.

That holds because a broken read **fails the run** instead of finishing quietly:
a page that does not exist, a challenge or consent interstitial served instead of
the page, a rate limit, or retries running out all raise an error. So you can
trust an empty result, and treat a failed run as something to retry.

### Call it from the API

```bash
curl -s "https://api.apify.com/v2/acts/simple.actors~facebook-page-posts/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"startUrls": [{"url": "https://www.facebook.com/CoJCTN"}]}'
```

### Settings

- **`maxPosts`** — how many of the latest posts to return, newest first.
  Defaults to 8, the number one request to Facebook normally supplies. It is a
  target rather than a cap: if a batch comes back short, the run asks for more
  instead of returning fewer. A page that simply has fewer posts returns
  everything it has.
- **`proxy`** — defaults to Apify **residential** proxy, and needs to. Facebook
  serves the page itself to anyone, but answers the posts request from a
  datacenter IP with a rate-limit error rather than posts, so a datacenter run
  fails instead of returning a short result. A run moves under half a megabyte,
  which puts the residential bandwidth at roughly a third of a cent.
- **`onlyPostsNewerThan`** — keep only posts after a window like `20 hours`,
  `last 3 days` or `90 minutes`, or an ISO date such as `2026-08-01`. Minutes,
  hours, days, weeks and months all work, with or without a leading "last".
  Ideal for incremental polling: ask for a window shorter than the
  gap between your runs and you get exactly what is new, or a failed run telling
  you the gap grew too large to answer honestly.
- **`includeRaw`** — attach Facebook's untouched post object under `raw`.
  Useful for a field with no named equivalent; it makes items much larger.
- Runs are capped at 256 MB.

### Limits

- **Public pages only.** A private page, or a personal profile that is not
  public, returns nothing — there is no session to log in with.
- **The latest 8 posts, not the archive.** One request to Facebook returns about
  eight posts and asking again returns the same ones with a fresh cursor, so
  eight is a ceiling rather than a default — `maxPosts` above 8 is rejected
  rather than quietly under-delivered. Run the actor on a schedule to follow a
  page over time instead of trying to reach further back in one run.
- **The newest post is always included.** Every run loads the page fresh, and
  Facebook serves it `no-store`, so nothing is cached between runs. Results are
  sorted by date, so a pinned older post cannot displace a recent one.
- **Date filtering only within the window a run reads.** `onlyPostsNewerThan`
  keeps posts published after a window like `20 hours` or an ISO date. Because a
  run reads only the latest 8 posts, a cutoff reaching further back than those
  posts do **fails the run** rather than returning a partial answer — so an
  empty result always means "nothing new since then", never "we did not look
  that far". There is no filter for *older* posts: reaching back past the latest
  batch is not something one request can do.
- **`topComments` is a sample, not the thread**, and video URLs expire within
  hours. See the notes above.

### Note

This actor is **not affiliated with, endorsed by, or sponsored by Facebook or
Meta**. It reads publicly visible pages without logging in. Meta's Terms of
Service restrict automated collection from its properties, so review them and
your own obligations before using it, and run it at a volume and rate you are
comfortable defending.

# Actor input Schema

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

Public Facebook pages to read, e.g. https://www.facebook.com/CoJCTN. Vanity URLs and profile.php?id=... both work, as do m.facebook.com links — they are normalised for you. Private pages and personal profiles that are not public will not return posts.

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

How many of the latest posts to return, newest first. Defaults to 8, which is what one request to Facebook returns. Asking for more is not possible: a second request comes back with the same posts, so 8 is the ceiling. Lower it if you only want the newest few.

## `proxy` (type: `object`):

Defaults to Apify residential proxy, which is required: Facebook answers /api/graphql/ from a datacenter IP with a rate-limit error instead of posts, so a datacenter run fails. A run moves under half a megabyte, so the residential bandwidth cost stays around a third of a cent.

## `includeRaw` (type: `boolean`):

Attach Facebook's untouched post object under `raw`. Turn this on when you need a detail that has no named field; it makes each item much larger, so leave it off otherwise.

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

Keep only posts published after this point. Takes a window like "20 hours", "last 3 days" or "90 minutes", or an ISO date such as 2026-08-01. Because a run reads only the page's latest posts, the run fails rather than returning a partial answer if the window reaches further back than those posts do — so an empty result always means "nothing new", never "we did not look".

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.facebook.com/CoJCTN"
    }
  ],
  "maxPosts": 8,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "includeRaw": false,
  "onlyPostsNewerThan": "20 hours"
}
```

# Actor output Schema

## `posts` (type: `string`):

One row per post with its engagement counts.

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

Reactions, comments, shares and views per post.

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

Posts carrying photos or video, with media URLs.

## `full` (type: `string`):

Every field, including the complete post body.

## `raw` (type: `string`):

Every field of every item, exactly as pushed.

# 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": [
        {
            "url": "https://www.facebook.com/CoJCTN"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("simple.actors/facebook-page-posts").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": [{ "url": "https://www.facebook.com/CoJCTN" }] }

# Run the Actor and wait for it to finish
run = client.actor("simple.actors/facebook-page-posts").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": [
    {
      "url": "https://www.facebook.com/CoJCTN"
    }
  ]
}' |
apify call simple.actors/facebook-page-posts --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,simple.actors/facebook-page-posts"
        }
    }
}

```

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/k7ogj3wHdrVwawraU/builds/a62ahnhU87F9jjuKa/openapi.json
