How to Pull Facebook Group Posts with an API (2026)
List posts from a public Facebook group as paginated JSON — sorted feeds, cursor pagination, comment threads, and local intel workflows you can run on a cron job.
Facebook groups hold the conversations brand pages miss — ISO requests in a city buy/sell group, franchise operators comparing vendors, parents trading daycare waitlist tips. Meta does not ship a clean export for any of it, and the Graph API was never built for polling arbitrary public groups on a schedule.
This guide walks through a monitoring sprint — the same steps a local ops analyst runs manually, but with API calls you can cron, dedupe, and route to Slack. We use Austin-area buy/sell and local services groups as the running example; swap in your metro and vertical.
The short version
GET /v1/facebook/groups/posts?url={groupUrl} returns a page of posts. Pass sortBy for feed ordering, cursor for pagination, and GET /v1/facebook/posts/comments when replies carry more signal than the post body.
You'll need an API key. Test with a public group URL in the Playground.
Why groups beat brand pages for local intel
Official pages show what a business wants you to see. Groups show what people ask each other when no one is selling.
Three patterns show up in local and vertical monitoring:
- Intent in plain language — "ISO queen mattress, pickup only" or "anyone know a good plumber in Round Rock?" beats a branded lead form for understanding demand.
- Geography baked into membership — a group named after a suburb or metro already scopes your territory. You do not need geocoding on every row.
- Negotiation in comments — the post might be one line; price talk, vendor names, and complaint details often sit in replies.
Franchise ops, real estate investors, home services, and resale marketplaces use group feeds for territory scans. The API work is collection and dedupe — your rules still decide which posts warrant a human.
Only collect public data and respect group rules and Terms. Private groups are out of scope.
Phase 0: Define the job before the URL list
Before touching code, write one sentence the sprint must answer. Vague goals produce noisy alerts.
| Monitoring job | Example question | Bad default | Better default |
|---|---|---|---|
| Buy/sell inventory | What moved under $500 this week? | sortBy=top | chronologicalListings, hourly poll |
| Service requests | Who asked for a plumber today? | scrape one page once | chronological, keyword plumber OR recommend |
| Brand complaints | Did anyone mention our franchise? | read the group manually | recentActivity, brand name in client filter |
| Vendor research | Which suppliers get named in trade groups? | top only | recentActivity + comment pulls on short posts |
Tag each group in your spreadsheet with:
- Sort order — marketplace groups usually want
chronologicalListings; debate threads wantrecentActivity. - Poll cadence — hourly for buy/sell, daily for slow vertical groups.
- Page depth — how many cursor pages per run (see Phase 4 for credit math).
- Comment policy — pull comments when
commentCountexceeds a threshold or when the post text is under 80 characters.
You will wire these tags into Phase 6. First, build the group catalog.
Phase 1: Find and catalog public groups
Social Fetch does not expose a "search all Facebook groups" endpoint. Discovery is manual — same as it would be without an API.
Practical sources:
| Source | What you get | Caveat |
|---|---|---|
| Facebook group search | Name, member count, public/closed badge | Confirm the group is Public before adding to cron |
| Competitor or partner lists | Groups your sales team already watches | URLs go stale when groups rename slugs |
| City + category queries | "Austin buy sell trade", "Round Rock moms" | Duplicate metro groups — dedupe by URL, not name |
| Customer interviews | "Where do you post when you move?" | High-signal but slow to collect |
Store one row per group:
{
"groupUrl": "https://www.facebook.com/groups/austin-buy-sell-trade",
"metro": "Austin-Round Rock",
"vertical": "buy-sell",
"sortBy": "chronologicalListings",
"pollEveryMinutes": 60,
"maxPagesPerRun": 2,
"keywords": ["ISO", "moving sale", "obo"]
}Validate a URL before scheduling it. A single probe call costs one credit and tells you whether the group resolves:
curl -sS \
-H "x-api-key: $SOCIALFETCH_API_KEY" \
-G "https://api.socialfetch.dev/v1/facebook/groups/posts" \
--data-urlencode "url=https://www.facebook.com/groups/austin-buy-sell-trade" \
--data-urlencode "sortBy=chronological"Check data.lookupStatus:
found— safe to schedule.postsmay still be empty if the group has no recent public activity.not_found— wrong URL, private group, or deleted community. Fix the row before burning credits on cron.
Reference: Facebook group posts.
Phase 2: Poll a group feed
Once the catalog is validated, hit the feed on your cadence. One group, one sort order, one page per request.
curl -sS \
-H "x-api-key: $SOCIALFETCH_API_KEY" \
-G "https://api.socialfetch.dev/v1/facebook/groups/posts" \
--data-urlencode "url=https://www.facebook.com/groups/examplepublicgroup" \
--data-urlencode "sortBy=recentActivity"The preset uses sortBy=recentActivity — good for threads with fresh comments. Buy/sell groups often need chronologicalListings instead (Phase 3).
TypeScript with the SDK:
import { SocialFetchClient } from "@socialfetch/sdk";
const client = new SocialFetchClient({
apiKey: process.env.SOCIALFETCH_API_KEY!,
});
const result = await client.facebook.listGroupPosts({
url: "https://www.facebook.com/groups/austin-buy-sell-trade",
sortBy: "chronologicalListings",
});
if (!result.ok) {
console.error(result.error.code, result.error.requestId);
process.exit(1);
}
if (result.value.data.lookupStatus !== "found") {
console.log("Group not accessible:", result.value.data.lookupStatus);
process.exit(0);
}
for (const post of result.value.data.posts ?? []) {
console.log(post.publishedAt, post.text?.slice(0, 80), post.commentCount);
}Each post includes id, url, text, publishedAt, reactionCount, commentCount, and author (name, url when available). Video posts may include video with thumbnail and duration fields — useful when you filter out pure image listings.
There is no server-side keyword parameter. Match in your worker:
const keywords = ["iso", "moving sale", "obo"];
const hits = (result.value.data.posts ?? []).filter((post) => {
const body = (post.text ?? "").toLowerCase();
return keywords.some((kw) => body.includes(kw));
});Phase 3: Pick the right sortBy
The sortBy query parameter maps to Facebook's feed tabs. Wrong sort wastes credits — you page through posts your job does not care about.
| Value | Feed behavior | Typical monitoring job |
|---|---|---|
chronologicalListings | Marketplace-style listing order | Buy/sell, ISO, furniture, vehicles |
chronological | Newest-first timeline | Service requests, same-day alerts |
recentActivity | Threads with fresh comments | Vendor recommendations, complaint threads |
top | High-engagement posts in the window | Weekly digest, "what dominated this group" |
Buy/sell example — listing sort surfaces inventory order, not comment heat:
curl -sS \
-H "x-api-key: $SOCIALFETCH_API_KEY" \
-G "https://api.socialfetch.dev/v1/facebook/groups/posts" \
--data-urlencode "url=https://www.facebook.com/groups/austin-buy-sell-trade" \
--data-urlencode "sortBy=chronologicalListings"Local services example — chronological catches "need a plumber today" before it accumulates comments:
curl -sS \
-H "x-api-key: $SOCIALFETCH_API_KEY" \
-G "https://api.socialfetch.dev/v1/facebook/groups/posts" \
--data-urlencode "url=https://www.facebook.com/groups/round-rock-homeowners" \
--data-urlencode "sortBy=chronological"Vendor intel example — recent activity bumps threads where the answer arrived yesterday:
curl -sS \
-H "x-api-key: $SOCIALFETCH_API_KEY" \
-G "https://api.socialfetch.dev/v1/facebook/groups/posts" \
--data-urlencode "url=https://www.facebook.com/groups/contractors-central-texas" \
--data-urlencode "sortBy=recentActivity"If you are unsure, run two probe calls with different sortBy values on the same group and compare the first five post.id values. Pick the sort that surfaces posts your analyst would have opened first.
Phase 4: Paginate without blowing the budget
Responses include data.page.nextCursor and data.page.hasMore. Pass nextCursor verbatim on the next request — do not parse or construct cursors yourself.
const groupUrl = "https://www.facebook.com/groups/austin-buy-sell-trade";
const posts = [];
let cursor: string | undefined;
let pages = 0;
const maxPages = 3;
do {
const result = await client.facebook.listGroupPosts({
url: groupUrl,
sortBy: "chronologicalListings",
cursor,
});
if (!result.ok) {
console.error(result.error.code, result.error.requestId);
break;
}
if (result.value.data.lookupStatus !== "found") break;
posts.push(...(result.value.data.posts ?? []));
cursor = result.value.data.page.nextCursor ?? undefined;
pages += 1;
} while (cursor && pages < maxPages);
console.log(posts.length, "posts from", pages, "pages");Stop when:
data.page.hasMoreis falsenextCursoris null- You hit
maxPagesPerRunfrom your catalog
For incremental monitoring, store the newest post.id or publishedAt from each run. On the next poll, stop paging when you reach a post you already ingested — even if hasMore is still true. That pattern cuts credits on busy groups.
Dedupe across groups and runs on post.id before writing to your warehouse or firing webhooks. The same listing sometimes gets cross-posted; url is the better dedupe key if you want one row per discussion.
Phase 5: Pull comment threads
Group post text is often thin. "Anyone know a good electrician?" might be twelve words; the thread names three businesses, two price ranges, and a warning about one of them.
Pass the post permalink to the comments endpoint:
curl -sS \
-H "x-api-key: $SOCIALFETCH_API_KEY" \
-G "https://api.socialfetch.dev/v1/facebook/posts/comments" \
--data-urlencode "url=https://www.facebook.com/groups/examplepublicgroup/posts/1234567890/"Paginate comment pages the same way as the feed:
const postUrl = "https://www.facebook.com/groups/examplepublicgroup/posts/1234567890/";
const comments = [];
let cursor: string | undefined;
do {
const result = await client.facebook.getPostComments({ url: postUrl, cursor });
if (!result.ok || result.value.data.lookupStatus !== "found") break;
comments.push(...(result.value.data.comments ?? []));
cursor = result.value.data.page.nextCursor ?? undefined;
} while (cursor);Reference: Facebook post comments. For scoring comment text at scale, see the sentiment guide.
Pull comments selectively — not every feed row deserves another credit. Heuristics that work in production:
commentCountabove 5 for local service requestscommentCountabove 15 for buy/sell (price negotiation lives in replies)- Post
textunder 80 characters (headline-only posts) - Keyword hit on the post body for your brand or competitor list
Optional: pass feedbackId when you have it from a prior response — can speed up repeat lookups on the same thread.
Phase 6: Local intel workflows
Wire Phases 1–5 into jobs your team already runs.
Hourly buy/sell watch
- Poll each buy/sell group with
sortBy=chronologicalListings,maxPages=2. - Filter
textfor ISO, price patterns ($,obo,firm), and category keywords. - Dedupe on
post.id, compare against yesterday's IDs. - Slack or email new rows with
url,text,publishedAt, andauthor.name.
Daily service-request digest
- Poll homeowner or city groups with
sortBy=chronological, one page per group. - Match
recommend,looking for,anyone know, plus trade terms (plumber,HVAC,roofer). - Pull comments on matches with
commentCount >= 5. - Aggregate vendor names from comment
textinto a frequency table by metro.
Weekly brand mention scan
- Poll brand-adjacent or franchise groups with
sortBy=recentActivity, three pages. - Client-side match on brand strings and common misspellings.
- Pull full comment threads on negative-sentiment keywords (
scam,refund,never again). - Archive rows with
requestIdfor support escalation.
Multi-group batch worker
Loop your catalog JSON and respect per-group sortBy and maxPages:
import { SocialFetchClient } from "@socialfetch/sdk";
const client = new SocialFetchClient({
apiKey: process.env.SOCIALFETCH_API_KEY!,
});
type GroupJob = {
groupUrl: string;
sortBy: "top" | "recentActivity" | "chronological" | "chronologicalListings";
maxPages: number;
metro: string;
};
const catalog: GroupJob[] = [
{
groupUrl: "https://www.facebook.com/groups/austin-buy-sell-trade",
sortBy: "chronologicalListings",
maxPages: 2,
metro: "Austin",
},
{
groupUrl: "https://www.facebook.com/groups/round-rock-homeowners",
sortBy: "chronological",
maxPages: 1,
metro: "Round Rock",
},
];
const rows = [];
for (const job of catalog) {
let cursor: string | undefined;
let pages = 0;
do {
const result = await client.facebook.listGroupPosts({
url: job.groupUrl,
sortBy: job.sortBy,
cursor,
});
if (!result.ok) break;
if (result.value.data.lookupStatus !== "found") break;
for (const post of result.value.data.posts ?? []) {
rows.push({
metro: job.metro,
groupUrl: job.groupUrl,
postId: post.id,
text: post.text,
url: post.url,
publishedAt: post.publishedAt,
commentCount: post.commentCount,
requestId: result.value.meta.requestId,
creditsCharged: result.value.meta.creditsCharged,
});
}
cursor = result.value.data.page.nextCursor ?? undefined;
pages += 1;
} while (cursor && pages < job.maxPages);
}
console.log(rows.length, "posts collected");Schedule the batch with cron or a queue worker — same pattern as the social listening guide, but scoped to a group URL list instead of platform-wide search.
To link post authors to a business page, follow up with GET /v1/facebook/profiles when author.url is present. Reference: Facebook profiles.
Store rows your ops team can filter
Normalize API responses into flat rows — Airtable, Postgres, Google Sheets:
{
"capturedAt": "2026-06-30T14:00:00.000Z",
"metro": "Austin",
"groupUrl": "https://www.facebook.com/groups/austin-buy-sell-trade",
"postId": "1234567890",
"text": "ISO queen mattress, pickup south Austin",
"commentCount": 4,
"url": "https://www.facebook.com/groups/austin-buy-sell-trade/posts/...",
"publishedAt": "2026-06-30T13:42:00.000Z",
"requestId": "req_01example",
"creditsCharged": 1
}Suggested columns:
| Column | Source | Why |
|---|---|---|
| Intent | your taxonomy | "ISO", "recommend", "complaint" — manual or rule-tagged |
| Evidence | url | Link back for quotes in ops decks |
| Weight | commentCount, reactionCount | Rank threads with replies over lone posts |
| Freshness | publishedAt | Deprioritize stale listings unless buy/sell |
| Metro | catalog tag | Groups already encode geography |
Keep requestId on every row. If a post looks wrong in your dashboard, support can trace the exact lookup.
Troubleshooting
not_found on a group that opens in your browser
- Confirm the group is Public, not closed or private. Closed groups often fail for logged-out-style lookups.
- Copy the URL from the address bar — slug renames break bookmarked links.
m.facebook.comandwww.facebook.comvariants usually work; avoid share links that redirect through login walls.
found but empty posts array
- The group resolved; there may simply be no recent public posts in the feed window.
- Try a different
sortBy—topon a quiet group can look empty whilechronologicalreturns rows. - New groups with low activity are common; empty is not a billing bug.
Duplicate posts across runs
- Dedupe on
post.idbefore alerting. - Incremental polling should stop paging when you hit a known ID, not only when the cursor ends.
Comments return not_found
- Pass the full group post permalink (
.../groups/{slug}/posts/{id}/), not just the group URL. - Very new posts may not have comment threads indexed yet — retry on the next poll.
Reaction and comment counts look stale
- Each response is a point-in-time snapshot. Re-fetch before quoting counts in a report.
- Store
capturedAtnext tocommentCount.
lookup_failed or HTTP 503
- Not charged. Retry with backoff; include
meta.requestIdfrom the failed attempt if you contact support.
Rate limits
- No hard cap on metered routes; your credit balance is the practical limit. Staying under ~500 concurrent requests is a courtesy, not a ceiling.
Billing and honest boundaries
Each group feed page and each comment page is one credit (typically 1 per successful request). A completed lookup with zero posts still ran upstream and is billed. You are not charged for pre-send validation errors, lookup_failed, or 503 temporarily_unavailable.
Rough weekly math:
| Step | Calls | Credits (approx.) |
|---|---|---|
12 groups × 2 pages, chronologicalListings | 24 | 24 |
8 groups × 1 page, chronological | 8 | 8 |
| 20 comment pulls on high-signal posts | 20 | 20 |
| Total | 52 | 52 |
Public data only — you remain responsible for lawful use under Terms. See Credits.
What you can build
- Territory inventory feeds — hourly
chronologicalListingspull with price parsing on posttext. - Service lead digests — daily chronological scan with comment enrichment on recommendation threads.
- Franchise complaint routing —
recentActivitypass with brand keywords and comment pulls for escalation. - Vendor scorecards — count how often supplier names appear in comment
textacross trade groups. - Moderation pre-filter — flag new posts against policy rules before a human opens Facebook.
Next steps: Playground · Facebook API reference · Facebook group data API use case · Pricing