Instagram API Python Tutorial: Read, Publish, Refresh

By Mika ReyesPublished 4 min read

The short answer

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 with pip 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_url can be missing. Meta omits it when the media contains copyrighted material.
  • thumbnail_url is video-only. Image posts don't return it.
  • like_count can 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 data

Call 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 201 with the post's permalink.
  • Readable failures. An expired connection returns 422 connection_expired, and an Instagram rejection returns 422 provider_rejected with Instagram's message.
  • Carousels in the same call. Swap image for images with 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.

faq

Frequently asked questions

Is there an official Instagram API library for Python?

Meta documents the Instagram API as plain HTTP requests with curl examples, so most Python integrations call it directly with the requests library. Each endpoint takes a URL, query parameters or a JSON body, and an access token, which fits requests.get and requests.post.

Should I use Instagram Login or Facebook Login in Python?

Use Instagram Login unless your users manage Instagram through a Facebook Page. Instagram Login needs no Page and calls graph.instagram.com with an Instagram User token. Facebook Login calls graph.facebook.com and publishes with a Page access token. The publishing code is the same apart from the host and the token.

Can I post to a personal Instagram account with Python?

No. The Instagram API only works with professional accounts, meaning Business or Creator accounts. A personal account has to switch to a professional account in the Instagram app before any code can publish to it.

Can I upload a local image file to Instagram with Python?

Not directly. Meta's container endpoint takes an image_url that it downloads from a public server, so a local file has to be uploaded to public hosting first. Adeli's publish endpoint accepts the image as base64 and stages it where Instagram can fetch it.

Power your next project
with one social media API

Give your users publishing, scheduling, and analytics through one integration your team can actually maintain.

Try with agents

Start with free credits. No credit card required.