---
title: "Search Facebook groups by keyword"
method: GET
path: "/facebook/search/groups"
tags: ["facebook"]
---

# Search Facebook groups by keyword

`GET /facebook/search/groups`

## Request

- Base URL: `https://www.socialcrawl.dev/v1`
- URL: `https://www.socialcrawl.dev/v1/facebook/search/groups`
- Auth: API key in header `x-api-key`

Finds public Facebook groups by keyword, so you can pick the groups to read before calling `/v1/facebook/group/posts`. Returns one row per group, in the order the search found them, on the same Author shape `/v1/facebook/group` returns: `author.id` is the group's key (its numeric id, or the vanity name of a group that set one), `author.url` is the canonical `https://www.facebook.com/groups/<key>/` form that `/v1/facebook/group/posts` and `/v1/facebook/group` take as `url` with no re-mapping, and `author.display_name` is the group's name. Groups are discovered through a public web search index limited to Facebook group pages, so a group is found either through its own page or through a post inside it: `author.ext.search_hit.match` says which (`group_page` or `post_in_group`), and `author.ext.search_hit` also carries the hit's `title`, `snippet` and `evidence_url` (the exact page that matched). A group found only through a post has `display_name: null`, because a post's title is not the group's name. The search hit carries no member count, description, privacy or creation date, so on a plain call `author.followers`, `author.bio`, `author.joined_at` and `author.ext.group` are null. Send `include=details` and every group is joined, in the same call, to the group lookup: the member count, the description, the creation date, the privacy and visibility labels, the posting activity (`author.ext.group.activity`), the numeric id (`author.ext.group.id`) and, where it was null, the name land on each row. Cost: 1 credit for the page plus 1 credit per group whose member count, description or activity was filled from a fresh lookup (at most 10 groups a page, so 11 credits at most); groups already in cache are free, groups that could not be filled are refunded, and a repeat of the same call within the cache window is 0 credits. Time: a plain page takes about 2 to 5 seconds; `include=details` adds 3 to 7 seconds on a fresh page (the lookups run in parallel and the call waits for the slowest, never more than 12 seconds) and nothing when the groups are already cached. The response carries a `hydration` block itemising rows, lookups, cache hits, credits and milliseconds. Every row also carries `computed.relevance` against your query, free, and `relevance=filter` drops the groups that are not about it. A page holds up to 10 groups, fewer when several hits belong to one group. Forward `pagination.next_cursor` as `cursor` for the next page. The same group can appear again on a later page, and once under its vanity name and once under its numeric id, so dedupe on `author.ext.group.id` after `include=details` when that matters. A group can be public in the search and still private to read: check `author.ext.group.privacy_label` before calling `group/posts`.

## Query parameters

- `query` string, required
- `page` integer
- `include` 'details'
- `relevance` 'score' | 'filter'
- `relevance_threshold` string
- `relevant_to` string
- `judgments` 'on' | 'off'
- `dry_run` '1'
- `fit` 'goal'
- `goal` string
- `fit_tokens` integer
- `cursor` string

## Headers

- `Cache-Control` string
- `Idempotency-Key` string

## Response `200`

Successful response

- object
  - `success` boolean, required: Whether the request succeeded
  - `platform` string, required: Platform name
  - `endpoint` string, required: API endpoint path
  - `data` object, required: Platform-specific response data
    - `items` object[]: Array of canonical author wrappers ({ author })
      - `author` object: Canonical Author object (unified across all platforms)
        - `id` string: Platform-specific user ID (always a string; platform-specific prefixes like `did:`, `spotify:artist:`, `t2_` are stripped)
        - `username` string, nullable: User handle or username
        - `display_name` string, nullable: Display name or full name
        - `avatar_url` string, nullable: URL to profile picture
        - `bio` string, nullable: Profile biography or description
        - `verified` boolean, nullable: Whether the account is verified
        - `followers` integer, nullable: Follower or subscriber count as an integer. Exact on Instagram and on TikTok when the source exposes an unrounded figure. YouTube above 1,000 subscribers is YouTube's published three-significant-figure figure. When the integer is a published/rounded value, `author.ext.followers_approximate` is true.
        - `following` integer, nullable: Number of accounts followed
        - `posts_count` integer, nullable: Total number of posts / videos / tracks / episodes
        - `likes_count` integer, nullable: Total likes received across the author's content (when surfaced)
        - `url` string, nullable: Direct URL to the profile page
        - `location` string, nullable: ISO region code (e.g. `US`) or freeform location string when surfaced
        - `external_url` string, nullable: Bio link / external website URL when surfaced by the platform
        - `private` boolean, nullable: Boolean at author.private
        - `joined_at` string, nullable: String at author.joined_at
        - `last_post_at` string, nullable: String at author.last_post_at
        - `ext` object, nullable: Nested object: author.ext
          - `social_context` string, nullable: String at author.ext.social_context
          - `account_created` string, nullable: String at author.ext.account_created
          - `country` string, nullable: String at author.ext.country
          - `collects_received` integer, nullable: Numeric at author.ext.collects_received
          - `former_usernames` string[], nullable: Array at author.ext.former_usernames
          - `public_email` string, nullable: String at author.ext.public_email
          - `contact_email` string, nullable: Opt-in (`contact_email=1` on instagram/profile, youtube/channel and youtube/channel/about). When the bio lists two or more email addresses, the one the creator gives for contacting them personally, copied verbatim. Null when there is one address or none, when every address belongs to a manager, agency or brand, or when the choice was not confident. `author.ext.public_email` (the first address in the bio) is unchanged.
          - `public_phone` string, nullable: String at author.ext.public_phone
          - `similar_source` string, nullable: String at author.ext.similar_source
          - `instagram_username` string, nullable: String at author.ext.instagram_username
          - `similar_via` string, nullable: String at author.ext.similar_via
          - `business_category` string, nullable: String at author.ext.business_category
          - `bio_links` object[], nullable: Array at author.ext.bio_links
            - `title` string, nullable: String at author.ext.bio_links.title
            - `url` string, nullable: String at author.ext.bio_links.url
            - `link_type` string, nullable: String at author.ext.bio_links.link_type
          - `is_business` boolean, nullable: Boolean at author.ext.is_business
          - `fbid` string, nullable: String at author.ext.fbid
          - `hd_avatar_url` string, nullable: String at author.ext.hd_avatar_url
          - `website` string, nullable: String at author.ext.website
          - `cover_url` string, nullable: String at author.ext.cover_url
          - `page_active` boolean, nullable: Boolean at author.ext.page_active
          - `employee_count` integer, nullable: Numeric at author.ext.employee_count
          - `employee_count_range` object, nullable: Nested object: author.ext.employee_count_range
            - `start` integer, nullable: Numeric at author.ext.employee_count_range.start
            - `end` integer, nullable: Numeric at author.ext.employee_count_range.end
          - `founded_year` integer, nullable: Numeric at author.ext.founded_year
          - `specialities` string[], nullable: Array at author.ext.specialities
          - `industries` string[], nullable: Array at author.ext.industries
          - `headquarters` string, nullable: Leaf at author.ext.headquarters
          - `locations` string[], nullable: Array at author.ext.locations
          - `hashtags` string[], nullable: Array at author.ext.hashtags
          - `funding` string, nullable: Leaf at author.ext.funding
          - `address` string, nullable: String at author.ext.address
          - `price_range` string, nullable: String at author.ext.price_range
          - `rating` string, nullable: String at author.ext.rating
          - `rating_count` integer, nullable: Numeric at author.ext.rating_count
          - `talking_about_count` integer, nullable: Numeric at author.ext.talking_about_count
          - `business_hours` string[], nullable: Array at author.ext.business_hours
          - `links` string[], nullable: Array at author.ext.links
          - `ad_library_page_id` string, nullable: String at author.ext.ad_library_page_id
          - `ad_library_status` string, nullable: String at author.ext.ad_library_status
          - `urn` string, nullable: String at author.ext.urn
          - `is_top_voice` boolean, nullable: Boolean at author.ext.is_top_voice
          - `is_premium` boolean, nullable: Boolean at author.ext.is_premium
          - `is_creator` boolean, nullable: Boolean at author.ext.is_creator
          - `is_influencer` boolean, nullable: Boolean at author.ext.is_influencer
          - `is_open_to_work` boolean, nullable: Boolean at author.ext.is_open_to_work
          - `is_hiring` boolean, nullable: Boolean at author.ext.is_hiring
          - `member_id` string, nullable: String at author.ext.member_id
          - `company_id` string, nullable: String at author.ext.company_id
          - `reaction_type` string, nullable: String at author.ext.reaction_type
          - `followers_approximate` boolean, nullable: True when `author.followers` is a published or rounded figure rather than an unrounded census: LinkedIn people-list display buckets, TikTok `stats.followerCount` when the exact sibling is absent, and YouTube subscriber counts at or above 1,000. Null or absent on exact counts.
          - `keywords` string, nullable: String at author.ext.keywords
          - `total_views` integer, nullable: Numeric at author.ext.total_views
          - `joined_at_timestamp` string, nullable: String at author.ext.joined_at_timestamp
          - `topicCategories` string[], nullable: Array at author.ext.topicCategories
          - `bannerExternalUrl` string, nullable: String at author.ext.bannerExternalUrl
          - `madeForKids` boolean, nullable: Boolean at author.ext.madeForKids
          - `hiddenSubscriberCount` boolean, nullable: Boolean at author.ext.hiddenSubscriberCount
          - `related_playlists` string, nullable: Leaf at author.ext.related_playlists
          - `topic_ids` string[], nullable: Array at author.ext.topic_ids
          - `unsubscribed_trailer` string, nullable: String at author.ext.unsubscribed_trailer
          - `localizations` string, nullable: Leaf at author.ext.localizations
          - `monthly_listeners` integer, nullable: Numeric at author.ext.monthly_listeners
          - `total_ratings` integer, nullable: Spotify podcasts only: how many listeners have rated the show. Cumulative over the show's whole run, so it reflects longevity as well as size, and it is NOT an audience count (Spotify publishes no play, download, subscriber or follower count for a podcast)
          - `average_rating` integer, nullable: Spotify podcasts only: mean listener rating from 0 to 5. Fractional (e.g. 4.66) despite the integer type this schema emits for every numeric leaf
          - `creator_username` string, nullable: String at author.ext.creator_username
          - `join_policy` string, nullable: String at author.ext.join_policy
          - `is_nsfw` boolean, nullable: Boolean at author.ext.is_nsfw
          - `rules` string[], nullable: Array at author.ext.rules
          - `weekly_active_users` integer, nullable: Numeric at author.ext.weekly_active_users
          - `weekly_contributions` integer, nullable: Numeric at author.ext.weekly_contributions
          - `rules_text` string, nullable: String at author.ext.rules_text
          - `language` string, nullable: String at author.ext.language
          - `post_karma` integer, nullable: Numeric at author.ext.post_karma
          - `comment_karma` integer, nullable: Numeric at author.ext.comment_karma
          - `awardee_karma` integer, nullable: Numeric at author.ext.awardee_karma
          - `trophy_count` integer, nullable: Numeric at author.ext.trophy_count
          - `banner_url` string, nullable: String at author.ext.banner_url
          - `profile_title` string, nullable: String at author.ext.profile_title
          - `social_links` string[], nullable: Array at author.ext.social_links
          - `bio_link` string, nullable: String at author.ext.bio_link
          - `embed_html` string, nullable: String at author.ext.embed_html
          - `embed_url` string, nullable: String at author.ext.embed_url
          - `group` string, nullable: Leaf at author.ext.group
          - `search_hit` object, nullable: Nested object: author.ext.search_hit
            - `match` string, nullable: Leaf at author.ext.search_hit.match
            - `title` string, nullable: String at author.ext.search_hit.title
            - `snippet` string, nullable: String at author.ext.search_hit.snippet
            - `evidence_url` string, nullable: String at author.ext.search_hit.evidence_url
            - `matched_urls` string[], nullable: Array at author.ext.search_hit.matched_urls
    - `next_cursor` string, nullable: Opaque cursor for the next page. Pass it back as a query parameter on endpoints that support pagination. Present only when the upstream reports more results.
    - `total` integer, nullable: Total number of matching results, when the upstream provides a count. Omitted otherwise.
    - `dropped` integer, required: Number of upstream list items dropped because they could not be repaired to the endpoint schema. Valid list responses include 0.
    - `_warnings` string[]: Non-fatal notices about this response (field-map drift, clamped computed values). Advisory only: its presence never means the request failed. Omitted entirely when there is nothing to report, so treat absent as 'no warnings'.
  - `credits_used` integer, required: Number of credits consumed
  - `credits_remaining` integer, nullable, required: Current account balance, or null when this response could not establish one. Free cache hits always return null because they do not establish an authoritative post-request balance. An idempotency replay also returns null when its current-balance lookup fails. It is never a fabricated zero, so a zero here means the account really is empty. The authoritative reading is the one on a billed response, which comes straight from the deduction.
  - `request_id` string, required: Unique request identifier for support
  - `cached` boolean, required: Whether the response was served from cache
  - `idempotent_replay` boolean: True only when this response is an idempotency replay
  - `meta` object: Optional. Present only when this call completed a pattern that one other call does more cheaply or in one step, at most once per pattern per API key per day. Send the header `X-SocialCrawl-Hints: off` to never receive it.
    - `hint` object: One suggestion: what to call instead and why, with the price when the suggested call costs more than the calls it replaces.
      - `code` string: creator_card | batch_profiles | search_multi | incremental_sync | comment_labels | search_repeat
      - `message` string: One sentence for a person or an agent to read.
      - `method` string: GET or POST.
      - `path` string: The call the hint points to.
  - `pagination` object, required: Cursor state for this page. Present on every list response.
    - `next_cursor` string, nullable, required: Opaque token to send back as `cursor` for the next page, or null at end-of-list. Pass it back verbatim; never decode or trim it.
    - `has_more` boolean, required: Explicit stop signal. Prefer this over inspecting next_cursor or comparing against total.
    - `page_size` integer, required: Number of items in THIS page.
    - `stopped_at` 'since' | 'known_id' | 'end' | 'null', nullable: Present only when the request sent `since` or `stop_at_id`. Why the walk stopped: `since` (this page reached a post older than the date), `known_id` (this page reached the post you already hold), `end` (the list ran out first), or null (not yet, keep walking with `next_cursor`).

## Other responses

- `400`: Invalid request - missing or malformed parameters
- `401`: Unauthorized - missing or invalid API key
- `402`: Payment required - the account balance is too low, or the calling key has spent its own per-key credit limit
- `404`: Not found - the endpoint does not exist, or the requested resource was not found upstream
- `405`: Method not allowed - wrong HTTP verb for this endpoint
- `409`: Conflict - an idempotent request is still in flight, its completed body exceeded the replay limit, or the cohort resource is not in a state that allows this operation
- `410`: Gone - the discover run's retention passed and its rows were deleted; start a new run
- `413`: Payload too large: the JSON request body exceeds the 1 MB size limit and is rejected before parsing, or a single cohort result cannot fit beneath the 1 MB response-page ceiling
- `422`: Idempotency payload mismatch - this Idempotency-Key was already used with a different request payload; or a discover request with async=false is over what one call can answer (NEEDS_ASYNC)
- `429`: Rate or concurrency limit exceeded. `RATE_LIMITED`: more than 600 requests in a 1-minute sliding window on this API key (headers `X-RateLimit-Limit`/`Remaining`/`Reset`; `Retry-After` is seconds until the window resets). `CONCURRENCY_LIMIT`: more than 50 simultaneous in-flight requests (headers `X-Concurrency-Limit`/`Remaining`; short static `Retry-After`). Both are unbilled. Honor `Retry-After`, then back off with jitter. See /docs/rate-limits.
- `500`: Internal server error - an immediate refund is attempted; credits_used reports the committed net charge and exact compensation is queued if that refund cannot commit
- `502`: Upstream error - the platform returned an error; an immediate refund is attempted and credits_used reports the committed net charge
- `503`: Service unavailable - the request is unbilled when rejected before deduction, otherwise an immediate refund is attempted and credits_used reports the committed net charge
- `504`: Gateway timeout - the request was still running close to the platform time limit and is answered as UPSTREAM_ERROR, the same code as 502. An immediate refund is attempted and credits_used reports the committed net charge. When details.retry_with is set, retryable is false and there is no Retry-After

---

[API](https://skmtc.dev/socialcrawl/apis/socialcrawl-api.md) · [All operations](https://skmtc.dev/socialcrawl/apis/socialcrawl-api/llms.txt) · [OpenAPI document](https://skmtc.dev/socialcrawl/apis/socialcrawl-api/revisions/a0ad2511004a?raw)
