Instagram client: Graph API methods
Instagram is the Instagram Graph API client in the Socialift Python SDK. It supports two connection types: Instagram(app_id, app_secret) uses Instagram Login without a Facebook Page via graph.instagram.com, and Instagram.via_facebook(app_secret=...) uses a Page token from Facebook Login via graph.facebook.com. It reads profiles and media, publishes photos, Reels, carousels and stories, moderates comments, handles Direct conversations and fetches insights.
from socialift import Instagram25 methodsas of socialift 1.3.0Updated:
How do I set up the Instagram client?
Without a Page (Instagram Login): Instagram(app_id=..., app_secret=...). Instagram.from_env() reads INSTAGRAM_APP_ID and INSTAGRAM_APP_SECRET, plus the Graph API version from META_GRAPH_VERSION. Requests go to graph.instagram.com with an Instagram user token; when ig_user_id is omitted, me is used. The keys are in the Instagram settings of your Meta app — see the "Instagram Login setup" guide.
Through a Page: Instagram.via_facebook(app_secret=...). No app_id is needed because OAuth happens in the Facebook client. Use the Page token from Facebook.pages() as the token and Page.instagram_id as the account ID; in this mode ig_user_id is required, otherwise ConfigurationError is raised. Meta app configuration is covered in the "Meta app setup" guide.
In both modes the account must be an Instagram professional account (Business or Creator). When app_secret is set, appsecret_proof is added to every request. You can store keys in the dashboard's Keys section, connect the account under Connections and try the methods in the Playground.
import asyncio
import os
from socialift import Instagram
async def main():
# 1) Instagram Login (no Page), from the environment:
# INSTAGRAM_APP_ID, INSTAGRAM_APP_SECRET, META_GRAPH_VERSION
ig = Instagram.from_env()
# or: Instagram(app_id=os.environ["INSTAGRAM_APP_ID"],
# app_secret=os.environ["INSTAGRAM_APP_SECRET"])
# 2) Through a Page: Page token plus Page.instagram_id
# ig = Instagram.via_facebook(app_secret=os.environ["FACEBOOK_APP_SECRET"])
async with ig:
profile = await ig.profile(os.environ["IG_TOKEN"])
print(profile.username, profile.followers_count)
asyncio.run(main())
Connection and tokens
The client in both modes, the Instagram Login URL, token exchange and refresh.
exchange_codefrom_envlogin_urlrefresh_tokenvia_facebook
exchange_code
asyncExchanges code for a short-lived token at api.instagram.com (POST form data), then for a long-lived one via ig_exchange_token, and returns a TokenBundle. Call it in your Instagram Login callback.
await ig.exchange_code(code: str, *, redirect_uri: str) -> socialift.models.TokenBundle- Per Meta, the short-lived token lasts 1 hour and the long-lived token 60 days.
- Pass exactly the same
redirect_urias in the login URL. - Raises
ApiErrorif no short-lived token is returned.
from_env
classmethodCreates an Instagram Login client from the environment: INSTAGRAM_APP_ID, INSTAGRAM_APP_SECRET and META_GRAPH_VERSION. For the Page-based mode use via_facebook.
Instagram.from_env(**kwargs) -> Instagram- The constructor does not validate empty keys: a missing
app_idsurfaces asConfigurationErrorwhen you calllogin_url.
Builds the Instagram Login dialog URL (https://www.instagram.com/oauth/authorize). It is synchronous and sends no request.
ig.login_url(
*,
redirect_uri: str,
state: Optional[str] = None,
scopes: Optional[list[str]] = None,
) -> str- Default scopes:
instagram_business_basic,instagram_business_manage_messages,instagram_business_manage_comments,instagram_business_content_publish,instagram_business_manage_insights; narrow them withscopes=. - Raises
ConfigurationErrorwithoutapp_id. - The SDK does not store
state: generate a random value and compare it in the callback. - Per Meta, the callback
codeis valid for 1 hour and can be used only once.
url = ig.login_url(redirect_uri="https://siz.uz/cb", state=state)refresh_token
asyncRefreshes a long-lived Instagram user token via ig_refresh_token and returns a new TokenBundle. Run it on a schedule (cron) so accounts do not silently disconnect.
await ig.refresh_token(token: str) -> socialift.models.TokenBundle- Meta rule: the token must be at least 24 hours old and not expired; refreshed tokens are valid for 60 days from the refresh date.
- Instagram Login tokens only — the Page-based mode uses Page tokens.
- Raises
ApiErrorif no token is returned.
bundle = await ig.refresh_token(token)via_facebook
classmethodCreates a client for the Facebook Login flow: requests go to graph.facebook.com and a Page token is expected. No app_id is needed — OAuth happens in the Facebook client and the secret is used for appsecret_proof.
Instagram.via_facebook(*, app_secret: str, **kwargs) -> Instagram- Profile, media, insights and publishing methods require
ig_user_id(memeans the Page here); take it fromPage.instagram_id. conversationsandsend_messagealso requirepage_id.login_url,exchange_codeandrefresh_tokenare for Instagram Login and are not used in this mode.
Profile and media
The account profile, the media list and a single media object.
media
asyncReturns the account's media as raw dicts. limit is the total number of items; the SDK follows pagination.
await ig.media(
token: str,
*,
ig_user_id: Optional[str] = None,
limit: int = 25,
) -> list[dict]- In the Page-based mode
ig_user_idis required. - For a single item use
media_details.
Required permissions:instagram_basic
posts = await ig.media(page_token, ig_user_id=ig_user_id, limit=25)media_details
asyncReturns a single media object by media_id. publish only returns IDs, so fetch permalink and other fields with this method after publishing.
await ig.media_details(media_id: str, token: str) -> dictRequired permissions:instagram_basic
post = await ig.media_details(media_id, page_token)profile
asyncReturns the professional account's profile as a Profile: username, name, picture, bio, website, followers, follows and media count. Use it to display or sync the account after connecting.
await ig.profile(
token: str,
*,
ig_user_id: Optional[str] = None,
) -> socialift.models.Profile- In the Page-based mode
ig_user_idis required; the SDK does not requestaccount_typethere, so it isNone. - The full response is in
Profile.raw.
Required permissions:instagram_basic
profile = await ig.profile(page_token, ig_user_id=ig_user_id)Publishing
Publishing photos, Reels and carousels from public HTTPS URLs.
publishpublish_carouselpublish_container
publish
asyncPublishes one photo or video: creates a container, waits for it to be ready if it is a video, then calls media_publish. Returns {container_id, media_id}.
await ig.publish(
token: str,
*,
ig_user_id: Optional[str] = None,
caption: Optional[str] = None,
image_url: Optional[str] = None,
video_url: Optional[str] = None,
is_reel: bool = False,
share_to_feed: Optional[bool] = None,
wait_timeout: float = 300.0,
) -> dictimage_url/video_urlmust be public HTTPS URLs — Meta downloads the file itself; images must be JPEG.- Video always goes out as
REELS— Meta stopped acceptingmedia_type=VIDEOon November 9, 2023.is_reelonly picks theshare_to_feeddefault. - Raises
ContainerNotReadyErrorif a video container is not ready withinwait_timeout(300 seconds by default) or reportsERROR— pass itscontainer_idtopublish_container(). - Meta limit: up to 100 API-published posts per account within a moving 24-hour period (some Meta pages state 50) —
content_publishing_limitreturns the exact remaining quota.
Required permissions:instagram_content_publish
post = await ig.publish(page_token, ig_user_id=ig_user_id, caption="Yangi mahsulot",
image_url="https://cdn.siz.uz/photo.jpg")publish_carousel
asyncPublishes 2–10 images as one carousel: a child container per image (in parallel), then a CAROUSEL container, a readiness wait and publish. Returns {container_id, media_id, children}.
await ig.publish_carousel(
token: str,
*,
ig_user_id: Optional[str] = None,
image_urls: Sequence[str],
caption: Optional[str] = None,
wait_timeout: float = 300.0,
) -> dict- With more than 10 URLs the first 10 are used; fewer than 2 raises
ConfigurationError. - Image URLs only — public HTTPS and JPEG.
- Raises
ContainerNotReadyErrorif the parent container is not ready withinwait_timeout(300 seconds by default) — continue withpublish_container().
publish_container
asyncPublishes a container created earlier: use it to continue after publish(), publish_carousel() or publish_story() raised ContainerNotReadyError, without re-uploading the file.
await ig.publish_container(
token: str,
container_id: str,
*,
ig_user_id: Optional[str] = None,
wait_timeout: float = 300.0,
) -> dict- Take
container_idfrom the exception (ContainerNotReadyError.container_id); the method waits for the container to be ready and then callsmedia_publish. - A container lives 24 hours: if it is not published by then it becomes
EXPIREDand the media must be uploaded again. - Raise
wait_timeoutfor long videos — Meta recommends polling the container status once a minute for no more than 5 minutes.
Stories
Publishing a story and listing the currently active ones. A story lives 24 hours and has no caption.
publish_story
asyncPublishes a photo or video as a story. The flow is the same as publish() (container → wait → media_publish) with one difference: the container carries media_type=STORIES. Returns {container_id, media_id}.
await ig.publish_story(
token: str,
*,
ig_user_id: Optional[str] = None,
image_url: Optional[str] = None,
video_url: Optional[str] = None,
wait_timeout: float = 300.0,
) -> dict- There is no
caption, by design: a story is not a feed post and caption text is not shown on it. Any text has to be burned into the image or video itself. - Pass exactly one of
image_urlandvideo_url— a public HTTPS URL, as inpublish()(Meta downloads the file). Passing both raisesConfigurationError. - Meta's media rules (the SDK cannot check them): images — JPEG up to 8 MB, 9:16 recommended; video — MOV or MP4, 3–60 seconds, up to 100 MB, H264 or HEVC, AAC audio at 48 kHz.
- If a video container is not ready within
wait_timeout(300 seconds by default),ContainerNotReadyErroris raised; continue withpublish_container()and itscontainer_id. - Stories go out through
media_publishtoo, so they count against the daily publishing limit. Meta's own docs state that number differently on different pages — read the remaining quota fromGET /{ig-user-id}/content_publishing_limit. - A published story disappears after 24 hours (the API exposes no archive and no highlights). Within those 24 hours
media_details()andmedia_insights()work;media_product_type == "STORY"identifies it as a story. - Permissions: Instagram Login needs
instagram_business_basicandinstagram_business_content_publish; Facebook Login needsinstagram_basic,instagram_content_publishandpages_read_engagement.
Required permissions:instagram_content_publishpages_read_engagement
story = await ig.publish_story(page_token, ig_user_id=ig_user_id,
image_url="https://cdn.siz.uz/story.jpg")stories
asyncReturns the account's currently active stories. Stories never appear in the media edge — Meta exposes a separate stories edge for them, which is why this is its own method.
await ig.stories(
token: str,
*,
ig_user_id: Optional[str] = None,
limit: int = 25,
) -> list[dict]- Facebook Login only: Meta marks this edge "Instagram API with Facebook Login only". On an Instagram Login client the method raises
ConfigurationErrorwithout sending a request — there is no official way to list active stories in that flow. - Only the last 24 hours are visible: the API exposes neither the archive nor highlights.
- Every entry is an IG Media object;
media_typeisIMAGE/VIDEOfor stories too, somedia_product_type == "STORY"is what identifies them — the SDK always requests that field. - Meta limits: Live Video stories are not returned; a new story created by resharing someone else's is not included; and if a story has several captions, only one comes back.
- Permissions (Facebook Login):
instagram_basicandpages_read_engagement.
Required permissions:instagram_basicpages_read_engagement
stories = await ig.stories(page_token, ig_user_id=ig_user_id, limit=25)Comments
Reading, replying to, hiding and deleting comments.
commentsdelete_commenthide_commentreply_comment
delete_comment
asyncDeletes a comment with DELETE /{comment_id}. This cannot be undone — hide_comment is often enough.
await ig.delete_comment(comment_id: str, token: str) -> dictRequired permissions:instagram_manage_comments
await ig.delete_comment(comment_id, page_token)hide_comment
asyncHides a comment (hide=true) or unhides it with hidden=False. A reversible alternative to deleting.
await ig.hide_comment(comment_id: str, token: str, hidden: bool = True) -> dictRequired permissions:instagram_manage_comments
await ig.hide_comment(comment_id, page_token, hidden=True)reply_comment
asyncReplies to a comment as the account (POST /{comment_id}/replies) and returns the new comment id.
await ig.reply_comment(comment_id: str, token: str, message: str) -> dictRequired permissions:instagram_manage_comments
await ig.reply_comment(comment_id, page_token, "Rahmat!")Direct messages
Conversations, message history, contact profiles and sending messages.
contactconversationsmessagessend_message
contact
asyncReturns a participant's profile (User Profile API) by IGSID. It never raises: on error it returns {id, available: False, reason}, on success it adds available: True.
await ig.contact(igsid: str, token: str) -> dict- Meta rule: the profile is available only after user consent — the user messaged the business, tapped an icebreaker or a persistent menu item. Someone who only commented cannot be looked up.
- Per Meta, the
profile_picURL expires in a few days — do not cache it for long.
Required permissions:instagram_manage_messages
contact = await ig.contact(igsid, page_token)conversations
asyncReturns Direct conversations as {data, paging}: each has contact (the customer), contacts, last_message and updated_time. With enrich=True every participant's profile is fetched in parallel from the User Profile API.
await ig.conversations(
token: str,
*,
ig_user_id: Optional[str] = None,
page_id: Optional[str] = None,
limit: int = 25,
after: Optional[str] = None,
enrich: bool = True,
) -> dict- Returns one page: pass the cursor from
pagingasafter=for the next one. - In the Page-based mode
page_idis required andplatform=instagramis added; with Instagram Login it calls/me/conversations. ig_user_idandpage_idare used to tell the business apart from the customer — pass them, orcontactmay be the business itself.- Meta limits: only the 20 most recent messages in a conversation can be read in detail; conversations in the Requests folder inactive for 30 days are not returned.
Required permissions:instagram_manage_messages
chats = await ig.conversations(page_token, page_id=page_id, ig_user_id=ig_user_id)
for chat in chats["data"]:
print(chat["contact"]["name"])messages
asyncReads one conversation's messages from the messages edge and returns the Graph API response (data, paging). Pass the after cursor for the next page.
await ig.messages(
conversation_id: str,
token: str,
*,
limit: int = 50,
after: Optional[str] = None,
) -> dict- Meta limit: only the 20 most recent messages can be read in detail; older ones return an error saying the message has been deleted.
Required permissions:instagram_manage_messages
history = await ig.messages(conversation_id, page_token, limit=50)send_message
asyncSends a text message in Direct (POST /{id}/messages, recipient.id is the IGSID). Use it to reply to a customer who arrived via a webhook or conversations.
await ig.send_message(
recipient_id: str,
token: str,
text: str,
*,
ig_user_id: Optional[str] = None,
page_id: Optional[str] = None,
) -> dict- 24-hour window: Meta allows replies within 24 hours of the customer's message and rejects them afterwards (
PermissionErrorin the SDK). - Meta requirement: text must be UTF-8 and at most 1000 bytes.
- In the Page-based mode
page_idis required; with Instagram Loginig_user_idormeis used. - Text only: no images, templates or message tags in this method.
Required permissions:instagram_manage_messages
await ig.send_message(recipient_id, page_token, "Salom!", page_id=page_id)Webhook subscription
Subscribing an account to your app's webhooks in the Instagram Login flow.
subscribe_account
asyncSubscribes the account to your app's webhooks in the Instagram Login flow (subscribed_apps) and returns a bool. Even with webhooks configured in the App Dashboard, comment and Direct events do not arrive until the account is subscribed.
await ig.subscribe_account(
token: str,
*,
ig_user_id: Optional[str] = None,
fields: Optional[Sequence[str]] = None,
) -> bool- Call it right after
exchange_code(), otherwise the first events are lost. - Without
fieldsthe SDK sends the default list:comments,messages,messaging_postbacks,message_reactions. - Not used for Page-connected accounts: there the subscription lives on the Page (
Facebook.subscribe_page) and IG events arrive through it. In that mode this method raisesConfigurationError.
Insights
Account metrics, follower demographics and media insights.
demographicsinsightsmedia_insights
demographics
asyncReturns follower demographics broken down by age,gender, city and country (follower_demographics, timeframe=this_month). Each breakdown is a separate request, so one failure does not block the others.
await ig.demographics(token: str, *, ig_user_id: Optional[str] = None) -> dict- Result:
{breakdown: {available, values}};valuesis a flat dict keyed like25-34 / F, and failures carryavailable: Falseandreason. - Meta rule: demographics are not returned for accounts with fewer than 100 followers.
Required permissions:instagram_manage_insights
demo = await ig.demographics(page_token, ig_user_id=ig_user_id)insights
asyncReturns the account's total metrics for a period as {since, until, metrics}. The SDK requests reach, profile_views, website_clicks, accounts_engaged and total_interactions with period=day and metric_type=total_value.
await ig.insights(
token: str,
*,
ig_user_id: Optional[str] = None,
since: Optional[datetime.date] = None,
until: Optional[datetime.date] = None,
) -> dict[str, Any]- The SDK clamps the range: a future
untilbecomes today, a missingsincemeans the last 30 days, and longer ranges are cut to 30 days. - Meta removed
profile_viewsandwebsite_clicksfrom account insights for all versions on January 8, 2025. If Graph API rejects the request,ApiErroris raised. - Per Meta, data can be delayed by up to 48 hours.
Required permissions:instagram_manage_insights
stats = await ig.insights(page_token, ig_user_id=ig_user_id)media_insights
asyncReturns one media item's metrics as {metric: value}. The set depends on the type: reach, likes, comments, saved, shares, total_interactions, plus profile_visits for images and views for videos and Reels; Stories get reach, replies, navigation, total_interactions.
await ig.media_insights(media_id: str, token: str) -> dict[str, Any]- Never raises on API errors — returns
{}instead. - Meta rules: Story metrics are only available for 24 hours; there are no insights for individual items inside an album (carousel).
Required permissions:instagram_manage_insights
stats = await ig.media_insights(media_id, page_token)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 ig.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 ig.get(url: str, **kw) -> Anypost
asyncShorthand for request("POST", ...).
await ig.post(url: str, **kw) -> Anydelete
asyncShorthand for request("DELETE", ...).
await ig.delete(url: str, **kw) -> Anypaginate
asyncFollows Meta's cursor pagination (paging.next) and returns every data item as a single list.
await ig.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 ig.close() -> NoneFrequently asked questions
Do I need a Facebook Page to use the Instagram API?
No. With Instagram Login (Instagram(app_id, app_secret)) a professional account connects without a Page. The Facebook Login path (Instagram.via_facebook) requires an account linked to a Page and a Page token. Either way, the account must be a Business or Creator account.
How long does an Instagram access token last and how do I refresh it?
Per Meta, with Instagram Login a short-lived token lasts 1 hour and a long-lived token 60 days; exchange_code returns the long-lived one directly. refresh_token refreshes a token that is at least 24 hours old and not expired, and the new token is valid for another 60 days. Run it on a regular schedule.
Why can't I send an Instagram Direct message to a customer?
Meta's rule: you can reply within 24 hours of the user's message; after that the request is rejected. The text must also be UTF-8 and at most 1000 bytes. When connected through a Page, pass page_id to send_message as well.
How do I publish a video or Reel to Instagram via the API?
Call publish(token, video_url=...). The video must be at a public HTTPS URL — Meta downloads the file itself — and the SDK waits for the container up to wait_timeout (300 seconds by default). Meta stopped accepting media_type=VIDEO on November 9, 2023, so the SDK always sends video as REELS. is_reel now only picks the share_to_feed default: False puts the Reel in the feed too, True matches Meta's default. Set share_to_feed yourself for exact control.
Can I publish an Instagram story through the API?
Yes. publish_story(token, ig_user_id=..., image_url=...) creates a container with media_type=STORIES and publishes it, and it works in both connection flows. Stories have no caption — any text must be burned into the image or video. Meta's rules: JPEG up to 8 MB for images, 3–60 seconds for video. The story disappears after 24 hours, and the API exposes no archive or highlights. Listing active stories with stories(), however, is Facebook Login only: Meta marks that edge "Instagram API with Facebook Login only".
How many posts per day can I publish through the Instagram API?
Per Meta, an account can publish up to 100 API posts within a moving 24-hour period, although other Meta pages state 50 — so read the remaining quota from GET /{ig-user-id}/content_publishing_limit. Stories count against the same limit. A carousel holds up to 10 items; the SDK's publish_carousel accepts 2 to 10 images.
comments
asyncReturns a media item's comments as a list of raw dicts.
limitis the total number of comments; pagination is automatic.instagram_basic,instagram_manage_commentsandpages_read_engagement; with Instagram Login,instagram_business_basicandinstagram_business_manage_comments.Required permissions:
instagram_manage_comments