Facebook Posts Scraper — Facebook Page Posts & Group Posts
Pricing
from $2.00 / 1,000 posts
Facebook Posts Scraper — Facebook Page Posts & Group Posts
Facebook posts scraper for pages and public groups: paste a link and get posts as rows — text, date, ten-way reactions, comment count, image and a playable MP4 link on video posts. Optional page details: followers, category, email, phone, address, hours, ads. No login, exact limit, no start fee.
Pricing
from $2.00 / 1,000 posts
Rating
0.0
(0)
Developer
Steadyfetch Team
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
21 hours ago
Last modified
Categories
Share
Click Start with nothing set and the run returns the built-in sample rows — a three-post sample from one public brand page with page details off, so you see the exact output shape before you spend anything: nothing is collected from Facebook, and there is no result fee. Then paste a Facebook page and get its posts, paste a public group and get its posts — same run, same columns. One JSON row per post: the post ID and permalink, the page or group it is in, who posted it, the text, the date, the post type, one image, a playable MP4 link with the moment it expires on video posts, the reaction total and its ten-way breakdown, the comment count, and the top comments Facebook serves with the post. From $2.00/1,000 posts on the Business plan ($5.00 on the Apify free plan), platform usage included, no start fee.
Using an AI agent? Pin this actor in Apify's MCP server with one link: https://mcp.apify.com?tools=steadyfetch/facebook-posts-scraper
- Actor id:
steadyfetch/facebook-posts-scraper - Input:
{ "pages": ["ladbible"] }— the one field you have to set. Or{ "groups": ["https://www.facebook.com/groups/dogspotting"] }for a public group, orstartUrlswith both mixed. - Cap the bill: set
maxTotalChargeUsdon the run (a run option, not Actor input), e.g.0.50— the run stops when it reaches it. - Your cap is the cap.
resultsLimit(default 20) is exact and per page or group;maxItems(default 1000) caps the whole run. If your input carrieslimit,countormaxPostsinstead — the names other Facebook post scrapers use — the smallest of them is read asmaxItemsand one uncharged note row says so.
Unofficial. Not affiliated with, endorsed by, or sponsored by Facebook or Meta Platforms, Inc.
What a row does NOT carry
Read this before the feature list, because it is the part a listing usually leaves for the Issues tab. Two numbers are not in the feed a logged-out reader of these pages and groups is shown, so this actor does not sell them:
shareCountis alwaysnull. How many times a post was shared is not published on the surface this actor reads.videoViewCountis alwaysnullon every video post. The key is served, and its value is empty every time.
Both ship as explicit null columns rather than as missing ones, so a sheet or a fixed-schema table never breaks and you never have to wonder whether a zero was really a zero. This actor will not promise a column it cannot fill. Everything else on the row is a real value, or an honest null with a reason beside it.
What you put in
| Input | What it takes |
|---|---|
pages | Facebook pages, one per line — a name (ladbible), @ladbible, a full page link, or a profile.php?id=… link. Posts come back newest first. |
groups | Public groups: a group link, a /groups/<numeric id> link, or a bare numeric group id. Same columns as a page, with source: group. |
startUrls | Page links and group links in one list, for moving over from another scraper. Each line is sent to the right door by its own shape, before anything is bought. |
resultsLimit | Posts per page and per group, exact. Default 20 — three targets at 20 collect up to 60. Set it to 0 with page details on for a details-only run. |
maxItems | The ceiling on posts for the whole run. Default 1000. It also answers to limit, count and maxPosts, and the smallest one present binds — one uncharged note row says which name was read. |
includePageDetails | One extra row per PAGE: followers, likes, talking-about, category, intro, creation date, website and links, email, phone, address, opening hours, price range, services, rating, ad status, verification and Instagram handle. Off by default, and groups never get one. |
newerThan · olderThan | A window on the post's own publish date — 2026-09-01, an ISO timestamp, or 7 days. The walk is newest first, so newerThan ends it at the boundary and buys nothing past it. No filter fee. |
maxRunSeconds | A wall clock on the run. Default 1800. The run stops cleanly and says what is left instead of being killed. |
includeSeen · resumeFromDatasetId | Re-run the same pages and groups without paying again for posts this account already has. |
What you get
One row per post, from a page and from a public group alike.
| Column | What it holds |
|---|---|
postId · url | The post's own ID and its permalink, on both doors. |
source | page or group — which door the row came through. |
pageName · pageId · groupName · groupId | Where the post lives. |
authorName · authorId | Who posted it. |
text | What was written. |
createdAt | When it was posted, ISO-8601 UTC, from Facebook's own publish time. |
postType | video, image or text. |
imageUrl | The post's image — one image per post, which is what the feed carries. |
videoUrl · videoSdUrl · videoUrlExpiresAt | A directly playable MP4 link in HD and SD on a video post, and the moment Facebook's signature on it stops working. |
thumbnailUrl | The video's cover frame. |
reactionCount | Reactions on the post, all kinds together. |
reactions | The ten-way breakdown, one number each: like, love, care, haha, wow, sad, anger, thankful, pride, confused. |
commentCount | How many comments sit under the post. |
shareCount · videoViewCount | Always null — see the block above. |
topComments | 0 to 2 comments, exactly as the feed serves them, and never charged. |
repeat | This account already had this post and asked for it again. |
charged · status · statusReason | Whether this row was billed and what happened, so your invoice reconciles from the dataset itself. |
The ten reaction columns are the reason most people are here: a post with 40 anger and 3 like is a different signal from 43 reactions, and you cannot get that from a total.
Column names are ours and they stay put. The wire is read in one place and renamed once, so a Facebook rename does not rename your columns, and a fixed-schema table never breaks on a new row.
Price
Two charged events, and each lands only on a row that was actually delivered. A row is charged as either a Post or a Page details row, never both.
| Event | Apify free plan | Bronze | Silver | Gold and above |
|---|---|---|---|---|
| Post — one page or group post | $0.005 | $0.0035 | $0.0026 | $0.002 |
Page details — one page row, only with includePageDetails | $0.012 | $0.008 | $0.006 | $0.005 |
The store header is the cheapest tier of the primary event: from $2.00/1,000 posts. Platform usage is included in both prices — there is no separate compute bill on top. No start fee, no filter fee, and no result fee on a miss. A date window costs nothing, and a run that finds nothing to deliver charges nothing. Set maxTotalChargeUsd on the run and it is a hard ceiling: the run stops cleanly under it, delivers everything already in hand, and the last row says what is left.
What is never charged
Every run closes its own books, and each of these ships as a labelled, uncharged row rather than as silence:
page_not_found— Facebook says there is no such page. A definitive answer about your link.page_private— the page is real and Facebook shows its posts to logged-in users only.page_no_posts— the page is real and its own feed ended the walk short of what you asked for: it ran out of posts, or your ownnewerThan/olderThanwindow closed the walk. Any posts already delivered say what each was charged, so the no-fee statement covers only what did not arrive.group_not_found— Facebook says there is no such group.group_private— the group is real and Facebook shows its posts to members only.group_no_posts— the same two endings on the group door: the group is real and public, and its own feed ran out or your window closed the walk.skipped.page_cap— a bound of THIS ACTOR'S own, never a verdict about your page or group: the feed was still serving and the run had read the 40 feed pages it buys for a single input. Split fewer targets across more runs to go deeper.skipped.all_repeat— the other bound of ours: two feed pages running that added no new post, so the run stopped paying to look. On a repeat run that usually means you already have everything there is.feed_unavailable— the read did not go through. Temporary, says nothing about your target, and a re-run is the fix. When it lands part-way through an input, the row opens with the posts already delivered and charged, and the no-fee statement covers only the rest.- Posts dropped by your own
newerThan/olderThanwindow, posts past your ownresultsLimitormaxItems, posts the run clock or the cost cap did not reach, and posts this account already had. - Starting a run. There is no start fee, per post or per run.
The last row of every run is a receipt: delivered, asked for, skipped, what stopped it, and the charged-event totals. A cost cap or a time limit ends the collecting, never the delivering — posts already in hand are always written out.
How often the data changes
A busy page posts hourly and an active group posts about daily, so the useful pattern here is the same targets again tomorrow rather than one deep run. Reactions and comment counts keep moving for roughly 72 hours after a post goes up, which is the window worth re-reading if you are measuring engagement. And videoUrl is a signed link that Facebook expires about four days out (videoUrlExpiresAt is on the row) — download the MP4 in the same week, or re-run for fresh links.
Re-running would otherwise re-buy the posts you already have, so it does not: every run remembers what it delivered to your account, and a later run skips those — not delivered again, not charged, and the slot goes to the next new post instead, so you still get the number you asked for. That makes a daily watch on the same pages cost only what is new. The memory is a key-value store in your own Apify account, so a run started with a scoped API token in restricted-access mode needs key-value store Read, Write and Create permission (or Actor runs set to Full access); without it the run says so on its status line and cannot skip what you already have (Write is what lets the run remember what it delivered; without it every run pays again). Turn on Include posts you already have to get them back anyway, marked repeat: true and still uncharged, and use Skip posts in this dataset (resumeFromDatasetId) to carry the same memory across accounts.
Honest caps
- Public groups only. A private group returns an uncharged row — Facebook shows its posts to members only, with or without a login, so there is no setting that opens one. A group that does not exist is a different uncharged row, so "private" and "gone" are never the same answer.
- Newest first, and that is the only order. There is no "top posts" order here, and no input that asks for one. A sort sent under any name comes back as one uncharged note row saying newest-first is what you get, never as a silent substitution.
- One image per post, not a carousel — the feed carries a single image per post.
- 0 to 2 top comments, exactly as the feed serves them, and never charged. They are a taste, not a thread; the thread is a different actor's job.
- Facebook may change how any of this is served. If it does, the rows that fail are labelled and not charged.
Where the other Facebook links go
- A reel, watch, share or fb.watch link: Facebook reels, with the MP4 and the transcript
- The comments under a post, in full, with their own reaction breakdown: Facebook comments
- An Ad Library link, or the ads an advertiser is running right now: Meta Ad Library rows
- The spoken words inside a Facebook video ad: Facebook ad transcripts
Paste one of those into this actor and it comes back as an uncharged row naming the actor that takes it, rather than as a failed run.
Something not right?
Open the Issues tab on this actor with your run ID and the input you used, and it gets looked at. If it earned its keep, a rating helps other buyers find it, and saving the actor keeps it one click away.