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.
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
exchange_code
asyncExchanges 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.TokenBundleredirect_urimust 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_atand store the token yourself. - Raises
ApiErrorif Meta returns no token.
from_env
classmethodCreates 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
ConfigurationErrorifFACEBOOK_APP_IDorFACEBOOK_APP_SECRETis empty. - Extra keyword arguments (
timeout,retry,session,license_key) are passed to the constructor.
granted_scopes
asyncReturns 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,scopeis not sent at all: permissions come from the configuration in the App Dashboard. Login for Business only works with Business-type apps.
url = fb.login_url(redirect_uri="https://siz.uz/cb", state=state, config_id="...")revoke
asyncRevokes 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_iddefaults tome.
await fb.revoke(user_token)Profile and Pages
The user's profile and the list of Pages with their Page tokens.
me
asyncReturns 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.
emailmay be empty; the full response is inProfile.raw.
pages
asyncLists 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_tokenis secret: never send it to a frontend or log it (it is hidden fromrepr).- Pagination is automatic, up to 500 Pages.
Required permissions:pages_show_list
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
subscribe_page
asyncSubscribes 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_metadataandpages_show_list. - Without
fieldsthe SDK sendsleadgen,leadgen_update,feed,messages,messaging_postbacks,mention. Meta also requirespages_messagingfor themessagesfield — if your app lacks it, passfieldsexplicitly. - Meta rule: Instagram webhooks cannot be configured through
subscribed_fields.
Required permissions:pages_manage_metadata
await fb.subscribe_page(page_id, page_token, fields=["leadgen", "feed"])subscriptions
asyncReturns 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
dataarray from Graph API is returned.
apps = await fb.subscriptions(page_id, page_token)unsubscribe_page
asyncRemoves 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.
await fb.unsubscribe_page(page_id, page_token)Posts and insights
Reading, publishing and deleting Page posts, plus post insights.
delete_postpost_insightspostspublish_post
delete_post
asyncPermanently 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_idis theidfromposts()orpublish_post().
Required permissions:pages_manage_posts
await fb.delete_post(post_id, page_token)post_insights
asyncReturns 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_impressionsfor all API versions on November 15, 2025, andpost_impressions_uniqueis 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_insightsandpages_read_engagement.
Required permissions:read_insightspages_read_engagement
stats = await fb.post_insights(post_id, page_token)posts
asyncReads 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.
limitis 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
posts = await fb.posts(page_id, page_token, limit=25)publish_post
asyncPublishes 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_engagementand theCREATE_CONTENT,MANAGEandMODERATEtasks on the Page. - Text and link only: this method does not upload photos or videos.
scheduled_publish_timeis not sent, so this is not scheduling.
Required permissions:pages_manage_postspages_read_engagement
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
publish_photo_story
asyncPublishes 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_storiesedge accepts only aphoto_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) andphoto_id(a photo you already uploaded unpublished). If the second step failed, reusephoto_idinstead 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_idis valid. - Permissions:
pages_manage_posts,pages_read_engagement,pages_show_listand theCREATE_CONTENTtask on the Page. Call it with a Page token.
Required permissions:pages_manage_postspages_read_engagementpages_show_list
story = await fb.publish_photo_story(page_id, page_token,
photo_url="https://cdn.siz.uz/story.jpg")publish_video_story
asyncPublishes 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) andvideo(bytes or a local file path). - The
upload_urlcomes from the response and is used as-is (it points at rupload.facebook.com); in the second phase the token travels in anAuthorization: OAuthheader, 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_attemptstries (3 by default) without resending the whole file. - Before the third phase the SDK waits for the upload status to reach
complete, up towait_timeout(300 seconds by default). On timeout it raisesApiErrorwhosepayloadstill carries thevideo_id, so you can inspect it withvideo_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
story = await fb.publish_video_story(page_id, page_token,
video_url="https://cdn.siz.uz/story.mp4")stories
asyncReturns 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 unknownfieldsvalue makes Graph API reject the entire request. - Archived stories are only returned when the Page has archiving enabled.
- Permissions:
pages_read_engagementandpages_show_list; call it with a Page token.
Required permissions:pages_read_engagementpages_show_list
stories = await fb.stories(page_id, page_token, limit=25)video_story_status
asyncReturns 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_transferedfield (Meta's own spelling) that tells you where to resume an interrupted upload. - When
publish_video_story()fails withApiError, thevideo_idremains in the exception'spayload, 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
hide_comment
asyncHides 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_hiddenonly applies to Page comments. - Call it with a Page token.
Required permissions:pages_manage_engagement
await fb.hide_comment(comment_id, page_token, hidden=True)reply_comment
asyncReplies 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
MODERATEtask on the Page and thepages_manage_engagementpermission.
Required permissions:pages_manage_engagement
await fb.reply_comment(comment_id, page_token, "Rahmat!")Lead Ads
Lead forms, leads and the ad context a lead came from.
ad_context
asyncReturns 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 emptyad_idit returns{}. - Keys:
ad_id,ad_name,adset_id,adset_name,campaign_id,campaign_name,objective,optimization_goal,targeting.
Required permissions:ads_management
context = await fb.ad_context(ad_id, token)lead
asyncFetches 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.
lead_forms
asyncReturns 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
forms = await fb.lead_forms(page_id, page_token)leads
asyncReturns 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]sinceis sent to Graph API as aGREATER_THANfilter ontime_created.Lead.fieldsholds normalized fields (full_name,email,phone, ...);Lead.field_datais Meta's raw list.- Per Meta, reading leads needs
leads_retrieval,ads_management,pages_show_list,pages_read_engagement,pages_manage_adsand a token from someone who can advertise on the ad account. limitdefaults to 200; pagination is automatic.
Required permissions:leads_retrieval
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
asyncSends 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,
) -> AnyNonevalues inparamsare not sent.- Rate limits (
RateLimitError) and transient server errors (HTTP 429, 500, 502, 503, 504) are retried according toRetryPolicy; aRetry-Afterheader is honoured. Network failures are raised asTransportError.
get
asyncShorthand for request("GET", ...).
await fb.get(url: str, **kw) -> Anypost
asyncShorthand for request("POST", ...).
await fb.post(url: str, **kw) -> Anydelete
asyncShorthand for request("DELETE", ...).
await fb.delete(url: str, **kw) -> Anypaginate
asyncFollows 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_sizesets thelimitof each request.
close
asyncCloses 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() -> NoneFrequently 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.
comments
asyncReturns comments on a post, newest first (
order=reverse_chronological). Use it to fill a moderation queue or sync comments into a CRM.filteris sent, so Graph API's default applies: top-level comments only, replies are not included.limitis the total number of comments; pagination is automatic.Required permissions:
pages_read_engagement