# Facebook (/docs/facebook) Facebook pages, posts, comments, reels, photos, groups, events, Marketplace, and the Ad Library through one API key Public Facebook data on a plain `GET`: page profiles and their feeds, post comment trees, reels and photo galleries, group posts, the events directory, Marketplace listings, and the full Facebook Ad Library. Nearly all of it is 1 credit, which makes Facebook one of the cheapest platforms here to crawl at volume. Base URL: `/v1/facebook/...` Reply threads do not take a comment id. `/v1/facebook/post/comment/replies` needs the `feedback_id` **and** `expansion_token` that `/v1/facebook/post/comments` returns for that comment. Neither is the comment's own id, and there is no way to construct them. You have to make the parent call first. ## Quickstart ### 1. Fetch a profile ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/facebook/profile?url=https://www.facebook.com/Meta" \ -H "x-api-key: YOUR_API_KEY" ``` ### 2. Fetch their content ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/facebook/profile/posts?url=https://www.facebook.com/Meta" \ -H "x-api-key: YOUR_API_KEY" ``` ### 3. Read computed fields When an endpoint supports a computed field and the required source inputs are present, the unified response includes that optional field. Depending on the endpoint, optional fields can include `engagement_rate`, `language`, `content_category`, and `estimated_reach`. See [Computed fields](/docs/computed-fields.md) for formulas, clamping rules, and null semantics. Where an endpoint supports them, list rows also carry judged labels (`computed.labels`, `computed.relevance`) by default at no extra credits; see [Labels](/docs/labels.md). ## Pages | Endpoint | Credits | What it returns | Key parameters | | --------------------------------- | ------- | ------------------------------------------------------------------- | --------------------------------------------- | | `GET /v1/facebook/profile` | 1 | Page identity, category, contact details, and the page id | `url`, `get_business_hours` | | `GET /v1/facebook/profile/full` | 5 | The page, its recent posts, and computed analytics in one flat call | `url`, `posts` (1-100, default 25), `include` | | `GET /v1/facebook/profile/posts` | 1 | The page's recent posts, cursor-paged | `url` or `pageId`, `cursor` | | `GET /v1/facebook/profile/events` | 1 | One page's own upcoming and past events | `url`, `cursor` | The profile response carries a page id. Pass it back as `pageId` on `profile/posts` instead of the `url` and the lookup gets faster. `pageId` is also what the Ad Library indexes an advertiser by, so it is worth holding. `profile/full` adds two average engagement rates, posting cadence, top post, and format mix over a window of `posts`. `avg_engagement_rate` divides each post's engagement by its view count, so it is null when no post in the window has one; `avg_engagement_rate_by_followers` divides likes and comments by the follower count and covers posts with no view count. The profile still comes back if the posts leg fails, with the post-dependent metrics null. Facebook keeps a post's share count, and a reel's exact view count and duration, on the permalink rather than in the feed, so on a plain `profile/posts` call those leaves are null. Send `include=engagement` and every row is joined to its own post record in the same call: 1 credit for the page plus 1 per row filled, 4 at most, with rows already in cache filled for free and anything the lookup could not fill refunded. Reach for it whenever you were about to walk the list and call `post` once per row. The same `include` pattern covers photos and both event lists; [Fill the rows, or fetch them?](/docs/choosing-endpoints.md#facebook-include) compares it against the alternatives. ## Posts and comments | Endpoint | Credits | What it returns | Key parameters | | --------------------------------------- | ------- | -------------------------------------------------------------- | --------------------------------------- | | `GET /v1/facebook/post` | 1 | One post in full, including the reactions breakdown | `url`, `get_comments`, `get_transcript` | | `GET /v1/facebook/post/comments` | 1 | The comment list, walked by cursor | `url` or `feedback_id`, `cursor`, `label`, `judgments` | | `GET /v1/facebook/post/comment/replies` | 1 | One reply thread | `feedback_id` **and** `expansion_token`, `label`, `judgments` | | `GET /v1/facebook/post/transcript` | 10 | The spoken words of a video post, from Facebook's own captions | `url` | `post` is the only endpoint that carries the **reactions breakdown**, the split between like, love, haha and the rest, rather than a single total. `get_comments=true` folds in the first several comments and `get_transcript=true` folds in a video transcript, so one call can cover a whole post when you do not need to page. On `post/comments`, pass `feedback_id` instead of `url` when you have one from a prior `post` call: it is materially faster. That same response is where each comment's `feedback_id` and `expansion_token` come from. ## Media | Endpoint | Credits | What it returns | Key parameters | | ------------------------------------- | ------- | ----------------------------------------------------------------- | ----------------------- | | `GET /v1/facebook/profile/photos` | 1 | A page's gallery: photo id, permalink, full-size image, thumbnail | `url`, `next_page_id` | | `GET /v1/facebook/profile/reels` | 1 | The cheap reel list, with the rounded public view count | `url`, `next_page_id` | | `GET /v1/facebook/profile/reels/full` | 5 to 25 | The same list with exact per-reel engagement merged in | `url`, `limit` (max 50) | `profile/photos` also returns the accessibility caption where Facebook generated one, but the plain listing carries no author, engagement, or publish time per photo. `include=details` fills all of it in the same call, at 1 credit for the page plus 1 per photo filled and 9 at most, so use it instead of passing each permalink to `post` yourself. A photo posted inside a multi-photo post carries its own counts rather than the parent post's. `profile/reels` has no likes, comments, or shares. `profile/reels/full` is metered: it consumes upstream pages of 10 reels and bills 5 credits per page, from 5 up to 25 for a `limit=50` walk. If the walk hits its internal time budget the response carries `_warnings: ["walk_deadline_reached"]`, the unfetched pages are refunded, and `next_cursor` resumes exactly where it stopped. ## Groups | Endpoint | Credits | What it returns | Key parameters | | ------------------------------ | ------- | ----------------------------------------------------------- | ---------------------------------------- | | `GET /v1/facebook/group` | 1 | Group identity: name, description, member count, privacy | `url` or `group_id` | | `GET /v1/facebook/group/posts` | 1 | That group's posts with reaction, comment, and share counts | `url` or `group_id`, `sort_by`, `cursor` | Facebook serves the group feed in slices of 3 to 4 posts, so depth is a cursor walk: send `pagination.next_cursor` back as `cursor` and repeat until `has_more` is false. Each page is 1 credit. `sort_by=CHRONOLOGICAL` is the setting for keeping a busy group current; the default and `TOP_POSTS` mix older high-engagement posts into the first pages. ## Events | Endpoint | Credits | What it returns | Key parameters | | -------------------------------- | ------- | ------------------------------------------------------------- | ----------------------- | | `GET /v1/facebook/events` | 1 | What is on in a place, from a city's Facebook Events page URL | `url`, `time`, `cursor` | | `GET /v1/facebook/events/search` | 1 | The public directory searched by keyword | `query`, `cursor` | | `GET /v1/facebook/event/details` | 1 | Description, start and end times, host, and RSVP counts | `id` or `url` | Three entry points into the same event objects. `events/search` also returns events that have already happened, so filter on `is_past` yourself. Both listings take `include=details`, which joins every row to its own event record in one call and fills the description, street address, coordinates, hosts, category, privacy, and RSVP counts the listing leaves out: 1 credit for the page plus 1 per event filled, 9 at most on a page's own events and 13 on a city feed, with cached rows free and unfilled rows refunded. A page's events tab carries no cover image and no RSVP counts of its own, so those arrive with `include=details` too. Call `event/details` directly when you already hold a single event id rather than a listing. ## Marketplace | Endpoint | Credits | What it returns | Key parameters | | ---------------------------------------------- | ------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GET /v1/facebook/marketplace/location/search` | 1 | A city or area name turned into a lat/lng pair | `query` | | `GET /v1/facebook/marketplace/search` | 1 | Listings near those coordinates | `query`, `lat`, `lng`, all required, plus `radius_km`, `min_price`, `max_price`, `condition`, `delivery_method`, `date_listed`, `availability`, `sort_by` | | `GET /v1/facebook/marketplace/item` | 1 | One listing in full | `id` or `url` | Marketplace search is geographic, not textual, which is why the coordinate lookup comes first. ```bash title="cURL" curl "https://www.socialcrawl.dev/v1/facebook/marketplace/location/search?query=Austin" \ -H "x-api-key: YOUR_API_KEY" curl "https://www.socialcrawl.dev/v1/facebook/marketplace/search?query=bike&lat=30.2672&lng=-97.7431&radius_km=40&max_price=500" \ -H "x-api-key: YOUR_API_KEY" ``` De-duplicate on listing id. The ordering shifts between calls, so consecutive cursor pages occasionally repeat an item. ## Ad Library | Endpoint | Credits | What it returns | Key parameters | | --------------------------------------------- | ------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `GET /v1/facebook/adlibrary/search/companies` | 5 | An advertiser name turned into a `pageId` | `query` | | `GET /v1/facebook/adlibrary/company/ads` | 5 | Everything that page is running | `pageId` or `companyName`, `country`, `status`, `media_type`, `language`, `sort_by`, `start_date`, `end_date` | | `GET /v1/facebook/adlibrary/search/ads` | 5 | Ad _copy_ searched by keyword across every advertiser | `query`, `search_type`, `ad_type`, `country`, `status`, `media_type` | | `GET /v1/facebook/adlibrary/ad` | 5 | One ad expanded into creative, spend, impressions, and targeting | `id` or `url`, `trim` | | `GET /v1/facebook/adlibrary/ad/transcript` | 10 | What a video ad says out loud | `id` or `url` | `search_type` gives exact-phrase matching, and `ad_type` restricts to political and issue ads. The transcript uses Facebook's captions where they exist and transcribes the video where they do not. ## All endpoints ## Platform notes **`author.following` and `author.posts_count` are always null.** The profile upstream does not publish those totals for a Facebook page. **`country` takes exactly one code.** Every Ad Library endpoint that accepts `country` accepts a single two-letter code, not a list. It defaults to `ALL`. **Ad Library status defaults to `ACTIVE`.** Historical ads are excluded unless you set `status` explicitly, which is a common reason a competitor sweep looks emptier than expected. **`trim=true` on the ad endpoints.** Ad Library payloads are large. The trimmed variant carries the same rows with the heavy creative metadata dropped. **Group posts come 3 to 4 at a time.** The page size is small by Facebook's design, so a day's worth of a busy group is several cursor pages rather than one call. Budget 1 credit per page and loop on `has_more`. **Multi-photo posts return the full `media_urls` array**, not just the first image. ## Labels These lists label their rows by default, at no extra credit. On `/v1/facebook/post/comments` and `/v1/facebook/post/comment/replies`, each comment carries `sentiment`, `question`, `purchase_intent` and `complaint` on `computed.labels`. On `/v1/facebook/search/posts`, each row carries `sponsored`, `intent` and `niche`. `/v1/facebook/search/posts` also scores each row's relevance to your `query` on `computed.relevance`, without dropping or reordering anything; add `relevance=filter` to drop the rows that are about something else, still free. Send `judgments=off` for the page without them. See [Labels](/docs/labels.md) for the field shapes, rows still `pending`, and the labels you can add with `label=`. ## Next steps - [Pagination](/docs/pagination.md): Cursor and page walks, and what has_more actually means. - [Credits](/docs/credits.md): What each tier costs and when a call is refunded. - [API reference](/docs/api-reference.md): Every parameter and response field, endpoint by endpoint. - [Instagram](/docs/instagram.md): The other Meta surface, same handle-first shape. - [LinkedIn](/docs/linkedin.md): The other ad library plus the whole B2B profile surface.