Instagram API Python Tutorial: Read, Publish, Refresh
To use the Instagram API from Python, call Meta's Instagram API with Instagram Login at graph.instagram.com using the requests library and an Instagram User access token. Read the profile from /me and posts from /{ig-id}/media, publish by creating a container, polling its status, and calling media_publish, and refresh the token before day 60. Adeli does the same publish in one POST.
You can use the Instagram API from Python with the requests library and nothing else: send an Instagram User access token to graph.instagram.com, read the profile and media, and publish with a container plus media_publish. Below is Python code for each call, written against Meta's documented endpoints and parameters, then a token refresh and the same publish through Adeli.
This tutorial assumes you already have a Meta app and a token. If you don't, start with our Instagram API guide.
Should I use Instagram Login or Facebook Login?
Use Instagram Login for most new Python projects. Meta runs two versions of the Instagram API. The read and publish calls differ in the host and the token, and Facebook Login adds a Page lookup to find the account.
| Instagram API with Instagram Login | Instagram API with Facebook Login | |
|---|---|---|
| Host | graph.instagram.com |
graph.facebook.com |
| Token | Instagram User access token | Facebook Page access token for publishing |
| Facebook Page needed | No | Yes |
| Read permission | instagram_business_basic |
instagram_basic, pages_read_engagement |
| Publish permission | instagram_business_content_publish |
instagram_content_publish |
Sources: Meta's content publishing guide and IG Media reference.
The examples below use Instagram Login. To switch to Facebook Login, change the base URL to https://graph.facebook.com/v25.0 and use a Page token. Our Graph API Explorer guide shows how to find the IG user ID on that path.
What do you need before you start?
- Python 3 and
requests. Install it withpip install requests. - A professional Instagram account. Business or Creator. Personal accounts can't be read or published to.
- An Instagram User access token. For a test on your own account, click Generate token on the Instagram API setup page in the App Dashboard. Meta says those tokens are valid for 60 days.
- A public JPEG URL for the publish step. Meta downloads the image from it.
Keep the token out of your source. The examples read it from an environment variable:
import os
import time
import requests
GRAPH = "https://graph.instagram.com/v25.0"
TOKEN = os.environ["IG_ACCESS_TOKEN"]
HEADERS = {"Authorization": f"Bearer {TOKEN}"}Step 1: Read the Instagram profile
Call /me with the fields you want. Meta's getting-started guide uses user_id and username, and lists account_type, followers_count, follows_count, media_count, name, and profile_picture_url as other fields.
resp = requests.get(
f"{GRAPH}/me",
headers=HEADERS,
params={"fields": "user_id,username,account_type,followers_count,media_count"},
)
resp.raise_for_status()
body = resp.json()
# Meta's sample response wraps the user in a "data" array
profile = body["data"][0] if "data" in body else body
ig_id = profile["user_id"]
print(profile["username"], ig_id)user_id is the IG ID that every other Instagram endpoint takes. Store it with the token.
Step 2: List the account's media
Call the /<IG_ID>/media edge. It returns media IDs in data and paging cursors in paging. Ask for fields to get more than the ID:
resp = requests.get(
f"{GRAPH}/{ig_id}/media",
headers=HEADERS,
params={"fields": "id,media_type,media_url,permalink,timestamp"},
)
resp.raise_for_status()
page = resp.json()
for media in page["data"]:
print(media["timestamp"], media["media_type"], media["permalink"])Some fields don't always come back, per Meta's IG Media reference:
media_urlcan be missing. Meta omits it when the media contains copyrighted material.thumbnail_urlis video-only. Image posts don't return it.like_countcan be missing. It's omitted when the owner hides like counts.
To fetch the next page, pass the cursor from page["paging"]["cursors"]["after"] as the after parameter.
Step 3: Create a media container
Publishing starts with a container: POST /<IG_ID>/media with a public image_url. Meta's content publishing guide sends a JSON body with a bearer token:
resp = requests.post(
f"{GRAPH}/{ig_id}/media",
headers=HEADERS,
json={
"image_url": "https://www.example.com/images/photo.jpg",
"caption": "Posted from Python",
},
)
resp.raise_for_status()
container_id = resp.json()["id"]The image has to be a JPEG on a publicly reachable server when Meta fetches it. A presigned URL that expires in seconds, or a file behind auth, fails. Our guide to posting to Instagram via API lists the size and aspect-ratio rules.
Step 4: Wait for the container to finish
Poll status_code until it reads FINISHED before you publish. Meta recommends checking once per minute, for no more than 5 minutes:
for _ in range(5):
resp = requests.get(
f"{GRAPH}/{container_id}",
headers=HEADERS,
params={"fields": "status_code"},
)
resp.raise_for_status()
status = resp.json()["status_code"]
if status == "FINISHED":
break
if status in ("ERROR", "EXPIRED"):
raise RuntimeError(f"Container {container_id} is {status}")
time.sleep(60)
else:
raise TimeoutError("Container did not finish in 5 minutes")Images usually finish fast, so the first check often passes. Video takes longer, which is why the loop exists.
Step 5: Publish with media_publish
POST the container ID as creation_id. The response is the ID of the live post:
resp = requests.post(
f"{GRAPH}/{ig_id}/media_publish",
headers=HEADERS,
json={"creation_id": container_id},
)
resp.raise_for_status()
print("Published:", resp.json()["id"])Each account can publish 100 posts in a rolling 24 hours through the API, per Meta's publishing guide. Our rate limit guide covers the call limits on top of that.
How do you refresh the token in Python?
Call refresh_access_token with grant_type=ig_refresh_token before day 60. Meta's refresh reference only accepts a long-lived token that is at least 24 hours old and not expired, and the refreshed token lasts 60 days from the refresh:
def refresh_token(long_lived_token: str) -> dict:
resp = requests.get(
"https://graph.instagram.com/refresh_access_token",
params={"grant_type": "ig_refresh_token", "access_token": long_lived_token},
)
resp.raise_for_status()
return resp.json() # access_token, token_type, expires_in (seconds)Save the new access_token and compute its expiry from expires_in. Run the refresh on a schedule, around day 45, so a failed job has time to retry. If you start from the 1-hour token that the login flow returns, exchange it first with grant_type=ig_exchange_token. Our access token guide has that call and the error 190 fixes.
How do you handle Instagram API errors in Python?
Read the error object instead of stopping at the HTTP status. Graph API errors carry a code, an error_subcode, and a message:
def check(resp: requests.Response) -> dict:
data = resp.json()
if "error" in data:
err = data["error"]
raise RuntimeError(f"{err.get('code')}/{err.get('error_subcode')}: {err.get('message')}")
return dataCall check(resp) in place of resp.raise_for_status() in the steps above. Code 190 means the token is dead. Send the user back through login. Publishing subcodes such as 2207052 (media couldn't be fetched) and 2207027 (not ready to publish) are listed in Meta's error code reference.
How do you publish the same image with Adeli in Python?
One POST to https://app.tryadeli.com/api/v1/posts replaces Steps 3 to 5 and the refresh job. Adeli holds the Meta developer app, carried Meta's review, and refreshes every connected account's tokens. Your code sends one API key and the image as base64, and Adeli stages it where Instagram can fetch it:
import base64
import os
import requests
with open("photo.png", "rb") as f:
image_b64 = base64.b64encode(f.read()).decode()
response = requests.post(
"https://app.tryadeli.com/api/v1/posts",
headers={"Authorization": f"Bearer {os.environ['ADELI_API_KEY']}"},
json={
"platform": "instagram",
"accountId": "00000000-0000-0000-0000-000000000000",
"caption": "Posted from Python",
"image": {"contentType": "image/png", "base64": image_b64},
},
)
print(response.status_code, response.json())What changes compared with the direct calls:
- No public hosting. Images can be JPEG, PNG, or WebP up to 8 MiB each, and Adeli normalizes them to JPEG.
- No polling. Success returns
201with the post'spermalink. - Readable failures. An expired connection returns
422 connection_expired, and an Instagram rejection returns422 provider_rejectedwith Instagram's message. - Carousels in the same call. Swap
imageforimageswith 2 to 10 entries.
Every field is in the Publish to Instagram docs. The same endpoint in Adeli's posting API publishes to TikTok, YouTube, Facebook, X, and Bluesky, and your first 3 connected accounts are free on Adeli's pricing.