# Instagram API Python Tutorial: Read, Publish, Refresh

Working Python requests code for the Instagram API: read a profile and media, publish an image with a container and media_publish, and refresh the token.

Canonical page: https://www.tryadeli.com/blog/instagram-api-python
Last updated: 2026-10-12

By Mika Reyes, Co-founder, Adeli. Published 2026-10-12.

## 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](https://www.tryadeli.com/blog/instagram-api-guide) 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](https://developers.facebook.com/docs/instagram-platform/content-publishing) and [IG Media reference](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/ig-media).

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](https://www.tryadeli.com/blog/instagram-graph-api-explorer) 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:

```python
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](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/get-started) uses `user_id` and `username`, and lists `account_type`, `followers_count`, `follows_count`, `media_count`, `name`, and `profile_picture_url` as other fields.

```python
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:

```python
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](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/ig-media):

- **`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](https://developers.facebook.com/docs/instagram-platform/content-publishing) sends a JSON body with a bearer token:

```python
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](https://www.tryadeli.com/blog/post-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**:

```python
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:

```python
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](https://www.tryadeli.com/blog/instagram-api-rate-limit) 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](https://developers.facebook.com/docs/instagram-platform/reference/refresh_access_token) 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:

```python
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](https://www.tryadeli.com/blog/instagram-api-access-token) 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`:

```python
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](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/error-codes).

## 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](https://www.tryadeli.com/product/instagram) 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:

```python
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](https://www.tryadeli.com/docs/api/posts/publish-instagram). The same endpoint in [Adeli's posting API](https://www.tryadeli.com/social-media-posting-api) publishes to TikTok, YouTube, Facebook, X, and Bluesky, and **your first 3 connected accounts are free** on [Adeli's pricing](https://www.tryadeli.com/pricing).

## 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.

## Related Adeli pages

- [Instagram API for Developers](https://www.tryadeli.com/product/instagram.md): Publish, schedule, and read analytics for Instagram through one API. Adeli owns the Instagram developer app, token refresh, and Meta review. Free to start.
- [Social Media Posting & Scheduling API](https://www.tryadeli.com/social-media-posting-api.md): Adeli's social media posting and scheduling API publishes to Instagram, TikTok, YouTube, Facebook, X, and Bluesky in one request, now or at a set time. Free to start.
- [All posts on the Adeli blog](https://www.tryadeli.com/blog.md)

## About the author

Mika Reyes is the CEO behind Adeli and a tech and AI content creator. She was previously co-founder and CEO of Parallax, which was acquired by Phantom, and a product lead at LinkedIn. She is a Forbes 30 Under 30 honoree.
