Instagram Private API in Python: a practical guide with instagrapi
Updated
Why a private API at all?
Instagram exposes two very different surfaces. The Graph API is the official, documented one — but it only works with Business and Creator accounts, and it intentionally hides most of what users actually see in the app: personal profiles, their full media history, stories, and direct messages.
The private mobile API is what the official Instagram apps speak to the backend. It is undocumented, it changes without notice, and Instagram never hands out tokens for it. It also covers everything the apps can do, on any account type. If you need to read a personal feed, walk a tag, pull comments, or send a DM from Python, the private API is the only path.
instagrapi is a Python wrapper around that private API. It impersonates an Android device, signs requests, manages cookies, and exposes pydantic-typed responses so you can stay focused on your code instead of reverse-engineering protobuf.
If your codebase is async/await, the same author maintains aiograpi — same auth, login, and endpoint surface, async-first. Everything in this guide applies to either library; just pip install aiograpi and await the calls.
Installing instagrapi
instagrapi supports Python 3.10 and newer (3.10, 3.11, 3.12, 3.13, 3.14). Install it like any other library:
pip install instagrapi
Or with Poetry:
poetry add instagrapi
You will also want Pillow available if you plan to upload media — it is pulled in transitively, but if you ship your own slim image you may need to declare it explicitly. By convention, instagrapi projects keep a session.json file next to the script that holds device fingerprint and cookies between runs. Add it to .gitignore immediately; that file authenticates as your account.
Logging in
The simplest possible login takes three lines:
from instagrapi import Client
cl = Client()
cl.login("YOUR_USERNAME", "YOUR_PASSWORD")
print(cl.account_info())
This works once. The reason it stops working — usually on the second or third run from the same machine, sometimes immediately from a new IP — is that Client() generates a fresh device fingerprint every time. To Instagram’s risk system, every login looks like a brand-new Android phone authenticating from your IP. After two or three of those it flips you into a challenge_required flow: SMS code, email code, or “we sent a notification to your other device.”
Two-factor auth makes this worse. If your account has TOTP or SMS 2FA enabled, plain cl.login(user, pw) will raise immediately and you need to pass verification_code= or hook the challenge handler. The pattern that survives in production is to log in once interactively, persist the device + session, and reuse it on every subsequent run. That is the next section.
Reusing the session
instagrapi exposes dump_settings and load_settings to persist the device fingerprint and cookies. Run a real login once, save the settings, and on every subsequent run load them before calling login again:
# First run
cl.login("YOUR_USERNAME", "YOUR_PASSWORD")
cl.dump_settings("session.json")
# Later runs
cl = Client()
cl.load_settings("session.json")
cl.login("YOUR_USERNAME", "YOUR_PASSWORD") # reuses cookies + device
The second login call is cheap when valid cookies are present — instagrapi checks them with a lightweight call and only re-authenticates if they have expired. The device fingerprint stays stable across runs, which is the part Instagram’s risk system actually cares about.
Storing session.json on disk works for a single script. For anything multi-process, multi-host, or running in a container that forgets the filesystem on restart, you want the session in Redis, S3, or your database — keyed by account. See the session persistence guide for storing sessions in Redis and rotating credentials.
Fetching user info and media
Once you are logged in, the read API is straightforward and fully typed:
user = cl.user_info_by_username("instagram")
print(user.full_name, user.follower_count)
medias = cl.user_medias(user.pk, amount=20)
for m in medias:
print(m.taken_at, m.like_count, m.caption_text[:80] if m.caption_text else "")
user_info_by_username returns a UserShort-style pydantic model with the fields you would expect: pk, username, full_name, follower_count, following_count, biography, is_private, profile_pic_url. Because it is pydantic, autocomplete works in any modern editor and m.dict() gives you a serializable representation for free.
user_medias(pk, amount=N) walks Instagram’s pagination internally and stops at N. Pass amount=0 to fetch everything (be careful: a large account is hundreds of requests). Each Media exposes taken_at, like_count, comment_count, caption_text, media_type (1 = photo, 2 = video, 8 = album), and resources for carousels. For comments use media_comments(media_id); for likers, media_likers(media_id). The pagination cursor is handled for you, but you can get it back with the *_v1_chunk variants when you need to checkpoint a long crawl.
Posting a photo
Uploads are a single call. The path can be a string or a pathlib.Path:
from pathlib import Path
media = cl.photo_upload(
Path("./photo.jpg"),
caption="Hello from instagrapi #python",
)
print(media.code) # short URL slug
The returned Media includes code, which is the slug you would paste into https://www.instagram.com/p/<code>/. Instagram is picky about what it accepts: JPEG only (instagrapi will re-encode PNG via Pillow, but the source needs to decode), aspect ratio between 4:5 and 1.91:1, and 1080×1080 or larger is the safe minimum — anything smaller gets compressed aggressively and may be rejected outright.
For videos use video_upload, for carousels album_upload, and for stories photo_upload_to_story / video_upload_to_story. Reels go through clip_upload. All of them accept the same caption= and usertags= kwargs and return a typed Media so you can chain the next call without reparsing JSON.
Common errors and how to read them
Three error strings cover the vast majority of what you will see in production:
challenge_required— Instagram wants the account to verify it (SMS, email, or in-app). Triggered by new device fingerprints, new IPs, or risk signals. See the dedicated guide on handling 2FA and challenges.login_required— Your session cookies were invalidated server-side. Re-login is the only fix; if it happens repeatedly, your fingerprint or IP looks suspicious.please_wait_a_few_minutes(sometimes surfaced asfeedback_required) — Soft rate limit. Instagram is asking you to back off; usually 5–30 minutes is enough, but persistent offenders get longer cool-downs.
instagrapi raises typed exceptions for each (ChallengeRequired, LoginRequired, PleaseWaitFewMinutes, FeedbackRequired) so you can catch them precisely instead of grepping strings. Wrap your hot path in try/except against instagrapi.exceptions, and on rate-limit errors back off exponentially before retrying.
When to reach for the managed API instead
Self-hosting instagrapi is the right call for a single account, a research project, or anything that runs on a developer’s laptop. It gets painful in three specific scenarios:
- Multi-account orchestration. Running 10+ accounts means 10+ session files, 10+ proxies, and a scheduler that knows which account is currently rate-limited. That is its own service.
- Residential proxy rotation. Datacenter IPs get flagged fast on Instagram. A residential pool costs real money and needs sticky sessions per account, plus health-checking when one IP starts returning challenges.
- 24/7 uptime. Instagram changes the private API surface on its own schedule. When a signing routine breaks at 2am, someone has to ship a fix; if that someone is you, you are now on call for Instagram’s release cycle.
If any of those describe your situation, the managed alternative — same endpoints, sessions and proxies handled — is worth a look. See the full comparison page.
Wrapping up
The private API is the only way to read most of Instagram from Python, and instagrapi is the most complete wrapper for it. The two things that separate working setups from broken ones are session reuse and proxy hygiene; everything else is an ordinary Python library. Start with Installing, persist your session early, and read the 2FA guide before you ship. Try it on a throwaway account first.
Related guides
- Handling instagrapi 2FA and challenge_required errors in Python Resolve instagrapi 2FA prompts and challenge_required errors: SMS, email, and TOTP flows with working callback handlers.
- Configuring proxies in instagrapi: HTTP, SOCKS5, and residential setups Configure HTTP and SOCKS5 proxies in instagrapi (Python). Residential vs datacenter, per-account pinning, and rotating without breaking sessions.
- Persisting instagrapi sessions: file, Redis, and Postgres patterns Reuse instagrapi login sessions across runs and processes: dump_settings, load_settings, and storing the session blob in Redis or Postgres.
Frequently asked
What is the Instagram Private API?
The Instagram Private API is the undocumented mobile API that the official Instagram apps speak. Unlike the Graph API, it covers personal accounts, full feeds, stories, and direct messages, but Instagram does not publish a contract or grant tokens for it.
Is using the Instagram Private API legal?
It is a gray area. Instagram's Terms of Service prohibit automated access, and accounts that abuse the API may be challenged or disabled. Personal research, archiving, and small-scale automation are common uses; commercial use should be reviewed against your local laws and Instagram's terms.
Why use instagrapi instead of writing my own client?
instagrapi handles device fingerprinting, the login flow with all its challenge variants, request signing, pagination, and pydantic-typed responses for every endpoint. Reproducing that surface is months of work; instagrapi is MIT-licensed and battle-tested.
Will Instagram block my account when using instagrapi?
Risk goes up with request volume, missing or rotating proxies, fresh accounts, and aggressive actions like mass follows. Reuse sessions, set realistic delays, use residential proxies, and do not automate engagement at scale.
Does instagrapi work with the Graph API?
No. instagrapi targets the private mobile API. If you have a Business or Creator account and only need official endpoints, use Meta's Graph API instead.
Skip the infra?
Managed Instagram API — same endpoints, sessions and proxies handled.
Try HikerAPI → Full comparison