# Facebook Pages & Recent Posts Scraper (`scraper-engine/facebook-pages-scraper`) Actor

Facebook Page Scraper collects data from Facebook pages, including posts, followers, page info, and engagement metrics. It automates public page extraction for research, analytics, or marketing insights, exporting clean JSON or CSV data for easy integration into workflows.

- **URL**: https://apify.com/scraper-engine/facebook-pages-scraper.md
- **Developed by:** [Scraper Engine](https://apify.com/scraper-engine) (community)
- **Categories:** Social media, Lead generation, Other
- **Stats:** 305 total users, 2 monthly users, 95.3% runs succeeded, 1 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

$19.99/month + usage

To use this Actor, you pay a monthly rental fee to the developer. The rent is subtracted from your prepaid usage every month after the free trial period. You also pay for the Apify platform usage, which gets cheaper the higher Apify subscription plan you have.

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

## 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 Pages Scraper — Followers, Contacts and Recent Posts

Facebook Pages & Recent Posts Scraper extracts public Facebook Page profiles into structured JSON — Page name, category, follower and like counts, intro text, email, phone, address, website and linked Instagram — and, when you switch on **Fetch recent posts**, adds each Page's latest posts as separate child rows with post text, permalink, publish time, a seven-type reaction breakdown, comment and share counts, and posting-cadence metrics on the parent row. Paste Page URLs or bare usernames, no login, no HTML parsing.

⚠️ **Three things worth knowing before your first run.** `maxPagesToScan` defaults to `10` and **truncates your URL list** — paste 50 Pages and only the first 10 are scraped. `pageUrls` is prefilled in the Apify Console with a sample Page, so a Console run you never edit will scrape that sample; an API call that omits `pageUrls` fails outright instead. And unlike most scrapers here, a Page that could not be fetched still gets a row, and that row **is charged** — filter it out with `success !== false`.

### What is Facebook Pages & Recent Posts Scraper?

Facebook Pages & Recent Posts Scraper is an Apify Actor that reads a public Facebook Page the way a logged-out visitor sees it, and returns two record types in one dataset: one **page** row per Page with its full public profile block, and — optionally — one **post** row per recent post from that Page's timeline. It resolves each Page's numeric id and Facebook's own timeline GraphQL operation from the live page on every run, so nothing is hard-coded against Facebook's markup.

No Facebook account, login, cookie or session is used anywhere in the run. The only credential you need is your Apify token.

It is built for lead researchers building prospect lists from public business Pages, social media analysts benchmarking competitor Pages, and developers and AI engineers piping Page profiles and post engagement into dashboards, RAG indexes or agent tools.

### What Facebook Page and post data is publicly available to scrape?

Facebook renders a public Page to logged-out visitors: the Page name, category, intro, follower and like counts, whatever contact details the Page owner chose to publish, and the top of the timeline with each post's text, permalink, time and engagement counters. What sits behind the login wall is *who* engaged, and anything a Page admin sees.

| Data Category | Publicly Available | Gated behind |
| ----- | ----- | ----- |
| Page name, category, intro/about text, profile and cover photo | ✅ Public | — |
| Follower count and like count, as Facebook displays them | ✅ Public | — |
| Contact block — email, phone, address, website, linked Instagram | ✅ Public when the Page owner publishes it | — |
| Recommendation percentage and review count | ✅ Public when the Page enables recommendations | — |
| Recent post text, permalink and publish time | ✅ Public | — |
| Per-post reaction totals, comment count and share count | ✅ Public | — |
| Comment text and commenter identities | ❌ Not returned — counts only, and not part of the timeline feed this Actor reads | A dedicated comments scraper |
| Identities of the people who reacted to or shared a post | ❌ | Logged-in Facebook account |
| Page insights — reach, impressions, click-throughs | ❌ | Page admin access |

Facebook Pages & Recent Posts Scraper only returns publicly visible data — what any logged-out visitor sees on the Page. Nothing behind a login wall.

### ⚠️ What "recent posts" actually means here

"Recent" is **not** a date window, and it is not a fixed number of days. It is the top of the Page's timeline, newest first, cut off by the count you set.

With `fetchPosts` on, the Actor bootstraps the Page's timeline feed and then pages through it with Facebook's own cursor, collecting **`maxPostsPerPage` posts per Page** (default `25`, schema range 1–5,000) in batches of eight posts per request. It stops at whichever comes first:

- your `maxPostsPerPage` count is reached — **the Actor's** limit;
- Facebook stops returning a next cursor, i.e. the public feed will not go deeper — **Facebook's** limit;
- the Actor's own pagination guard of **300 timeline requests per Page** trips. At eight posts per request that is an effective ceiling of roughly 2,400 posts per Page, below the 5,000 the schema accepts — **the Actor's** limit;
- with `onlyPostsNewerThan` set, a whole batch comes back older than your date floor, at which point paging stops early — **the Actor's** limit.

So on default settings, "recent" means *the newest 25 posts Facebook will serve to a logged-out visitor*. Set `onlyPostsNewerThan` if you want a genuine time window instead — it is the only input that turns the count into a date-bounded window. How far back that reaches depends entirely on how often the Page publishes.

With `fetchPosts` off — the default — no posts are collected at all and you get Page profile rows only.

### What data can I extract with Facebook Pages & Recent Posts Scraper?

Two row shapes share the dataset, told apart by `type` and `isChild`: a **page** row carries the Page's identity, contact block, audience figures and rating summary; a **post** row carries one recent post's text, timing, media and engagement.

#### Page identity, contact and profile fields

A successful page row carries 41 keys, or 42 when `fetchPosts` is on.

| Field Name | Description |
| ----- | ----- |
| `type` | Always `"page"` on this row shape |
| `isChild` | Always `false` on page rows |
| `facebookUrl` | The normalised Page URL the row was collected from |
| `pageUrl` | Same value as `facebookUrl`, kept as a compatibility alias |
| `title` | The Page's display name |
| `pageName` | First path segment of the URL — the Page slug, not the display name |
| `pageId` | Facebook's numeric id for the Page |
| `facebookId` | Same value as `pageId`, kept as a compatibility alias |
| `categories` | Array of category names as Facebook lists them. `[]` when none was found |
| `category` | First entry of `categories`, or `null` |
| `intro` | The Page's intro / best-description text |
| `about_me` | Object: `text` — the same intro string, in the legacy nested shape |
| `info` | Array built from the Page's meta description, typically the "N likes" fragment and the remainder |
| `email` | Public email from the Page's intro card, when published |
| `phone` | Public phone number, when published |
| `address` | Public street address, when published |
| `addressUrl` | A Bing Maps search URL the **Actor builds** from `address` — not a link Facebook published |
| `websites` | Array of every outbound URL found on the Page — website, Google Maps link and Instagram, de-duplicated and with `l.facebook.com` redirects decoded |
| `website` | Bare domain of the first website found, e.g. `copperkettleyqr.com` |
| `instagram` | Array of objects: `username`, `url` |
| `alternativeSocialMedia` | The linked Instagram URL as a plain string |
| `services` | Services line from the Page's intro card |
| `business_services` | Same value as `services`, kept as a compatibility alias |
| `business_hours` | Hours line from the intro card, or an "Open now" / "Closed now" fragment |
| `priceRange` | Price band normalised to dollar symbols — a `££` Page is rewritten as `$$` |
| `business_price` | Human-readable form: `"Price Range · $$"` |
| `creation_date` | Page creation date. String; format varies with the source — usually `"March 14, 2011"` |
| `ad_status` | `"This Page is currently running ads."` when Facebook exposes it, otherwise `null` |
| `pageAdLibrary` | Object: `is_business_page_active`, `id` — the ids you need for Meta Ad Library lookups |
| `profilePictureUrl` | Direct CDN URL of the profile picture |
| `coverPhotoUrl` | Direct CDN URL of the cover photo |
| `profilePhoto` | A `facebook.com/photo/?fbid=…` link, reconstructed from the profile picture id |
| `scrapedAt` | ISO-8601 UTC timestamp of when the row was built |
| `success` | `true` on a scraped Page, `false` on a Page that could not be fetched |
| `error` | **Only present when `success` is `false`** — the failure message |

#### Page audience, rating and posting-cadence fields

| Field Name | Description |
| ----- | ----- |
| `likes` | Page like count, parsed from the Page's meta description |
| `followers` | Follower count as an integer, expanded from the compact string Facebook displays — `2.6K` becomes `2600`, so it inherits Facebook's own rounding |
| `followings` | How many Pages this Page follows, as an integer, when exposed |
| `rating` | Recommendation summary text, e.g. `"94% recommend (202 Reviews)"` |
| `ratings` | Same value as `rating`, kept as a compatibility alias |
| `ratingOverall` | The recommendation percentage as an integer |
| `ratingCount` | Number of reviews behind that percentage |
| `recentPosts` | **Only present when `fetchPosts` is on.** Object with `postsFetched`, `postsWithDate`, `newestPostAt`, `oldestPostAt`, `postsPerWeek`, `avgGapDays`, `spanDays` |

`recentPosts` is computed from the timestamps of the posts actually collected for that Page, so it describes your sample, not the Page's whole history. `postsPerWeek`, `avgGapDays` and `spanDays` are `null` when fewer than two dated posts were collected — the Actor leaves them empty rather than inventing a cadence from a single post.

#### Recent post fields

Every post row carries the same 21 keys, always present.

| Field Name | Description |
| ----- | ----- |
| `type` | Always `"post"` on this row shape |
| `isChild` | Always `true` on post rows |
| `parentPageId` | Numeric id of the Page the post came from — the join key back to the page row |
| `parentPageUrl` | That Page's `facebookUrl` |
| `parentPageName` | That Page's display name |
| `postId` | Facebook's numeric post identifier. `null` when Facebook did not expose one |
| `postUrl` | Post permalink. `null` when absent |
| `postType` | Attachment class: only ever `"photo"` or `"video"` — see the note below |
| `text` | Full post message. **`null`, not `""`,** on a media-only post |
| `publishedAt` | Publish time as an ISO-8601 UTC string, e.g. `2026-07-22T16:05:11Z` |
| `publishedAtUnix` | The same moment as a Unix timestamp in **seconds** |
| `reactionsTotal` | Total reactions of every type, from Facebook's `reaction_count` |
| `reactions` | Object with all seven keys always present: `like`, `love`, `care`, `haha`, `wow`, `sad`, `angry` |
| `commentsCount` | Total comment count on the post |
| `sharesCount` | Total share count on the post |
| `engagementTotal` | `reactionsTotal + commentsCount + sharesCount`, computed by the Actor |
| `imageUrl` | Direct CDN URL of the post image. `null` on posts without one |
| `videoUrl` | Direct playable video URL. `null` on posts without one |
| `externalUrl` | Outbound link on a link-preview post. `null` otherwise |
| `author` | Object: `id`, `name`, `url`, `profile_picture_url` — empty strings when Facebook did not expose them |
| `scrapedAt` | ISO-8601 UTC timestamp of when the post was parsed |

Three shape rules to code against. First, `postType` has only two values, `"photo"` and `"video"`, and `"photo"` is the fallback — so a **text-only post with no attachment is still labelled `"photo"`**. Treat an empty `imageUrl` and `videoUrl` as the real signal for a text post, not `postType`. Second, the seven `reactions` counts come from Facebook's **top reactions** list for that post, so they do **not** necessarily sum to `reactionsTotal` — a reaction type Facebook did not surface stays at `0`. Use `reactionsTotal` for the true total and the breakdown for the mix. Third, `author.profile_picture_url` is snake\_case inside an otherwise camelCase row; that is the shape as published, kept stable rather than quietly renamed.

Counts are read from each story's **own** feedback node, so a post's reaction, comment and share figures always belong to that exact post — never a Page-wide total leaking into every row.

#### 🤖 Add-on: Need additional Facebook data?

Post rows carry counts, not conversations. **Facebook Posts Scraper** goes far deeper on a single Page or profile timeline — media arrays, feedback ids and video transcripts — when the Page profile block is not what you are after. **Facebook Group Post Scraper** reaches posts inside public groups, which a Page timeline never contains. For discovery rather than a known Page, **Facebook Hashtag Search Scraper** and **Facebook Groups Search Scraper** find content and communities by keyword.

### How does Facebook Pages & Recent Posts Scraper differ from the official Facebook Graph API?

Meta publishes the Graph API, and it is the supported route when you administer the Page in question — versioned fields, documented terms, and access to insights no visitor can see. The gap this Actor fills is third-party access: reading a Page you do not administer requires an approved app and, on most surfaces, permission from the Page owner.

| Feature | Meta Graph API | Facebook Pages & Recent Posts Scraper |
| ----- | ----- | ----- |
| App id, app secret and access token | ✅ Required | ❌ Not required — an Apify token is enough |
| App Review before production access | ✅ Required | ❌ Not required |
| Reading a Page you do not administer | Needs an approved app and, per Meta's docs, the Page owner's permission | ✅ Any public Page URL |
| Page profile block and recent posts together | Separate edges and separate permission scopes | ✅ One run returns both row types |
| Quota and throttling terms | Governed by Meta's published platform limits | Bounded by this Actor's own per-Page request budget |
| Page insights — reach, impressions | ✅ Available to Page admins | ❌ Not returned — not publicly visible |
| Output shape | Meta's versioned Graph schema | Two fixed row shapes, same keys every run |

Every constraint above should be verified against Meta's current published Graph API documentation before you choose — permission scopes, review requirements and platform limits change, and no figures from them are reproduced here.

Use the Graph API when you own or manage the Page, need Meta's contractual guarantees, or want admin-only insight metrics. Use this Actor for competitor, prospect and market research across Pages you have no relationship with.

### How to use Facebook Pages & Recent Posts Scraper

The Actor runs on Apify. Start it from the Apify Console or call it through the Apify API — there is no separate signup and no other key to obtain.

1. Open Facebook Pages & Recent Posts Scraper on Apify and click **Try for free**
2. Replace the prefilled sample in **Facebook page URLs or usernames** (`pageUrls`) with your own targets — full URLs and bare usernames both work, and you can mix them
3. Raise **Max pages to scan** (`maxPagesToScan`) to at least the number of URLs you pasted — the default `10` silently cuts a longer list
4. Switch on **Fetch recent posts** (`fetchPosts`) if you want post rows, and set **Max posts per page** (`maxPostsPerPage`)
5. Optionally set **Only posts newer than** (`onlyPostsNewerThan`) to `2026-01-01` or `3 months` to bound the feed by date
6. Click **Start**, then export the dataset as JSON, CSV, Excel or XML

Pages are processed **sequentially**, one after another with a short pause between them, and each Page's post rows are written straight after its page row, so the dataset reads as page-then-posts, page-then-posts.

#### How to scale to bulk Page extraction

`pageUrls` is a list, so one run can cover as many Pages as you need — plain strings and `{ "url": "..." }` objects are both accepted, which means output from another Actor can usually be fed straight in. Just remember `maxPagesToScan` is the run's real ceiling: it is applied to your list, not to a crawl, so it must be raised to match. `maxPostsPerPage` then applies **per Page**, not per run — twenty Pages at 25 posts is a ceiling of 500 post rows plus 20 page rows.

Input URLs are **not** de-duplicated. The same Page listed twice is fetched twice and billed twice, so de-duplicate your list before you submit it. For a recurring sweep, put the same input on an Apify schedule and attach a webhook to push each finished dataset into your own store.

### What can you do with Facebook Page and post data?

- 📍 **A local-lead researcher** building a restaurant prospect list uses `category`, `email`, `phone`, `address` and `websites` to qualify public business Pages before any outreach, dropping Pages with no published contact route.
- 📊 **A social media analyst** benchmarking a competitor set enables `fetchPosts` and ranks Pages by `recentPosts.postsPerWeek` and `avgGapDays`, separating the accounts that publish constantly from the ones coasting on an old audience.
- 😀 **A brand manager** watching sentiment reads `reactions.angry` and `reactions.sad` against `reactions.love` across a Page's newest posts, catching a negative shift that `reactionsTotal` alone would hide.
- 🕐 **A content strategist** planning a calendar groups `publishedAtUnix` by weekday and hour across the collected posts and correlates each slot with `engagementTotal` to find when that audience actually responds.
- 🤖 **An AI engineer** building a brand-monitoring agent indexes `title`, `intro` and every post's `text` into a vector store, keying each chunk on `parentPageId` and citing `postUrl`, so the agent answers questions against live public Pages rather than a stale export.

Every one of these is callable from an agent framework over the Apify API, since the Actor is a standard HTTP-triggered run.

### How does Facebook Pages & Recent Posts Scraper handle rate limits and blocking?

The Actor does not drive a browser and does not solve CAPTCHAs. It defends against blocking with residential egress, session rotation and layered retries.

**Egress.** Every request goes through **Apify Residential** proxy, always. A fresh residential session URL is drawn before each Page, so one Page's blocked IP does not poison the next.

**Retries on the Page fetch.** A Page gets up to four attempts — the first fetch plus three residential rotations — but rotation is triggered only by a block signal: a response mentioning `429`, `403`, `401`, `blocked`, `rate limit`, `forbidden`, `login` or `captcha`, or a redirect to the login page. Any other error ends that Page after a single attempt. There is a two-second pause between rotations and between Pages.

**Retries on the posts fetch.** Collecting a Page's posts gets three attempts with a new residential session each time, and inside each attempt the underlying HTML and GraphQL requests retry three times of their own with a short escalating backoff. Posts are gathered fully into memory before any row is written, so a mid-collection rotation can never duplicate rows in your dataset.

If a Page still cannot be fetched, the Actor writes a failure row for it — `success: false` and an `error` string — and moves to the next Page. If **every** Page in the run fails, the run itself is marked failed. Proxy access must be enabled on your Apify plan; if the residential proxy cannot be started at all, the run stops immediately rather than falling back to a direct connection.

### ⬇️ Input

Six parameters and **none of them is required by the schema** — which makes the two notes under the table the most important part of this section.

| Parameter | Required | Type | Description | Example Value |
| ----- | ----- | ----- | ----- | ----- |
| `pageUrls` | No | array | Facebook Page targets, one per line. Full links and bare usernames both work; a username is expanded to `facebook.com/{username}`. Plain strings and `{ "url": "..." }` objects are both accepted. A `startUrls` list is accepted as an alias. | `["https://www.facebook.com/copperkettleyqr", "nytimes"]` |
| `maxPagesToScan` | No | integer | How many Page URLs to process this run — counts Pages, not posts. Minimum 1, maximum 10000. Default `10`. | `50` |
| `fetchPosts` | No | boolean | When on, each Page also returns its recent posts as child rows and gains a `recentPosts` cadence object. Default `false` — Page data only. | `true` |
| `maxPostsPerPage` | No | integer | Cap on recent posts collected **per Page**. Minimum 1, maximum 5000. Default `25`. | `100` |
| `onlyPostsNewerThan` | No | string | Keep only posts published on or after this date. Absolute `YYYY-MM-DD`, or relative with a unit — `7 days`, `2 weeks`, `1 month`, `1 year`. Empty means no date limit. | `"2026-01-01"` |
| `proxyConfiguration` | No | object | Accepted for the Console UI only. The Actor always uses Apify Residential — see the note below. | `{"useApifyProxy": true}` |

**Nothing is required, but an empty run does not silently do something.** With no `pageUrls` and no `startUrls`, the Actor fails immediately with `Missing pageUrls: add at least one Facebook page URL or username.` — there is no hidden default Page, and no rows are written or charged. The trap is the other way round: `pageUrls` carries a **Console prefill** of `https://www.facebook.com/copperkettleyqr`, so a Console user who never touches the field starts a run against that sample Page and pays for the row, while an API caller who omits the same field gets a hard failure. The prefill only exists in the Console; the API applies no default.

**`maxPagesToScan` truncates, it does not extend.** It is applied to the list you supplied — the first `maxPagesToScan` entries are scraped and the rest are dropped without a warning row. At its default of `10`, a 50-URL list becomes a 10-URL run. Values outside 1–10,000 are clamped, and a non-numeric value falls back to `10`.

Four more honest notes on the remaining inputs:

- **`proxyConfiguration` is not an override.** The value is read and then discarded: the Actor always requests an Apify **Residential** proxy. Selecting datacenter groups, supplying custom `proxyUrls` or unchecking Apify Proxy changes nothing and does not produce a direct connection. If residential proxy is unavailable on your account, the run fails at startup with a message saying so.
- **`maxPostsPerPage` is a ceiling you may not reach.** It is clamped to 1–5,000, but the Actor's own guard of 300 timeline requests per Page — eight posts each — caps a Page at roughly 2,400 posts regardless of what you type. Facebook's public feed depth and the Page's actual post count usually end collection long before either. A non-numeric value falls back to `100`, not to the schema default of `25`.
- **`onlyPostsNewerThan` is validated even when `fetchPosts` is off.** An unparseable value fails the whole run with `Invalid onlyPostsNewerThan: …` before any Page is fetched. Relative values need a unit word — `7 days`, `2 weeks`, `1 month`, `1 year`; a bare number such as `7` is rejected rather than ignored. When the filter is active, a post whose timestamp could not be read is **dropped**, not kept.
- **`maxPostsPerPage` and `onlyPostsNewerThan` do nothing while `fetchPosts` is `false`.** They are parsed, but with posts off no timeline is ever requested.

#### Example input

```json
{
  "pageUrls": [
    "https://www.facebook.com/copperkettleyqr",
    "https://www.facebook.com/nytimes/",
    "bbcearth"
  ],
  "maxPagesToScan": 50,
  "fetchPosts": true,
  "maxPostsPerPage": 25,
  "onlyPostsNewerThan": "3 months",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

### ⬆️ Output

Two typed, normalized row shapes with stable keys across runs: page rows with 41 keys — 42 when `fetchPosts` is on — and post rows with 21. Post rows for a Page follow that Page's own row, so the dataset reads in page-then-posts order. Export as JSON, CSV, Excel or XML, or read the dataset through the Apify API. With `fetchPosts` on, post rows are also mirrored into a named per-run dataset called `{runId}-posts`, so you can consume posts on their own without filtering.

Three shape rules to code against:

- **Failure rows exist, and they are charged.** A Page that could not be fetched produces `{"facebookUrl": …, "error": "Failed to scrape page after 4 attempts", "success": false, "type": "page", "isChild": false}` — plus a zeroed `recentPosts` object when `fetchPosts` is on. Every profile key is **omitted, not nulled**, so never index a page row blindly. One `row_result` event is charged per row pushed, failure rows included; the mirrored `{runId}-posts` copies are not charged again.
- **The exact filter for real data is `success !== false`.** Do not filter on `success === true`: post rows carry no `success` key at all, so that expression would discard every post. In Python, `[r for r in items if r.get("success") is not False]`.
- **`success: true` on a page row does not mean posts were collected.** If the timeline fetch fails for an otherwise healthy Page, the page row still reports success, `recentPosts.postsFetched` is `0`, and no post rows follow. Check `recentPosts.postsFetched` when you need posts specifically.

#### Example output

A page row with `fetchPosts` on:

```json
{
  "facebookUrl": "https://www.facebook.com/copperkettleyqr",
  "categories": ["Restaurant"],
  "info": [
    "Copper Kettle Restaurant, Regina, SK. 2,384 likes",
    "1,205 talking about this · 3,410 were here"
  ],
  "likes": 2384,
  "priceRange": "$$",
  "title": "Copper Kettle Restaurant",
  "address": "1953 Scarth St, Regina, SK, Canada",
  "pageId": "100063568041234",
  "pageName": "copperkettleyqr",
  "pageUrl": "https://www.facebook.com/copperkettleyqr",
  "intro": "Family-run restaurant in downtown Regina serving pizza, pasta and Greek classics since 1976.",
  "websites": [
    "http://www.copperkettleyqr.com/",
    "https://www.instagram.com/copperkettleyqr"
  ],
  "phone": "+1 306-525-3545",
  "email": "hello@copperkettleyqr.com",
  "alternativeSocialMedia": "https://www.instagram.com/copperkettleyqr",
  "website": "copperkettleyqr.com",
  "services": "Dine-in · Takeout · Delivery",
  "rating": "94% recommend (202 Reviews)",
  "followers": 2600,
  "followings": null,
  "profilePictureUrl": "https://scontent-ord5-2.xx.fbcdn.net/v/t39.30808-1/302145678_436705561807015_5583912047_n.jpg",
  "coverPhotoUrl": "https://scontent-ord5-2.xx.fbcdn.net/v/t39.30808-6/419883012_899104455112307_1120448371_n.jpg",
  "profilePhoto": "https://www.facebook.com/photo/?fbid=436705561807015",
  "ratingOverall": 94,
  "ratingCount": 202,
  "category": "Restaurant",
  "addressUrl": "https://www.bing.com/maps/default.aspx?v=2&pc=FACEBK&mid=8100&where1=1953+Scarth+St%2C+Regina%2C+SK%2C+Canada&FORM=FBKPL1&mkt=en-US",
  "instagram": [
    {
      "username": "copperkettleyqr",
      "url": "https://www.instagram.com/copperkettleyqr"
    }
  ],
  "ratings": "94% recommend (202 Reviews)",
  "business_hours": "Open now",
  "business_price": "Price Range · $$",
  "business_services": "Dine-in · Takeout · Delivery",
  "creation_date": "March 14, 2011",
  "ad_status": "This Page is currently running ads.",
  "about_me": {
    "text": "Family-run restaurant in downtown Regina serving pizza, pasta and Greek classics since 1976."
  },
  "facebookId": "100063568041234",
  "pageAdLibrary": {
    "is_business_page_active": true,
    "id": "100063568041234"
  },
  "scrapedAt": "2026-07-25T09:14:22.481Z",
  "success": true,
  "type": "page",
  "isChild": false,
  "recentPosts": {
    "postsFetched": 25,
    "postsWithDate": 25,
    "newestPostAt": "2026-07-22T16:05:11Z",
    "oldestPostAt": "2026-04-03T18:30:02Z",
    "postsPerWeek": 1.59,
    "avgGapDays": 4.58,
    "spanDays": 109.9
  }
}
```

The first post row that follows it:

```json
{
  "type": "post",
  "isChild": true,
  "parentPageId": "100063568041234",
  "parentPageUrl": "https://www.facebook.com/copperkettleyqr",
  "parentPageName": "Copper Kettle Restaurant",
  "postId": "1198437265510934",
  "postUrl": "https://www.facebook.com/copperkettleyqr/posts/pfbid02rTk9Ye7mQwLdX4sVbnH3pKcAqZ1uWgRf8yNjM6xEoTs",
  "postType": "photo",
  "text": "Patio season is officially open. Doors at 11, kitchen until 10, and the Greek platter is back on the board for the summer.",
  "publishedAt": "2026-07-22T16:05:11Z",
  "publishedAtUnix": 1784649911,
  "reactionsTotal": 143,
  "reactions": {
    "like": 96,
    "love": 31,
    "care": 4,
    "haha": 0,
    "wow": 2,
    "sad": 0,
    "angry": 0
  },
  "commentsCount": 18,
  "sharesCount": 6,
  "engagementTotal": 167,
  "imageUrl": "https://scontent-ord5-2.xx.fbcdn.net/v/t39.30808-6/620114873_1198437198844274_7739021845_n.jpg",
  "videoUrl": null,
  "externalUrl": null,
  "author": {
    "id": "100063568041234",
    "name": "Copper Kettle Restaurant",
    "url": "https://www.facebook.com/copperkettleyqr",
    "profile_picture_url": "https://scontent-ord5-2.xx.fbcdn.net/v/t39.30808-1/302145678_436705561807015_5583912047_n.jpg"
  },
  "scrapedAt": "2026-07-25T09:14:41.902Z"
}
```

Note that the seven `reactions` values sum to 133 while `reactionsTotal` is 143 — the breakdown reflects the reaction types Facebook surfaced for that post, so the gap is expected rather than a parsing error.

### How does it work?

Facebook builds a public Page from its own embedded JSON and a GraphQL timeline feed, and that is what this Actor reads. For each target it normalises your entry into a Facebook URL, draws a fresh Apify Residential session, and loads the Page once with a full desktop Chrome header set. From that single HTML response it pulls the Page's structured profile blocks — the description payload and the intro-card context list — and falls back to targeted regex over the raw HTML for fields the JSON does not carry.

If `fetchPosts` is on, it then bootstraps the timeline: the Page's numeric id comes from Facebook's own deep-link meta tags, and the id of the timeline GraphQL operation is lifted from Facebook's live JavaScript bundles on every single run — nothing is hard-coded, so a renamed bundle or a rotated operation id does not break it. It calls that operation directly, following the feed cursor eight posts at a time, and reads each post's engagement from that post's own feedback node.

Only publicly visible data is collected — no account, cookie or session is used anywhere. Because the Actor reads structured feed data rather than rendered markup, a Facebook visual redesign generally does not move your field names.

### Integrations

Facebook Pages & Recent Posts Scraper is an Apify Actor, so it works with anything that can call the Apify API or consume a dataset.

#### Calling Facebook Pages & Recent Posts Scraper from Python

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("<YOUR_USERNAME>/facebook-pages-recent-posts-scraper").call(run_input={
    "pageUrls": ["https://www.facebook.com/copperkettleyqr", "nytimes"],
    "maxPagesToScan": 50,
    "fetchPosts": True,
    "maxPostsPerPage": 25,
    "onlyPostsNewerThan": "3 months",
})

for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if row.get("success") is False:
        continue
    if row["isChild"]:
        print("  post", row["publishedAt"], row["engagementTotal"], (row["text"] or "")[:60])
    else:
        print(row["title"], row["followers"], row.get("email"))
```

Works in Go, Ruby, Node.js, cURL — any language that can make an HTTP request. Guard on `success is not False` before reading profile keys, since failure rows omit them entirely.

#### Scheduling and webhooks

Put the same input on an Apify schedule for a recurring sweep — a weekly pull of the last seven days is `"onlyPostsNewerThan": "7 days"` on a weekly cron. Attach an Apify webhook on run success to have each finished dataset posted to your own endpoint, so new Page data lands in your warehouse without polling.

#### No-code tools (n8n, Make, Zapier)

In n8n, use the Apify node — or an HTTP Request node pointed at the Apify run endpoint with your token — and pass the same JSON input shown above; an Item Lists node then splits page and post rows for downstream steps, and a Filter node on `isChild` separates them cleanly. In Make, the Apify module supports run-and-wait, so a daily competitor monitor can feed Google Sheets, Airtable or Slack directly. Both platforms handle scheduling, so a recurring Page sweep needs no code at all.

### Is it legal to scrape Facebook Pages and posts?

Scraping publicly visible Pages is broadly treated as permissible where no authentication is bypassed, and this Actor returns only public data — what any logged-out visitor sees. **But this output contains personal data, and that changes your obligations rather than the Actor's.**

A Page belonging to an organisation is a business identity; a Page belonging to an individual creator, consultant or public figure is a **person**, and the same fields then carry personal data — which is why you should treat the whole output as personal data unless you have segmented your targets. The identifying fields are specific: on page rows, `title`, `pageName`, `pageId`, `facebookId`, `email`, `phone`, `address`, `intro`, `about_me.text`, `profilePictureUrl`, `coverPhotoUrl`, `instagram` and `alternativeSocialMedia`; on post rows, `author.id`, `author.name`, `author.url`, `author.profile_picture_url`, `parentPageName`, `text`, `postId` and `postUrl`. Under GDPR, the UK GDPR and CCPA, storing or processing those needs a lawful basis you can document — legitimate interest, consent, or another ground — and public availability is not a lawful basis by itself.

Practise data minimisation: drop `email`, `phone`, `address`, the `author` object and `text` first, since they carry the most identifying weight. What remains — `followers`, `likes`, `reactionsTotal`, `commentsCount`, `sharesCount`, `engagementTotal`, `publishedAtUnix` and the `recentPosts` cadence object — supports most engagement analysis with no personal data at all. Set a retention limit rather than keeping rows indefinitely, and be ready to honour access and deletion requests against whatever you store.

Consult legal counsel if your use case involves bulk storage of personal data, building contact lists for outreach, profiling individuals, or re-publishing post text and author identities.

### ❓ Frequently asked questions

#### What Facebook Page fields does Facebook Pages & Recent Posts Scraper return?

The five most used are `title`, `category`, `followers`, `email` and `phone` on the page row, with `text`, `publishedAt`, `reactionsTotal`, `commentsCount` and `engagementTotal` leading the post rows. A page row carries 41 keys — 42 with `fetchPosts` on — and every post row carries 21. See the data fields tables above for all of them.

#### Does Facebook Pages & Recent Posts Scraper require a Facebook account, login or cookie?

No. No account, no cookie, no session, no Facebook app credentials, and none of that is hidden in an optional field either — the Actor sends logged-out requests throughout and reads only what a public Page serves to an anonymous visitor. The one credential you need is your Apify token, and Apify Proxy must be enabled on your plan because residential egress is mandatory.

#### How many Pages and posts can I extract in one run?

`maxPagesToScan` sets the Page ceiling — up to 10,000 — and `maxPostsPerPage` applies per Page, up to 5,000. In practice the post number is bounded twice more: the Actor stops a Page after 300 timeline requests, which at eight posts per request is roughly 2,400 posts, and Facebook's public feed only goes as deep as it chooses to serve. How many posts you actually get depends on how many the Page has published in the window you asked for.

#### What happens if a Page has no recent posts, is unpublished, or is region-restricted?

You still get a page row, but what is in it depends on which layer failed. An unpublished, deleted or region-restricted Page that will not render to a logged-out visitor produces a failure row — `success: false` with an `error` string, every profile key omitted — after four attempts, and the run continues to the next Page. A healthy Page that simply has no posts in your window produces a normal page row with `success: true`, `recentPosts.postsFetched: 0`, `postsPerWeek: null`, and no post rows following it. The same shape appears when the Page loads fine but its timeline cannot be read, so branch on `recentPosts.postsFetched` rather than on `success` when posts are what you need. If every Page in the run fails, the run itself is marked failed.

#### Can I scrape multiple Facebook Pages at once?

Yes. `pageUrls` is a list — add as many Pages as you like, mixing full URLs and bare usernames freely, including usernames containing dots, which are expanded verbatim to `facebook.com/{username}`. Two caveats: raise `maxPagesToScan` above its default of `10` or the list is truncated, and de-duplicate your list first, because a repeated URL is scraped and billed twice.

#### Does Facebook Pages & Recent Posts Scraper return comment text or the names of people who reacted?

No — it returns counts. `commentsCount` and `sharesCount` are totals, and `reactions` breaks reactions into seven types, but comment text, commenter names, sharer identities and reactor identities are not part of the timeline feed this Actor reads. Collect `postUrl` and `postId` from the rows and pass them to a dedicated Facebook comments Actor when you need the underlying conversation.

#### Does Facebook Pages & Recent Posts Scraper work with Claude, ChatGPT and other AI agent tools?

Yes. It is callable as a standard HTTP endpoint through the Apify API, so LangChain, CrewAI, n8n or a hand-written tool definition can invoke it and receive typed JSON with no parsing step.

#### How does Facebook Pages & Recent Posts Scraper compare to other Facebook Page scrapers?

Checked on the Apify Store on 25 July 2026: `danek/facebook-pages-posts-ppr` is the most used of the alternatives and is posts-only — its listing documents `post_id`, `url`, `message`, `timestamp`, `comments_count`, `reactions_count`, `author`, `image`, `video` and `attached_post_url`, with no per-type reaction breakdown, no share count and no Page profile record; it states pricing of "$2.99 per 1k results", that the Actor is "only for paid users" with free users limited to 2 results, and that no proxy is needed. `powerful_bachelor/Facebook-Posts-Scraper` describes page bio, category, followers and engagement metrics in prose, but its listing documents no input parameters and no output field names. `pear_fight/facebook-scraper` is Playwright-driven, documents `pageUrls`, `maxPosts` (default 20) and `proxyConfig` as inputs and page name plus post text, date, likes, comments, shares, images and videos as output, described as "either page info or post data" without a field list.

What this Actor documents that they do not: the full contact block as named keys, a seven-type reaction breakdown read from each post's own feedback node, `recentPosts` cadence metrics computed on the parent Page row, exactly which keys are omitted rather than nulled, and the fact that failure rows are written and charged.

#### Does Facebook Pages & Recent Posts Scraper return data in a format LLMs can use directly?

Yes. Every row is typed, normalized JSON with the same field names on every run. No HTML parsing, no selectors. Pass a row straight into an LLM context window, index it into a vector store, or hand it to an agent tool — no transformation step required.

#### What happens when Facebook changes its layout or anti-bot system?

The scraper is maintained, and because it reads Facebook's embedded JSON and structured timeline feed rather than rendered HTML, a visual redesign generally does not affect it. The parts most exposed to change are the bootstrap step — the Page's numeric id and the timeline operation id are rediscovered from the live page and its JavaScript bundles on every run, which is exactly what makes them resilient to renaming — and the anti-bot layer, handled by residential egress, session rotation and backoff. Your field names and types do not change on your end.

#### Can I use Facebook Pages & Recent Posts Scraper without managing proxies or browser infrastructure?

Yes. There is no browser to run — the Actor talks to Facebook over plain HTTP with a full desktop Chrome header set, so there is no Chromium to provision. Proxying is automatic and not optional: Apify Residential is requested for you, with a fresh session per Page and rotation on block signals. You never create a proxy account or rotate an IP. Note that it does not solve CAPTCHAs; it rotates away from them and backs off.

#### Which Facebook Page fields work best for AI training data and RAG indexing?

For RAG indexing: `intro` and `title` describe what the Page is, and each post's `text` is the primary document body — chunk per post, key on `parentPageId`, and cite `postUrl`. For training data: `followers`, `likes`, `reactionsTotal`, `commentsCount`, `sharesCount`, `engagementTotal` and `publishedAtUnix` are the most structurally consistent values across records, since the post row's 21 keys are always present and always the same type. All values come back as typed primitives, arrays or plain nested objects, so no normalization pass is needed before indexing — and if your index does not need identity, drop the `author` object and the contact fields before you store anything.

### 🔗 Related scrapers

| Scraper Name | What it extracts |
| ----- | ----- |
| Facebook Posts Scraper | Deep post extraction from a single Page **or personal profile** timeline — flat post rows with media arrays, feedback ids and optional video transcripts, much higher per-target post limits, but **no Page profile record**. Use it when posts are the whole job; use this Actor when you also want the Page's category, followers and contact block, or cadence metrics |
| Facebook Group Post Scraper | Posts, comments and engagement from public Facebook groups — content a Page timeline never contains |
| Facebook Hashtag Search Scraper | Posts and reels found by hashtag, with reactions and attachments |
| Facebook Video Search Scraper | Public video results by keyword — URL, caption, thumbnail, duration, creator |
| Facebook Groups Search Scraper | Groups by keyword — id, name, privacy, member count, recent activity |
| Instagram Profile Scraper | Instagram profile data, for following the `instagram` link on a Page row cross-platform |

### 💬 Your feedback

Found a bug, or need a field that is in Facebook's Page payload but not in the output? Open an issue on the Actor's Issues tab. Reports that include the exact input JSON and the target Page URL are the fastest to reproduce and fix.

# Actor input Schema

## `pageUrls` (type: `array`):

Full page links or bare usernames, one per line. Bulk lists supported. Examples: https://www.facebook.com/copperkettleyqr or just copperkettleyqr. (Base-compatible: a `startUrls` list is also accepted.)

## `maxPagesToScan` (type: `integer`):

How many page URLs to process this run (counts pages, not posts). Range 1–10,000. Default 10.

## `fetchPosts` (type: `boolean`):

When on, each page also returns its recent posts as separate child records (type=post, isChild=true) with a 7-type reaction breakdown, comments, shares and engagement total, and adds posting-cadence metrics to the page row. Default off (page data only).

## `maxPostsPerPage` (type: `integer`):

Cap on how many recent posts to collect per page. Range 1–5,000. Example: 25 collects up to 25 recent posts for each page. Default 25.

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

Keep only posts published on or after this date. Absolute (YYYY-MM-DD) or relative (e.g. '7 days', '2 weeks', '1 month', '1 year'). Leave empty for no date limit.

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

This actor always runs through Apify RESIDENTIAL proxy — datacenter or custom groups in this field are ignored. On tough pages it retries with fresh residential sessions. Proxy access must be enabled on your Apify plan.

## Actor input object example

```json
{
  "pageUrls": [
    "https://www.facebook.com/copperkettleyqr"
  ],
  "maxPagesToScan": 10,
  "fetchPosts": false,
  "maxPostsPerPage": 25,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

All scraped page and post rows from this run, in the Actor's default dataset.

# 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 = {
    "pageUrls": [
        "https://www.facebook.com/copperkettleyqr"
    ],
    "maxPagesToScan": 10,
    "maxPostsPerPage": 25,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scraper-engine/facebook-pages-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 = {
    "pageUrls": ["https://www.facebook.com/copperkettleyqr"],
    "maxPagesToScan": 10,
    "maxPostsPerPage": 25,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("scraper-engine/facebook-pages-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 '{
  "pageUrls": [
    "https://www.facebook.com/copperkettleyqr"
  ],
  "maxPagesToScan": 10,
  "maxPostsPerPage": 25,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call scraper-engine/facebook-pages-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scraper-engine/facebook-pages-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/ci7Uru5wpRkzTl5gr/builds/King5nk4O2K1cahXg/openapi.json
