Skip to main content
Socialift
Documentation sections
SDK reference

Facebook client: Graph API methods

Facebook is the Meta Graph API client in the Socialift Python SDK. It obtains a user token via Facebook Login (classic scope or a Facebook Login for Business config_id), lists managed Pages with their Page tokens, publishes posts and stories, moderates comments, subscribes Pages to webhooks and reads leads from Lead Ads forms. Tokens are passed to every method and never stored on the client.

from socialift import Facebook25 methodsas of socialift 1.3.0Updated:

How do I set up the Facebook client?

Create the client with Facebook(app_id=..., app_secret=...). Both values are required; an empty one raises ConfigurationError. Facebook.from_env() reads them from FACEBOOK_APP_ID and FACEBOOK_APP_SECRET, the optional config_id from FACEBOOK_CONFIG_ID and the Graph API version from META_GRAPH_VERSION (falling back to the SDK default).

The App ID and App Secret are in your app settings on Meta for Developers — the "Meta app setup" guide walks through it. For a Business-type app (Facebook Login for Business), permissions are chosen in a configuration in the App Dashboard and its config_id goes into the login URL; otherwise the scopes list is sent. You can store the keys and validate them with the provider in the dashboard's Keys section.

Token types: me, pages, granted_scopes and revoke take a user token; post, comment, webhook and lead methods take a Page token — Page.access_token returned by pages(). appsecret_proof is added to every request automatically.

The license key comes from the SOCIALIFT_LICENSE_KEY environment variable or the license_key= argument. Use the client with async with so the HTTP session is closed deterministically.

python
import asyncio
import os

from socialift import Facebook

async def main():
    # 1) Explicit arguments
    fb = Facebook(
        app_id=os.environ["FACEBOOK_APP_ID"],
        app_secret=os.environ["FACEBOOK_APP_SECRET"],
        config_id=os.environ.get("FACEBOOK_CONFIG_ID"),  # Login for Business
    )
    # 2) Or from the environment: FACEBOOK_APP_ID, FACEBOOK_APP_SECRET,
    #    FACEBOOK_CONFIG_ID, META_GRAPH_VERSION
    # fb = Facebook.from_env()

    async with fb:
        user_token = os.environ["FB_USER_TOKEN"]  # from exchange_code()
        for page in await fb.pages(user_token):
            print(page.id, page.name, page.instagram_username)

asyncio.run(main())

Client and OAuth

Creating the client, the Facebook Login URL, token exchange, checking and revoking permissions.

exchange_codefrom_envgranted_scopeslogin_urlrevoke

Exchanges the callback code for a short-lived user token and immediately for a long-lived one, returning a TokenBundle. Call it once in your OAuth callback.

await fb.exchange_code(code: str, *, redirect_uri: str) -> socialift.models.TokenBundle
  • redirect_uri must be identical to the one in the login URL (Meta requirement).
  • Per Meta, a long-lived user token generally lasts about 60 days; read TokenBundle.expires_at and store the token yourself.
  • Raises ApiError if Meta returns no token.

from_env

classmethod

Creates a client from environment variables: FACEBOOK_APP_ID, FACEBOOK_APP_SECRET, FACEBOOK_CONFIG_ID and META_GRAPH_VERSION. Use it in server apps so keys never live in code.

Facebook.from_env(**kwargs) -> Facebook
  • Raises ConfigurationError if FACEBOOK_APP_ID or FACEBOOK_APP_SECRET is empty.
  • Extra keyword arguments (timeout, retry, session, license_key) are passed to the constructor.

Returns the permissions actually granted to a token, via debug_token. Users can decline some permissions in the dialog, so enable features from this list rather than from what you requested.

await fb.granted_scopes(token: str) -> list[str]
  • The call uses the app token (app_id|app_secret); pass the token you want to inspect.
  • Returns an empty list when no scopes are present.

Builds the Facebook Login dialog URL to redirect the user to; it is synchronous and sends no request. With a config_id (argument or client) it uses Facebook Login for Business, otherwise the scopes list.

fb.login_url(
    *,
    redirect_uri: str,
    state: Optional[str] = None,
    config_id: Optional[str] = None,
    scopes: Optional[list[str]] = None,
) -> str
  • The SDK does not store state: generate a random value (e.g. secrets.token_urlsafe(32)), keep it in the session and compare it in the callback — Meta documents it as CSRF protection.
  • With config_id, scope is not sent at all: permissions come from the configuration in the App Dashboard. Login for Business only works with Business-type apps.
Example — python
url = fb.login_url(redirect_uri="https://siz.uz/cb", state=state, config_id="...")

revoke

async

Revokes all of the app's permissions on Facebook's side (DELETE /{user_id}/permissions) and returns a bool. Call it when a user disconnects — deleting the token from your database is not enough.

await fb.revoke(token: str, *, user_id: str = 'me') -> bool
  • Per Meta, once all permissions are revoked the user's access tokens are invalidated and they must log in again.
  • Call it with a user token; user_id defaults to me.
Example — python
await fb.revoke(user_token)

Profile and Pages

The user's profile and the list of Pages with their Page tokens.

mepages

me

async

Returns the token owner's Facebook profile as a Profile: id, name, picture and email. Use it right after login to identify who connected.

await fb.me(token: str) -> socialift.models.Profile
  • Needs a user token, not a Page token.
  • email may be empty; the full response is in Profile.raw.

pages

async

Lists the Pages the user manages as Page objects, each with its Page token (access_token) and linked Instagram professional account (instagram_id, instagram_username). Post, comment, webhook and lead methods take their token from here.

await fb.pages(token: str) -> list[socialift.models.Page]
  • Call it with a user token.
  • Per Meta, a Page token obtained with a long-lived user token has no expiration date and is only invalidated under certain conditions.
  • Page.access_token is secret: never send it to a frontend or log it (it is hidden from repr).
  • Pagination is automatic, up to 500 Pages.

Required permissions:pages_show_list

Example — python
for page in await fb.pages(user_token):
    print(page.name, page.instagram_username)

Webhook subscriptions

Subscribing a Page to your app's webhooks, checking and removing it.

subscribe_pagesubscriptionsunsubscribe_page

Subscribes a Page to your app's webhooks (POST /{page_id}/subscribed_apps). Even with webhooks configured at app level, a Page sends no events until it is subscribed.

await fb.subscribe_page(
    page_id: str,
    page_token: str,
    fields: Optional[list[str]] = None,
) -> bool
  • Needs a Page token; Meta requires pages_manage_metadata and pages_show_list.
  • Without fields the SDK sends leadgen, leadgen_update, feed, messages, messaging_postbacks, mention. Meta also requires pages_messaging for the messages field — if your app lacks it, pass fields explicitly.
  • Meta rule: Instagram webhooks cannot be configured through subscribed_fields.

Required permissions:pages_manage_metadata

Example — python
await fb.subscribe_page(page_id, page_token, fields=["leadgen", "feed"])

Returns which apps and fields a Page is subscribed to. Use it to diagnose missing webhook events.

await fb.subscriptions(page_id: str, page_token: str) -> list[dict]
  • Call it with a Page token; the raw data array from Graph API is returned.
Example — python
apps = await fb.subscriptions(page_id, page_token)

Removes your app's webhook subscription from a Page (DELETE /{page_id}/subscribed_apps) and returns a bool. Call it when a customer disconnects the Page.

await fb.unsubscribe_page(page_id: str, page_token: str) -> bool
  • Afterwards no lead, comment or other events arrive from that Page.
Example — python
await fb.unsubscribe_page(page_id, page_token)

Posts and insights

Reading, publishing and deleting Page posts, plus post insights.

delete_postpost_insightspostspublish_post

Permanently deletes a Page post (DELETE /{post_id}) and returns a bool. This cannot be undone.

await fb.delete_post(post_id: str, page_token: str) -> bool
  • Needs a Page token; post_id is the id from posts() or publish_post().

Required permissions:pages_manage_posts

Example — python
await fb.delete_post(post_id, page_token)

Returns post metrics as a {metric: value} dict. The SDK requests post_impressions, post_impressions_unique, post_engaged_users, post_clicks and post_reactions_by_type_total.

await fb.post_insights(post_id: str, page_token: str) -> dict[str, Any]
  • Never raises on API errors — it returns {} instead (for example, an old post without insights).
  • Meta deprecated post_impressions for all API versions on November 15, 2025, and post_impressions_unique is also on the deprecated list (replacements: post_media_view, post_total_media_view_unique). Graph API returns an error for deprecated metrics, so this method can come back as {}.
  • Needs a Page token; Meta requires read_insights and pages_read_engagement.

Required permissions:read_insightspages_read_engagement

Example — python
stats = await fb.post_insights(post_id, page_token)

posts

async

Reads the Page's published posts from /published_posts. /feed is not used because it also includes posts by other people on the Page.

await fb.posts(page_id: str, page_token: str, *, limit: int = 25) -> list[dict]
  • Call it with a Page token.
  • limit is the total number of posts returned; the SDK follows pagination.
  • Per Meta, the endpoint returns roughly 600 published posts per year.

Required permissions:pages_read_engagement

Example — python
posts = await fb.posts(page_id, page_token, limit=25)

Publishes a text post (optionally with a link) as the Page to /{page_id}/feed and returns Meta's response with the post id. published=False sends published=false, so the post is not published immediately.

await fb.publish_post(
    page_id: str,
    page_token: str,
    *,
    message: str,
    link: Optional[str] = None,
    published: bool = True,
) -> dict
  • Needs a Page token. Meta's Pages API guide lists pages_manage_posts, pages_manage_engagement, pages_read_engagement and the CREATE_CONTENT, MANAGE and MODERATE tasks on the Page.
  • Text and link only: this method does not upload photos or videos.
  • scheduled_publish_time is not sent, so this is not scheduling.

Required permissions:pages_manage_postspages_read_engagement

Example — python
post = await fb.publish_post(page_id, page_token, message="Salom!", link="https://siz.uz")

Page stories

Publishing photo and video stories, listing them and checking video upload status. A story lives 24 hours.

publish_photo_storypublish_video_storystoriesvideo_story_status

Publishes a photo as a Page story and returns {photo_id, post_id, success}. Two steps: the photo is first uploaded to POST /{page-id}/photos with published=false, then that photo_id is posted to the photo_stories edge.

await fb.publish_photo_story(
    page_id: str,
    page_token: str,
    *,
    photo_url: Optional[str] = None,
    photo_id: Optional[str] = None,
) -> dict
  • Meta's photo_stories edge accepts only a photo_id, never a URL, so the first step cannot be skipped — the SDK performs it for you.
  • Pass exactly one of photo_url (a public URL that Meta downloads) and photo_id (a photo you already uploaded unpublished). If the second step failed, reuse photo_id instead of uploading again.
  • Meta's rules: .jpeg, .bmp, .png, .gif or .tiff up to 10 MB (1 MB recommended for .png). Media from a previous post cannot be reused — every story needs a fresh upload.
  • An unpublished photo stays on Facebook's servers for about 24 hours, which is also how long the photo_id is valid.
  • Permissions: pages_manage_posts, pages_read_engagement, pages_show_list and the CREATE_CONTENT task on the Page. Call it with a Page token.

Required permissions:pages_manage_postspages_read_engagementpages_show_list

Example — python
story = await fb.publish_photo_story(page_id, page_token,
                                        photo_url="https://cdn.siz.uz/story.jpg")

Publishes a video as a Page story and returns {video_id, post_id, success}. Meta requires a three-phase resumable upload here: upload_phase=start → upload the file to the upload_url from the response → upload_phase=finish.

await fb.publish_video_story(
    page_id: str,
    page_token: str,
    *,
    video_url: Optional[str] = None,
    video: Union[bytes, bytearray, str, os.PathLike, NoneType] = None,
    wait_timeout: float = 300.0,
    upload_attempts: int = 3,
) -> dict
  • Pass exactly one of video_url (a public URL Meta downloads itself) and video (bytes or a local file path).
  • The upload_url comes from the response and is used as-is (it points at rupload.facebook.com); in the second phase the token travels in an Authorization: OAuth header, not in the query string.
  • If the upload breaks off, Meta reports how many bytes arrived and the SDK resumes from there — up to upload_attempts tries (3 by default) without resending the whole file.
  • Before the third phase the SDK waits for the upload status to reach complete, up to wait_timeout (300 seconds by default). On timeout it raises ApiError whose payload still carries the video_id, so you can inspect it with video_story_status().
  • Meta's rules (the SDK cannot check them): .mp4, H.264 or H.265, AAC audio at 48 kHz; 9:16, between 540x960 and 1080x1920, 24–60 FPS. Meta's docs give two different duration limits (3–90 seconds and "no longer than 60 seconds"), so staying within 3–60 seconds is the safe choice.
  • Quality errors such as "Resolution too low" surface in the post-upload status rather than at the third phase; the full message is kept in ApiError.payload.

Required permissions:pages_manage_postspages_read_engagementpages_show_list

Example — python
story = await fb.publish_video_story(page_id, page_token,
                                        video_url="https://cdn.siz.uz/story.mp4")

stories

async

Returns the Page's stories: each entry has post_id, status, creation_time, media_type, media_id and url. The post_id from publish_photo_story()/publish_video_story() matches an entry here, which is how you track a published story.

await fb.stories(page_id: str, page_token: str, *, limit: int = 25) -> list[dict]
  • The SDK deliberately sends no fields: Meta does not document field selection for this edge, and an unknown fields value makes Graph API reject the entire request.
  • Archived stories are only returned when the Page has archiving enabled.
  • Permissions: pages_read_engagement and pages_show_list; call it with a Page token.

Required permissions:pages_read_engagementpages_show_list

Example — python
stories = await fb.stories(page_id, page_token, limit=25)

Returns the upload and processing status of a video story (GET /{video-id}?fields=status): uploading_phase, processing_phase and publishing_phase, each with a status of not_started, in_progress, complete or error.

await fb.video_story_status(video_id: str, page_token: str) -> dict
  • During upload there is an extra bytes_transfered field (Meta's own spelling) that tells you where to resume an interrupted upload.
  • When publish_video_story() fails with ApiError, the video_id remains in the exception's payload, and this method is how you find out what happened.
  • Call it with a Page token.

Comments

Reading post comments, replying and hiding them.

commentshide_commentreply_comment

Returns comments on a post, newest first (order=reverse_chronological). Use it to fill a moderation queue or sync comments into a CRM.

await fb.comments(post_id: str, page_token: str, *, limit: int = 50) -> list[dict]
  • Needs a Page token. Per Meta, getting comment IDs on a Page post requires moderator rights on that Page.
  • No filter is sent, so Graph API's default applies: top-level comments only, replies are not included.
  • limit is the total number of comments; pagination is automatic.

Required permissions:pages_read_engagement

Example — python
comments = await fb.comments(post_id, page_token, limit=50)

Hides a comment (is_hidden=true) or unhides it with hidden=False. A reversible alternative to deleting.

await fb.hide_comment(comment_id: str, page_token: str, hidden: bool = True) -> dict
  • Per Meta, is_hidden only applies to Page comments.
  • Call it with a Page token.

Required permissions:pages_manage_engagement

Example — python
await fb.hide_comment(comment_id, page_token, hidden=True)

Replies to a comment as the Page (POST /{comment_id}/comments) and returns the new comment id.

await fb.reply_comment(comment_id: str, page_token: str, message: str) -> dict
  • Per Meta, commenting needs a Page token from a user who can perform the MODERATE task on the Page and the pages_manage_engagement permission.

Required permissions:pages_manage_engagement

Example — python
await fb.reply_comment(comment_id, page_token, "Rahmat!")

Lead Ads

Lead forms, leads and the ad context a lead came from.

ad_contextleadlead_formsleads

Returns the context of the ad a lead came from: ad, ad set and campaign IDs and names, objective, optimization_goal and targeting. Useful for showing lead source in a CRM.

await fb.ad_context(ad_id: Optional[str], token: str) -> dict
  • Never raises: on an API error (e.g. missing ads_management) or an empty ad_id it returns {}.
  • Keys: ad_id, ad_name, adset_id, adset_name, campaign_id, campaign_name, objective, optimization_goal, targeting.

Required permissions:ads_management

Example — python
context = await fb.ad_context(ad_id, token)

lead

async

Fetches one full lead by the leadgen_id from a leadgen webhook event and returns a Lead. This is the main path for pushing leads into a CRM in real time.

await fb.lead(leadgen_id: str, page_token: str) -> socialift.models.Lead
  • Meta's webhook carries only IDs (leadgen_id, page_id, form_id, adgroup_id, ad_id, created_time); the form answers are fetched with this method.
  • Needs a Page token with leads_retrieval.

Returns the Page's Lead Ads forms as LeadForm objects: name, status, locale, questions and leads count. Use it to pick a form and map its questions to CRM fields.

await fb.lead_forms(page_id: str, page_token: str) -> list[socialift.models.LeadForm]
  • Needs a Page token; pagination is automatic and the raw response is in LeadForm.raw.

Required permissions:leads_retrievalpages_manage_ads

Example — python
forms = await fb.lead_forms(page_id, page_token)

leads

async

Returns the leads submitted to one form as Lead objects. Pass since (unix timestamp) to fetch only newer leads — for example, to backfill a period your webhook missed.

await fb.leads(
    form_id: str,
    page_token: str,
    *,
    since: Optional[int] = None,
    limit: int = 200,
) -> list[socialift.models.Lead]
  • since is sent to Graph API as a GREATER_THAN filter on time_created.
  • Lead.fields holds normalized fields (full_name, email, phone, ...); Lead.field_data is Meta's raw list.
  • Per Meta, reading leads needs leads_retrieval, ads_management, pages_show_list, pages_read_engagement, pages_manage_ads and a token from someone who can advertise on the ad account.
  • limit defaults to 200; pagination is automatic.

Required permissions:leads_retrieval

Example — python
leads = await fb.leads(form_id, page_token, limit=200)
for lead in leads:
    print(lead.fields.get("phone"), lead.campaign_name)

Methods shared by all clients

Every HTTP client (Telegram, Facebook, Instagram, LinkedIn, TikTok, Ads) has these low-level methods — for calling an endpoint the SDK doesn't wrap yet or for reading pages of results. The connection is closed automatically when the async with block ends.

request

async

Sends an arbitrary HTTP request and returns the JSON response — for endpoints the SDK doesn't wrap with a dedicated method yet. You pass the token and signing parameters yourself in params or headers (access_token, and for Meta appsecret_proof via socialift.crypto.appsecret_proof).

await fb.request(
    method: str,
    url: str,
    *,
    params: Optional[Mapping[str, Any]] = None,
    data: Optional[Any] = None,
    json: Optional[Any] = None,
    headers: Optional[Mapping[str, str]] = None,
) -> Any
  • None values in params are not sent.
  • Rate limits (RateLimitError) and transient server errors (HTTP 429, 500, 502, 503, 504) are retried according to RetryPolicy; a Retry-After header is honoured. Network failures are raised as TransportError.

get

async

Shorthand for request("GET", ...).

await fb.get(url: str, **kw) -> Any

post

async

Shorthand for request("POST", ...).

await fb.post(url: str, **kw) -> Any

delete

async

Shorthand for request("DELETE", ...).

await fb.delete(url: str, **kw) -> Any

Follows Meta's cursor pagination (paging.next) and returns every data item as a single list.

await fb.paginate(
    url: str,
    *,
    params: Optional[Mapping[str, Any]] = None,
    max_items: int = 500,
    page_size: int = 100,
) -> list[dict]
  • max_items (default 500) is a safety cap — a large account can have tens of thousands of records. page_size sets the limit of each request.

close

async

Closes the HTTP session the SDK created itself (a session you passed in is left open). Called automatically when you use async with Client(...).

await fb.close() -> None

Frequently asked questions

How do I get a Facebook Page access token in Python?

Send the user to Facebook Login with the URL from login_url, then exchange the callback code for a long-lived user token with exchange_code. pages(user_token) then returns Page.access_token for every Page. According to Meta, a Page token obtained with a long-lived user token has no expiration date and is only invalidated under certain conditions.

Why are no lead or comment webhooks arriving?

Configuring webhooks at app level is not enough: each Page must be subscribed to your app with subscribe_page. Check the subscription with subscriptions. Meta requires pages_manage_metadata for the subscription and additionally pages_messaging for the messages field.

How do I retrieve Facebook Lead Ads leads with Python?

A leadgen webhook event only carries IDs (leadgen_id, form_id, ad_id and others). Fetch the full answers with lead(leadgen_id, page_token) and backfill missed periods with leads(form_id, page_token, since=...). Lead.fields contains normalized fields such as name, phone and email.

What is config_id in Facebook Login for Business?

In a Business-type app, permissions are selected in a configuration in the Meta App Dashboard instead of a scope in code, and the configuration ID is added to the login URL as config_id. When the SDK has a config_id (on the client or in login_url), it does not send scope at all.

How do I publish a Facebook Page story through the API?

A photo takes two steps: publish_photo_story(page_id, page_token, photo_url=...) first uploads the photo with published=false, then posts the resulting photo_id to the photo_stories edge — Meta accepts only a photo_id there, never a URL. Video needs a three-phase upload (open a session → upload the file to the upload_url Meta returns → publish), which publish_video_story() handles. Permissions: pages_manage_posts, pages_read_engagement, pages_show_list and the CREATE_CONTENT task on the Page.

Does the Socialift SDK support Facebook Messenger?

No. The Facebook client has no methods for reading or sending Messenger messages. For Instagram Direct, use conversations and send_message on the Instagram client.

Sources

  1. Meta: Long-lived access tokens
  2. Meta: Facebook Login for Business
  3. Meta: Webhooks for Pages
  4. Meta: Pages API — Posts
  5. Meta: Page Stories API
  6. Meta: Retrieving leads
  7. Meta: Deprecated Page Insights metrics

Related pages