# Facebook Page Scraper – Posts, Photos & Details (`simple.actor/facebook-page-posts`) Actor

Scrape any public Facebook page: its latest posts with full text, reactions, comments, shares and media (52 fields each), or the page itself — About block, follower count, photo albums and video library. No login and no cookies. Unofficial.

- **URL**: https://apify.com/simple.actor/facebook-page-posts.md
- **Developed by:** [Simple Actor](https://apify.com/simple.actor) (community)
- **Categories:** Social media, Marketing, Lead generation
- **Stats:** 1 total users, 1 monthly users, 100.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/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 Page Scraper – Posts, Photos & Details

Scrape any public Facebook page two ways from one input. **`mode: "posts"`**
(the default) returns the page's **latest posts** — full text, publish date,
reactions, comments, shares, photos and video, newest first, 52 fields per post.
**`mode: "details"`** returns the **page itself**: its About block with category,
email, phone, website and address, its follower count, profile and cover photo,
its photo albums and its video library. The two read different parts of Facebook
and neither substitutes for the other. Built for social media monitoring, brand
and competitor tracking, lead lists and CRM enrichment.

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.*

### Features

- **Two modes, one input** — latest posts, or the page's own About block, photos
  and videos.
- **52 fields per post** — text, SEO title, hashtags, links, publish time,
  reactions broken out per type, comments, shares, view counts and media.
- **Reaction breakdown as columns** — `reactionLikeCount`, `reactionLoveCount`,
  `reactionHahaCount`, `reactionWowCount`, `reactionSadCount`,
  `reactionAngryCount`, `reactionCareCount`, ready for a spreadsheet.
- **Photos and video separated** — `images` with dimensions, `videos` with
  separate SD and HD URLs, thumbnail, duration and a captions track when one
  exists, plus the combined `media`.
- **Outbound links unwrapped** — the real destination, not the
  `l.facebook.com` redirect Facebook rewrites links into.
- **Drop-in field names** — `time`/`timestamp`, `likes`/`comments`/`shares`,
  `media`, `textReferences`, so existing code reads this Actor unchanged.
- **Page About block for lead data** — category, email, phone, website, address
  and hours, plus `aboutFields` carrying every value the page states with its
  type.
- **Incremental polling** — `onlyPostsNewerThan` takes `20 hours`,
  `last 3 days`, `90 minutes` or an ISO date.
- **URL forms normalised** — vanity, `profile.php?id=…` and `m.facebook.com`
  links all work.
- **One bad page never costs you the good ones** — an unreadable page comes back
  as its own row and the run still succeeds.

### Input

```json
{
  "startUrls": [{ "url": "https://www.facebook.com/CoJCTN" }],  // public page URLs
  "mode": "posts",                  // "posts" for latest posts, "details" for the page itself
  "maxPosts": 5,                    // latest posts to return, newest first (1–8)
  "onlyPostsNewerThan": "20 hours", // optional window: "last 3 days", "90 minutes", "2026-08-01"
  "includeRaw": false,              // attach Facebook's untouched post object under `raw`
  "proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}
```

`startUrls` is the only required field. Everything else has a working default.

| Field | Type | Default | What it does |
|---|---|---|---|
| `startUrls` | array | — | **Required.** Public Facebook page URLs. Vanity, numeric and `m.facebook.com` forms all work. |
| `mode` | string | `posts` | `posts` = one row per post. `details` = one row per page (About, photos, videos). |
| `maxPosts` | integer | `5` | Latest posts to return. Maximum **8** — one request is all Facebook serves. Posts mode only. |
| `onlyPostsNewerThan` | string | — | Keep only posts after a window or ISO date. Posts mode only. |
| `includeRaw` | boolean | `false` | Attach Facebook's raw post object. Makes items much larger. Posts mode only. |
| `includePhotos` | boolean | `true` | Read the Photos tab and album list. Details mode only. |
| `includeVideos` | boolean | `true` | Read the Videos tab. Details mode only. |
| `proxy` | object | residential | Residential is required — Facebook answers the posts request from a datacenter IP with a rate-limit error. |

#### Reading the page itself, not its posts

Set `mode` to `details`:

```json
{
  "startUrls": [{ "url": "https://www.facebook.com/nasa" }],
  "mode": "details"
}
```

That returns **one row per page** instead of one per post:

```json
{
  "resultType": "page-details",
  "pageName": "NASA - National Aeronautics and Space Administration",
  "pageId": "100044561550831",
  "isVerified": true,
  "category": "Government organisation",
  "email": "public-inquiries@hq.nasa.gov",
  "websites": ["http://www.nasa.gov/", "https://www.nasa.gov/nasa-app/"],
  "followersText": "28M followers",
  "followers": 28000000,
  "followersAreExact": false,
  "coverPhoto": "https://scontent.../cover.jpg",
  "aboutFields": [
    { "section": "category", "sectionTitle": "Categories", "type": "category", "value": "Government organisation" },
    { "section": "about_contact_info", "sectionTitle": "Contact info", "type": "profile_email", "value": "public-inquiries@hq.nasa.gov" }
  ],
  "photos": [{ "id": "...", "url": "https://www.facebook.com/photo.php?fbid=...", "image": "https://scontent.../photo.jpg" }],
  "albums": [{ "title": "Cover photos", "url": "https://www.facebook.com/media/set/?set=a...", "coverImage": "..." }],
  "videos": [{
    "url": "https://www.facebook.com/NASA/videos/.../27340367028974495/",
    "title": "NASA's Curious Universe - Roman Series - Episode 2",
    "durationSeconds": 29,
    "time": "2026-08-19T...Z",
    "playCount": 251617,
    "likes": 1606
  }],
  "photoCount": 8, "albumCount": 8, "videoCount": 18
}
```

**`aboutFields` is the field that will not let you down.** Facebook states each
About value with a type — `category`, `profile_email`, `website` — and a page
publishes whichever ones apply to it, so a restaurant carries an address, a
phone and opening hours where a government page carries none of them. The named
keys (`email`, `phone`, `website`, `address`, `category`, `hours`) are a
convenience over `aboutFields`, and `aboutFields` carries **everything** the
page stated, including field types that postdate this Actor. If a named key is
empty, look there before concluding the page does not publish the value.

**Follower counts are Facebook's rounded display figure, and the row says so.**
The page header carries `"28M followers"` and no exact number anywhere, so
`followersText` is kept verbatim, `followers` is parsed from it, and
`followersAreExact` is `false`. Do not trend `followers` — a rounded figure does
not move for months, and then jumps.

One caveat on `hours`: measured on a real page it carries Facebook's rendered
status — `"Closed now"` — rather than a weekly schedule, so read it as a display
string, not as opening times.

`includePhotos` and `includeVideos` each default to `true` and each cost a page
load, so turn them off when you only want the About block — that is the
difference between $0.004 and $0.010 a page, and it is why the two are priced
separately rather than as one number. A real About-only
read of a city government page returned category, address, phone, email,
website and hours in six fields for one page load.

#### Why this is not just "read the latest posts"

The two modes read different parts of Facebook, and neither substitutes for the
other:

- The **About block is not in the post feed** at all. No category, email,
  website, phone or follower count appears on a post.
- The **media on a page's latest posts is not its library**. A page whose recent
  posts are text or links returns no media at all, while its Videos tab still
  holds years of them — NASA's returned 18 videos reaching well past its latest
  8 posts, and the Photos tab includes album and cover photos that were never in
  a post.

### Output

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.

### How to use

#### From Apify Console

1. Open the Actor and click **Try for free** / **Start**.
2. Paste one or more public page URLs into **Facebook page URLs** — vanity
   (`facebook.com/CoJCTN`), numeric (`facebook.com/profile.php?id=…`) and
   `m.facebook.com` links all work.
3. Leave **What to read** on *Latest posts*, or switch it to *Page details,
   photos and videos* to read the About block and media library instead.
4. Raise **Number of posts** to 8 for the full batch Facebook serves in one
   request, or set **Only posts newer than** to poll incrementally.
5. Click **Start**, then open the **Dataset** tab. Four ready-made views ship
   with it — Posts, Engagement, Media and Posts with full text — and you can
   export as JSON, CSV or Excel.

#### From the API

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

Or with the JavaScript client:

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('simple.actor/facebook-page-posts').call({
    startUrls: [{ url: 'https://www.facebook.com/CoJCTN' }],
    maxPosts: 8,
    onlyPostsNewerThan: '24 hours',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

To follow a page over time, schedule the Actor and diff on `postId`.

### Use cases

- **Monitor a Facebook page for new posts** — run on a schedule with
  `onlyPostsNewerThan` set shorter than the gap between runs, and get exactly
  what is new.
- **Track emergency and service announcements** — city governments, utilities,
  schools and clubs post outage notices, closures and alerts to Facebook first,
  often before anywhere else.
- **Competitor and brand monitoring** — pull reactions, comments, shares and
  view counts across rival pages to see which posts actually performed.
- **Lead lists and CRM enrichment** — `mode: "details"` returns a page's
  category, email, phone, website and address straight from the About block.
- **Archive a page's output** — keep a dated, linkable, searchable record of
  what an organisation published, with its media.
- **Build a text dataset** — full, untruncated post bodies for search, tagging,
  sentiment or topic analysis.

### Usage notes

**$1 per 1,000 posts**, plus $0.003 per run — with Apify platform usage
included. A typical run reads a page's latest 5 posts for **$0.008**, and a run
that legitimately finds nothing costs only the run fee. Raise `maxPosts` to 8 for
the full batch Facebook serves, at $0.011.

Page details is priced separately, from **9 September 2026**: **$0.004 per page**
for the About block, plus **$0.006** when photos and videos are read too — so
$4 per 1,000 pages for contact details alone, or $10 per 1,000 for everything.
The media charge only applies when a media tab was actually read, and a page
that could not be read costs nothing either way.

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 read that fails **leaves a row** rather than finishing
quietly: a page that does not exist, a private or restricted page, a challenge
or consent interstitial served instead of the page, a rate limit, or retries
running out each come back as a row carrying `error` and `errorDescription`. So
an empty dataset is a real answer, and the error rows — not the run status — are
where you look for failures.

**The run itself succeeds even when every page in it failed.** The one input
that still fails a run is an `onlyPostsNewerThan` value that cannot be parsed as
a window or a date, which is rejected before any page is fetched.

### Settings

- **`maxPosts`** — how many of the latest posts to return, newest first.
  Defaults to **5**; raise it to **8** for the full batch Facebook serves in one
  request. 8 is the ceiling and anything above it is rejected rather than
  silently under-delivered. A smaller number also reaches less far back, so if
  `onlyPostsNewerThan` starts refusing a window it used to accept, raise this.
- **`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 an error row on that
  page 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 at most, 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 `maxPosts` posts, a cutoff reaching further back than those
  posts do returns an **error row for that page** rather than 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.

### FAQ

**Is scraping Facebook legal?**
This Actor reads only pages that are publicly visible to any logged-out visitor,
without logging in. Meta's Terms of Service restrict automated collection from
its properties, so check the platform's ToS and your own obligations before
using it, and run it at a volume and rate you are comfortable defending.

**Do I need a Facebook login, cookies or an API key?**
No. There is no account, session or token involved — it reads the post data
Facebook server-renders for logged-out visitors.

**Does it support pagination or scraping a page's full history?**
No, and that is a hard limit rather than a missing feature. One request to
Facebook returns about eight posts, and asking again returns the same eight with
a fresh cursor, so `maxPosts` is capped at 8. To follow a page over time, run
the Actor on a schedule with `onlyPostsNewerThan` instead of trying to reach
further back in a single run.

**Can it scrape private pages or personal profiles?**
No. A private page, or a personal profile that is not public, returns an
`error` row of `not_available` — there is no session to log in with.

**Why does it need a residential proxy?**
Facebook serves the page HTML to anyone, but answers the `/api/graphql/` posts
request from a datacenter IP with a rate-limit error instead of posts. A
datacenter run therefore returns no posts. A run moves under half a megabyte, so
the residential bandwidth stays small.

**Why does a post with 14 comments only return one in `topComments`?**
Facebook ships a couple of comments alongside the feed and this Actor passes
those through — it is a sample, not the thread. Use `comments` for the real
total.

**How do I get a page's email, phone and address?**
Set `mode` to `details`. The About block is not present in the post feed at all,
so no post carries a category, email, website, phone or follower count.

### 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. All three URL forms work and are normalised for you: vanity URLs (facebook.com/nasa), numeric ones (facebook.com/profile.php?id=100066882550448) and mobile links (m.facebook.com/...). Private pages and personal profiles that are not public cannot be read — they come back as a row with an "error" field rather than failing the run.

## `mode` (type: `string`):

**Latest posts** (the default) returns one row per post, exactly as this Actor always has — leave it alone and nothing about your existing runs changes. **Page details** instead returns one row per page: its About block (category, email, phone, website, address and every other field the page states), its follower count, profile and cover photo, plus its Photos tab, its albums and its Videos tab. The two read different parts of Facebook and neither is a substitute for the other — the About block is not in the post feed, and the media on a page's latest posts is not its photo and video library.

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

How many of the latest posts to return, newest first. Defaults to 5, which is enough to see what is new on a page you poll regularly. Raise it to 8 for the full batch Facebook serves in one request — 8 is the ceiling, because a second request comes back with the same posts, so asking for more cannot be delivered. Note that a smaller number also reaches less far back, so if you use "Only posts newer than" with a long window, raise this to 8. Latest posts mode only; Page details mode returns one row per page and ignores this.

## `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. Latest posts mode only — Page details rows do not carry `raw`, because Facebook's untouched payloads for a page run to several megabytes.

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

Keep only posts published after this point — ideal for incremental polling. Takes 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". Because a run reads only the page's latest posts, a window reaching further back than those posts do returns an error row for that page rather than a partial answer — so an empty result always means "nothing new", never "we did not look". Latest posts mode only.

## `includePhotos` (type: `boolean`):

Page details mode only. Reads the page's Photos tab and its album list. Turn it off to skip a page load and its GraphQL calls when you only want the About block.

## `includeVideos` (type: `boolean`):

Page details mode only. Reads the page's Videos tab — permalink, title, thumbnail, duration, publish date, play count and reactions for each. Turn it off to skip a page load when you only want the About block.

## Actor input object example

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

# Actor output Schema

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

One row per post with its engagement counts.

## `details` (type: `string`):

One row per page: About block, follower count, photos, albums and videos. Produced by the "Page details" mode.

## `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.actor/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.actor/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.actor/facebook-page-posts --silent --output-dataset

```

## MCP server setup

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