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

Facebook posts scraper API for the recent public posts of a Facebook Page. Each post has the full text, exact posting time and permalink, every photo and video, and reaction, comment and share counts, plus the Page identity. Up to 20 posts per Page, 50 Pages per run. $0.65 per 1,000 posts.

- **URL**: https://apify.com/dami_studio/facebook-posts-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Social media, Marketing, Automation
- **Features:** Limited permissions
- **Stats:** 83 total users, 39 monthly users, 98.9% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.62 / 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.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

**Facebook posts scraper for Page links or plain handles, recent public posts only**, one row each:
the full text, the exact posting time, the permalink, every photo and video, and the reaction,
comment and share counts.

The ceiling is the thing to know first: the 20 most recent posts per Page. This actor is built to go wide
instead: 50 Pages in a run, newest posts from each, rather than a thousand posts from one Page.

| | |
|---|---|
| **Input** | Page links or handles, or links to single posts and videos, up to 50 per run |
| **Output** | One row per post: text, posting time, permalink, photos and videos with view counts, reactions by type, comments, shares, the top comment, links and hashtags in the text, Page identity |
| **Ceiling** | 20 posts per Page, newest first |
| **Account needed** | None |
| **Price** | $0.65 per 1,000 posts on the free plan, down to $0.62 on Business. The free plan's $5 a month covers about 7,500 |

### 🔍 What Facebook Posts Scraper does

For every Page you list, it returns the newest posts that Page shows the public, one row per post.
They come back together, so 20 posts from a Page take about as long as 3. Paste the link of one
post or video instead and you get that one post.

Reactions come back two ways. `reactionsCount` is the total, and `topReactions` breaks that same
total down by type, so you can see 3,356 likes against 3 angry without a second request.

The two date fields narrow that recent batch rather than reaching further back. On a Page that posts
daily, asking for last quarter will correctly return nothing, and no post rows are charged.

### 📋 What data you get from each Facebook post

| What you get | Field |
|---|---|
| The post's id and permalink | `postId`, `postUrl` |
| The full text | `text`, `textLength` |
| When it was posted | `time`, `timestamp` |
| Reactions, by type, plus comments and shares | `reactionsCount`, `topReactions`, `commentsCount`, `sharesCount` |
| One column per reaction type | `reactionLikeCount`, `reactionLoveCount`, `reactionCareCount`, `reactionHahaCount`, `reactionWowCount`, `reactionSadCount`, `reactionAngryCount` |
| What kind of post it is | `postType`, `isReel`, `isSponsored` |
| Every photo and video, with alt text, and each photo's width and height | `media`, `mediaCount` |
| Views on a video, live viewers, and a link to the video file | `videoViews`, `liveViewerCount`, `videoUrl` |
| The outbound link on a link post, and the card's description, site and image | `linkUrl`, `linkTitle`, `linkDescription`, `linkSource`, `linkImage` |
| Links, hashtags and tagged accounts in the post text | `textLinks`, `hashtags`, `taggedAccounts` |
| The comment Facebook shows first: text, time, replies, likes, who wrote it | `topComment` |
| The original post when a Page reshares one: link, author, time | `isReshare`, `sharedPost` |
| Facebook's own captions on a video (optional) | `captionsText`, `captionsLanguage` |
| The Page it came from | `pageName`, `pageId`, `pageUrl`, `pageHandle`, `pageIsVerified`, `pageFollowers`, `pageLikes`, `pageProfilePicture` |
| The post's id for Facebook's reactions and comments | `feedbackId` |
| The Page or post link you asked for | `inputUrl` |

### ▶️ How to scrape Facebook posts

1. Open [Facebook Posts Scraper](https://apify.com/dami_studio/facebook-posts-scraper) and click
   **Try for free**.
2. Put your Pages into **Facebook Page URLs**, one per line.
3. Set **Posts per Page**. Leave it at 20 unless you want fewer.
4. Click **Start**.
5. Download the dataset as JSON, CSV or Excel, or pull it from the Apify API.

Run it with empty input and you get one labelled sample row, which is not charged, so you can see
the shape before you spend anything.

### 💰 How much does it cost to scrape Facebook posts?

**$0.65 per 1,000 posts.** That is the free plan's price; paid plans pay a little less: $0.64 on Starter, $0.63 on Scale and $0.62 on Business. On the free plan, the
$5 Apify gives you each month covers about 7,500 posts.

You pay for posts that come back with data. The sample row, every diagnostic row, posts dropped by
your date filters and the same Page listed twice are not charged, and neither is a Page that turns
out to have no public posts. A post from its own link costs the same as any other post, and the captions box does not change
the price.

### 📥 What you give it

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

| Field | Default | What it is |
|---|---|---|
| `startUrls` | none | Page links, one per line. A bare handle like `NASA` works, and so does a `profile.php?id=` link. A link to one post (`facebook.com/NASA/posts/...`) or one video (`.../videos/...`) returns that post alone, billed as one post. A link to a single photo gets a free row, and share, reel and watch links are refused the same way. First 50 per run, and the same Page written two ways is read once. |
| `resultsLimit` | empty, runs as 20 | Posts per Page, newest first. 1 to 20. |
| `onlyPostsNewerThan` | none | Drop posts older than this. A date (`2026-08-01`), a full timestamp, or a window like `7 days` or `30 minutes`. Dropped posts are never charged. Anything else, `yesterday` for one, stops the run before a Page is read and charges nothing for posts. |
| `onlyPostsOlderThan` | none | The other end of the window, same formats and the same refusal. Use both together to pull one month. |
| `includeCaptions` | off | Adds the text of Facebook's own captions, and their language, to video posts that have them. A captioned video takes a moment longer; videos without captions keep both columns empty. |
| `proxyUrls` | none | Optional. One URL per line if you want the traffic to leave through servers you already pay for. |

### 📤 What you get back

A real row from a run on 24 October 2026. The post text is cut short, two links are trimmed, and the
commenter's name and picture are left out here:

```json
{
  "ok": true,
  "charged": true,
  "recordType": "post",
  "inputUrl": "https://www.facebook.com/NASA",
  "pageName": "NASA - National Aeronautics and Space Administration",
  "pageId": "100044561550831",
  "pageUrl": "https://www.facebook.com/NASA/",
  "pageIsVerified": true,
  "pageFollowers": 28743583,
  "pageLikes": null,
  "postId": "1658860588942644",
  "postUrl": "https://www.facebook.com/NASA/videos/1557618349384047/",
  "text": "The aircraft lineup just keeps getting bigger. ✈️

MAX POWER, a new aerospace expo, is set for Nov. 7-8 ...",
  "textLength": 332,
  "time": "2026-10-16T19:37:44.000Z",
  "timestamp": 1792179464,
  "reactionsCount": 2105,
  "topReactions": [
    { "type": "Like", "count": 1878 },
    { "type": "Love", "count": 178 },
    { "type": "Care", "count": 25 },
    { "type": "Wow", "count": 17 },
    { "type": "Angry", "count": 3 },
    { "type": "Haha", "count": 2 },
    { "type": "Sad", "count": 2 }
  ],
  "commentsCount": 82,
  "sharesCount": 180,
  "postType": "video",
  "media": [
    {
      "type": "video",
      "id": "1557618349384047",
      "url": "https://www.facebook.com/NASA/videos/1557618349384047/",
      "thumbnailUrl": "https://lookaside.fbsbx.com/...",
      "durationMs": 17770,
      "style": "Video"
    }
  ],
  "mediaCount": 1,
  "isReel": false,
  "isSponsored": false,
  "linkUrl": null,
  "linkTitle": null,
  "scrapedAt": "2026-10-24T22:54:42.475Z",
  "videoViews": 119622,
  "liveViewerCount": 0,
  "videoUrl": "https://lookaside.fbsbx.com/...",
  "linkDescription": null,
  "linkSource": null,
  "linkImage": null,
  "reactionLikeCount": 1878,
  "reactionLoveCount": 178,
  "reactionCareCount": 25,
  "reactionHahaCount": 2,
  "reactionWowCount": 17,
  "reactionSadCount": 2,
  "reactionAngryCount": 3,
  "textLinks": ["https://www.nasa.gov/maxpower/"],
  "hashtags": [],
  "taggedAccounts": [
    { "name": "NASA's Kennedy Space Center", "id": "100053414617500", "url": "https://www.facebook.com/NASAKennedy/" }
  ],
  "pageHandle": "NASA",
  "feedbackId": "ZmVlZGJhY2s6MTY1ODg2MDU4ODk0MjY0NA==",
  "isReshare": false,
  "sharedPost": null,
  "topComment": {
    "text": "I'm curious what it would look like and what it would be like to create an object in the near future ...",
    "time": "2026-10-18T01:18:48.000Z",
    "timestamp": 1792286328,
    "url": "https://www.facebook.com/NASA/videos/1557618349384047/?comment_id=2993136914359141",
    "replyCount": 0,
    "likeCount": 2,
    "authorName": "...",
    "authorUrl": null,
    "authorPicture": "..."
  },
  "captionsText": null,
  "captionsLanguage": null
}
```

| Field | How to read it |
|---|---|
| `postId`, `postUrl` | Your key, and the permalink. Reels come back as `/reel/`, videos as `/videos/`, ordinary posts as `/posts/pfbid`. |
| `text` | The full message as posted, not a preview. Line breaks and emoji are kept exactly. On a reshare with nothing added, it holds the original post's text. |
| `time`, `timestamp` | The real posting time in ISO and in epoch seconds, read from the post rather than from a "2 days ago" label. |
| `postType` | `video`, `photo`, `link` or `status`. A post sharing an outside article is `link`, with `linkUrl` and `linkTitle` filled. |
| `media` | Each photo or video with an id, a permalink, a thumbnail, the alt text Facebook holds for screen readers, `durationMs` on videos, and `width` and `height` of the full-size photo. |
| `videoViews`, `liveViewerCount` | `null` on posts without a video. Views keep climbing while a video is live on the Page. |
| `videoUrl` | The video file. It is a `lookaside.fbsbx.com` link that may not play if you paste it into a browser tab; fetch it from code. |
| `reactionLikeCount` to `reactionAngryCount` | The same numbers as `topReactions`, one column each. A type with no reactions is `0`; `null` only if the breakdown did not add up to the total. |
| `linkDescription`, `linkSource`, `linkImage` | Filled on link posts. The description can be `null` while the site and image are there. |
| `textLinks` | The real addresses linked in the text, not Facebook's redirect links. |
| `taggedAccounts` | Each account tagged in the text, with its name, id and link as Facebook prints them. |
| `topComment` | The one comment Facebook shows under the post, or `null`. `likeCount` arrives rounded, so 1.2K comes back as 1200. `authorUrl` is `null` when the commenter has no public profile link. |
| `isReshare`, `sharedPost` | `sharedPost` holds the original's link, author and time when the Page reshared a post, else `null`. |
| `captionsText`, `captionsLanguage` | Only with `includeCaptions` on, and only on videos with Facebook captions. Otherwise `null`. |
| `pageHandle` | The Page's name in its link, `NASA` here. `null` for a Page known only by number. |
| `pageFollowers`, `pageLikes` | Whichever of the two the Page shows. The other comes back `null`. |
| `sharesCount` | `null` where the count is not published. |
| A post read from its own link | `pageFollowers`, `pageLikes` and `pageIsVerified` are `null`. A video read from its link has no `sharesCount`, and its `time` is the video's own publish time, which can be a few seconds before the post's. |

### 🧾 Reading the output

Three kinds of row can land in your dataset.

| Row | How to spot it | Billed |
|---|---|---|
| A post | `ok: true` and `charged: true` | yes |
| The sample row | `_sample: true` | no |
| A diagnostic | `_diagnostic: true` and an `errorCode` | no |

Filter on `charged == true` and you have your data. That flag is what separates real rows from the
sample and the diagnostics, not a receipt.

| `errorCode` | What happened |
|---|---|
| `BAD_INPUT` | That link could not be read as a Page or a post, or a date field held something that is not a date. Group, event, watch, share and single-photo links land here too. |
| `NOT_FOUND` | No public Page or post at that link. |
| `NO_RESULTS` | The Page came back but published no posts, or none inside your date window. A post link that showed no post, or a post outside the window, lands here too. |
| `NOT_SERVED` | Facebook did not show this Page's posts on any of the four tries this run, or a linked post on either of its two. Re-run it later. Runs before 13 October 2026 wrote `BLOCKED` here. |
| `NETWORK` | Facebook could not be reached. |
| `TIME_BUDGET` | The run ran out of time before reaching that Page or link. |
| `PROXY_INPUT_ADJUSTED` | A connection setting this actor does not use was dropped. The run carried on. |
| `CHARGE_ERROR` | A billing event could not be recorded. Told to you rather than hidden. |
| `UNEXPECTED_ERROR` | Anything else, with the message attached. |

A run ends as Succeeded unless something broke on our side. Running out of time before any post came back
gives `TIME_BUDGET` rows and an amber status. Only an `UNEXPECTED_ERROR` before the first post ends it as Failed.

One quirk of the Console's Overview table: it shows the Page and post columns, so the engagement
counts and the media look missing there. Open the row itself or download the JSON and they are all
present.

### 💡 What people use it for

- Pulling twenty Pages every morning and diffing on `postId` to see only what is new.
- Reporting: reaction, comment and share counts land as plain numbers, so engagement rate is a
  spreadsheet formula rather than a cleaning job.
- Deciding what to publish by reading what a whole category of Pages posted this week, sorted by
  `reactionsCount`.
- Keeping an archive of a Page you own, permalinks and media links included, so the record survives
  a deleted post.

From a Page's week to what people said about it, in three steps:

1. Run this on a Page with `onlyPostsNewerThan` set to `7 days`.
2. Sort on `commentsCount` and copy the `postUrl` of the top posts.
3. Paste them into
   [Facebook Comments Scraper](https://apify.com/dami_studio/facebook-comments-scraper) and read the
   comments themselves.

### 🚧 What it does not do

- **No deep history.** Twenty posts per Page is the ceiling, and the date filters work inside those
  twenty. If you need last year, this is the wrong tool.
- **Public Pages only.** Private timelines, groups, events and anything behind a login come back as
  an uncharged diagnostic row.
- **Age-restricted and country-restricted Pages** return an uncharged row, and re-running will not change it.
- **No comment threads and no reactor identities.** You get the counts and the one comment Facebook
  shows under each post. The full threads are a separate actor, linked below.
- **Pinned posts can be missing.** Facebook leaves them out of the public timeline, so a Page whose
  newest content is pinned may show it lower down or not at all.
- **Counts are a snapshot** at read time and keep moving on a live post.
- **Nothing is translated.** The text comes back in the language it was written in.
- It is not an official Facebook tool. Dami's Studio is independent and is not affiliated with or endorsed by Facebook or Meta, or by any other company named on this page.

### 🧭 Which Facebook scraper do you need?

| If you want | Use |
|---|---|
| Recent posts from a Page | This one |
| The comments under a post you have the link for | [Facebook Comments Scraper](https://apify.com/dami_studio/facebook-comments-scraper) |
| Posts from a public group | [Facebook Groups Scraper](https://apify.com/dami_studio/facebook-groups-scraper) |
| Reels from a Page | [Facebook Reels Scraper](https://apify.com/dami_studio/facebook-reels-scraper) |
| The website, e-mail and phone on a Page | [Facebook Page Contact Info Scraper](https://apify.com/dami_studio/facebook-page-contact-info-scraper) |
| The ads a Page is running | [Meta Ad Library Scraper](https://apify.com/dami_studio/meta-ads-library-scraper) |
| Recent posts from an Instagram account | [Instagram Scraper](https://apify.com/dami_studio/instagram-scraper) |

### ❓ Questions people ask

#### Do I need a Facebook account to scrape posts?

No. You need no Facebook account of your own for public Pages.

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

Not from a public Page. Nothing in the input raises the ceiling.

#### Can I scrape a single Facebook post?

Yes. Put the post's link (`facebook.com/<Page>/posts/...`) or a video's link in Facebook Page URLs
and you get one row for that post, at the price of one post.

#### Why did a Page return nothing?

Either it publishes no posts, everything it has is outside your date window, or it is restricted. The diagnostic row says which.

#### Why do the numbers differ from what I see on Facebook?

Counts keep moving on a live post, and the row holds them as they were when it was read. If a post
seems missing, open the Page in a private browser window.

#### Can I call it from code or connect it to an AI assistant?

The [API tab](https://apify.com/dami_studio/facebook-posts-scraper/api/python) has code for Python, JavaScript and the
CLI. MCP clients like Claude and ChatGPT use
`https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/facebook-posts-scraper`; coding assistants can load [our
skill file](https://api.apify.com/v2/key-value-stores/BTBHU3DdiuoF4JOsK/records/apify-facebook-meta-SKILL.md). All
three bill your Apify account at the same price.

#### Is scraping Facebook legal?

This reads public Pages only, never private content. Results can still contain personal data, which
GDPR and similar laws cover, so have a reason for collecting it. Apify's write-up on
[the legality of web scraping](https://blog.apify.com/is-web-scraping-legal/) is a good starting
point, and we are not lawyers.

### 🆘 If something breaks

Open the **Issues** tab on the actor page. Paste the Page link you used and the run ID, and the log
is enough for us to see what happened.

# Actor input Schema

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

Public Facebook Page URLs, one per line. A plain handle (NASA) or a profile.php?id= URL works too. A link to one post (facebook.com/NASA/posts/...) or one video (.../videos/...) returns just that post. Up to 50 lines per run.

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

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

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

Optional. Drop posts older than this. Accepts a date (2026-08-01), a full timestamp, or a relative window such as "7 days" or "30 minutes". Filtered-out posts are never charged. It filters within the recent posts a Page exposes publicly - it does not reach further back in time. Anything that is not a date in one of those forms, such as "yesterday", stops the run before any Page is read, with nothing charged.

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

Optional. Drop posts newer than this. Same formats as above, and anything else is refused the same way. Filtered-out posts are never charged.

## `includeCaptions` (type: `boolean`):

Optional, off by default. On video posts that carry Facebook's own captions, adds the caption text and its language to the row. It takes a moment more per captioned video. Videos without captions keep those two columns empty.

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

Leave this empty for a normal run. Fill it in only if you want the traffic to leave through proxy servers you already pay for, one URL per line, in the form http://user:pass@host:port.

## Actor input object example

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

# Actor output Schema

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

Every row in the default dataset: inputUrl, pageName, pageId, pageUrl, pageIsVerified, pageFollowers, pageLikes, pageProfilePicture, postId, postUrl, text, textLength, time, timestamp, reactionsCount, topReactions, commentsCount, sharesCount, postType, media, mediaCount, isReel, isSponsored, linkUrl, linkTitle, scrapedAt, videoViews, liveViewerCount, videoUrl, linkDescription, linkSource, linkImage, reactionLikeCount, reactionLoveCount, reactionCareCount, reactionHahaCount, reactionWowCount, reactionSadCount, reactionAngryCount, textLinks, hashtags, taggedAccounts, pageHandle, feedbackId, isReshare, sharedPost, topComment, captionsText, captionsLanguage. An empty, blocked or unmatched run returns a single uncharged row explaining what happened instead.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

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

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "startUrls": [
        "https://www.facebook.com/NASA",
        "https://www.facebook.com/BBCNews"
    ],
    "resultsLimit": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/facebook-posts-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "startUrls": [
        "https://www.facebook.com/NASA",
        "https://www.facebook.com/BBCNews",
    ],
    "resultsLimit": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/facebook-posts-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrls": [
    "https://www.facebook.com/NASA",
    "https://www.facebook.com/BBCNews"
  ],
  "resultsLimit": 20
}' |
apify call dami_studio/facebook-posts-scraper --silent --output-dataset

```

## MCP server setup

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

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/EXNh69YkiB1C2C1Fo/builds/LdYfKlt2xugdEVusu/openapi.json
