File: README.md
instagrapi
โ ๏ธ Telegram support group moved to aiograpi_support โ the previous
@instagrapigroup has been restricted by Meta and is no longer maintained.
Fast and effective unofficial Instagram API wrapper for Python.
instagrapi combines public web and private mobile API flows, supports session persistence and challenge handling, and covers the main automation primitives for users, media, stories, direct messages, notes, locations, comments, insights, and uploads.
Private API automation is fragile in production because account trust, proxies, device state, challenges, and rate limits can change independently of the library.
For account-owned business workflows, prefer official Instagram APIs where they cover your use case.
For production private API infrastructure, a hosted provider such as HikerAPI may be a better fit than maintaining accounts, proxies, and challenge handling yourself.
The instagrapi project is best suited for testing, research, and controlled internal automation.
โจ aiograpi - Asynchronous Python library for Instagram Private API โจ
Support Python 3.10+
Python 3.9 support was dropped in 2.5.0. Upstream security patches for Pillow 12.x and pytest 9.x are not backported to Python 3.9, leaving conditional pins permanently exposed to known CVEs. Users who need Python 3.9 should pin to instagrapi==2.4.5.
Installation
pip install instagrapiOptional public web TLS impersonation support is available as an extra:
pip install "instagrapi[curl]"Use it only for public web endpoints that are sensitive to browser TLS fingerprints:
cl = Client(public_transport="curl", public_transport_impersonate="chrome136")See the public transport guide for live comparison results and caveats.
TLS certificate verification is enabled by default. For a trusted debugging MITM proxy, prefer Client(tls_verify="/path/to/proxy-ca.pem"); use Client(tls_verify=False) only for temporary local debugging because it allows session interception.
If your project uses uv, you can add the package with:
uv add instagrapiOr install it into the active virtual environment:
uv pip install instagrapiVideo uploads can use a built-in MP4 metadata parser when you provide thumbnail=.... Automatic thumbnail generation, StoryBuilder, and video/audio composition still need the optional video dependencies, MoviePy 2.2.1, and executable ffmpeg:
pip install "instagrapi[video]"
pip install --no-deps "moviepy==2.2.1"MoviePy 2.2.1 currently declares Pillow<12, but instagrapi keeps Pillow>=12.2.0 for security fixes; the --no-deps install keeps the safe Pillow version. If your project imports MoviePy directly, migrate any MoviePy 1.x code from moviepy.editor, set_*, resize, and subclip APIs to the MoviePy 2.x API before upgrading.
Android users should see Pydroid and ffmpeg and Termux.
Quick Start
from instagrapi import Client
cl = Client()
cl.login(ACCOUNT_USERNAME, ACCOUNT_PASSWORD)
user_id = cl.user_id_from_username(ACCOUNT_USERNAME)
medias = cl.user_medias(user_id, 20)Runnable Examples
Practical scripts live in examples/README.md. They cover session login, public lookups, media
downloads, feed uploads, Reels and Trial Reels, story uploads, Direct messages, proxies, challenge handling, and optional
curl-backed public transport.
Session Persistence
from instagrapi import Client
cl = Client()
cl.login(USERNAME, PASSWORD)
cl.dump_settings("session.json")
# reload later; the saved session is validated before reuse
cl = Client()
cl.load_settings("session.json")
cl.login(USERNAME, PASSWORD)
cl.dump_settings("session.json")login() reuses a valid saved session. If Instagram rejects that session withlogin_required, instagrapi clears the stale authorization and logs in again
with the supplied credentials. Dump the settings after login so a refreshed
session is persisted.
If you want more explicit control over the loaded session object:
from instagrapi import Client
cl = Client()
cl.set_settings(cl.load_settings("session.json"))
cl.login(USERNAME, PASSWORD)
cl.dump_settings("session.json")Login using a sessionid
from instagrapi import Client
cl = Client()
cl.login_by_sessionid("<your_sessionid>")login_by_sessionid() is best treated as a lightweight compatibility path. For long-lived automation, prefer the normal login() -> dump_settings() -> load_settings()/set_settings() session flow.
If a browser/web sessionid returns login_required or logs the browser out, Instagram rejected that session for the private mobile API. Use a stable password login once, save settings with dump_settings(), and reuse those settings instead of repeatedly importing browser cookies.
Typical Tasks
List and download another user's posts
from instagrapi import Client
cl = Client()
cl.login(USERNAME, PASSWORD)
target_id = cl.user_id_from_username("target_user")
posts = cl.user_medias(target_id, amount=10)
for media in posts:
# download photos to the current folder
cl.photo_download(media.pk)See examples/session_login.py for a standalone script demonstrating these login methods.
Search locations by name or exact pk
from instagrapi import Client
cl = Client()
cl.login(USERNAME, PASSWORD)
places = cl.location_search_name("Times Square")
place = places[0]
same_place = cl.location_search_pk(place.pk)
print(same_place.name, same_place.pk)Send and read Direct messages
from instagrapi import Client
cl = Client()
cl.login(USERNAME, PASSWORD)
target_id = cl.user_id_from_username("target_user")
sent = cl.direct_send("Hello from instagrapi", user_ids=[target_id])
print("sent", sent.id)
threads = cl.direct_threads(amount=5)
for thread in threads:
last_message = thread.messages[0] if thread.messages else None
print(thread.id, thread.thread_title, last_message.text if last_message else "")Work with Direct messages over Realtime MQTT
Realtime MQTT support is experimental. It opens Instagram's private MQTToT
connection after login, emits live callbacks, and uses the same Client.proxy
settings as HTTP requests. The realtime client can receive Direct message sync
events and publish lightweight Direct actions such as text, reactions, typing,
and seen state. Use the regular direct_* methods for media sends and full
thread management.
import json
from instagrapi import Client
cl = Client()
cl.login(USERNAME, PASSWORD)
def handle_direct_message(payload):
print(json.dumps(payload, indent=2, ensure_ascii=False))
cl.realtime_on("message", handle_direct_message)
rt = cl.realtime_connect()
rt.direct_subscribe()
try:
rt.ping()
rt.direct_send_text(THREAD_ID, "Hello from MQTT")
while True:
rt.read_once()
finally:
cl.realtime_disconnect()See the full Realtime MQTT guide for lower-level
subscriptions and event details.
Receive Direct push notifications over FBNS
FBNS uses Instagram's separate push MQTT connection and registers an Android
push token for the logged-in session. It is useful when you need push payloads
such as Direct notification callbacks.
import json
from instagrapi import Client
cl = Client()
cl.login(USERNAME, PASSWORD)
def handle_push(payload):
print(json.dumps(payload, indent=2, ensure_ascii=False))
cl.fbns_on("push", handle_push)
fbns = cl.fbns_connect()
try:
fbns.ping()
while True:
cl.fbns_read_once()
finally:
cl.fbns_disconnect()Features
- Uses Web API and Mobile API flows where available
- Supports login by password, 2FA, 8-digit backup codes,
sessionid, and Bloks 2FA fallback/helpers for newer verification flows - Includes email/SMS-based challenge resolver hooks
- Uploads and downloads photos, videos, albums, IGTV, reels, and stories
- Works with users, media, comments, locations, hashtags, collections, notes, direct messages, and insights
- Exposes account notification setting helpers with typed notification categories
- Supports story building with mentions, hashtags, link stickers, and media stickers
- Includes helpers for current location search and Direct message workflows
- Supports mobile follower sorting with
date_followed_latestanddate_followed_earliest - App-side discovery surfaces:
chaining,fetch_suggestion_details,discover_recommended_accounts_for_category_v1,user_stream_*,user_web_profile_info_v1 - v2 search SERPs:
media_search,fbsearch_accounts_v2,fbsearch_reels_v2,fbsearch_topsearch_v2,fbsearch_typehead - Alternative media-info path (
media_info_v2) for ad-tagged / sponsored media that the canonical endpoint refuses - Experimental Realtime MQTT helpers for live events, Direct message sync, lightweight Direct actions, and FBNS push callbacks
Anonymous/public web paths are best treated as opportunistic rather than guaranteed. Instagram can change or restrict them independently of the library, so production-grade workflows should prefer authenticated sessions.
Documentation And Support
API reference and full usage guide live at subzeroid.github.io/instagrapi:
- Documentation index
- Getting Started
- Usage Guide
- Interactions reference
- Best Practices for sessions, proxies, and anti-abuse handling
- Handle Exceptions for centralizing
429, challenge, and relogin logic - GitHub Discussions
- Support chat in Telegram: aiograpi_support โ the previous
@instagrapigroup was restricted by Meta and is no longer maintained
For other languages, consider instagrapi-rest. For async Python, see aiograpi.
Tutorials
Hands-on guides for real instagrapi work โ login flows, sessions, proxies, scraping, posting, error handling โ live at instagrapi.com/guides:
- Instagram Private API in Python โ pillar walkthrough: login, sessions, fetching, posting
- 2FA and
challenge_required - Session persistence: file, Redis, and Postgres patterns
- Configuring proxies (HTTP, SOCKS5, residential)
- Instagram scraper in Python: a working setup
- Upload a photo from Python
- Download Instagram stories
- Common errors reference โ
bad_password,challenge_required,login_required,please_wait_a_few_minutes,feedback_required,proxy_address_is_blocked BadPassword: correct password rejected by Instagram โ proxy/IP/device/session trust troubleshooting- Framework integrations โ Django, FastAPI, Celery, Docker, AWS Lambda
Comparing instagrapi to other tools:
instagrapi vs Instaloader โ download-only vs authenticated automation
instagrapi vs aiograpi โ sync or async
Instagram API libraries by language โ what's actually maintained in 2026
Additional example
from instagrapi import Client
from instagrapi.types import StoryMention, StoryMedia, StoryLink, StoryHashtag
cl = Client()
cl.login(USERNAME, PASSWORD, verification_code="<2FA CODE HERE>")
media_pk = cl.media_pk_from_url("https://www.instagram.com/p/CGgDsi7JQdS/")
media_path = cl.video_download(media_pk)
subzeroid = cl.user_info_by_username("subzeroid")
hashtag = cl.hashtag_info("dhbastards")
cl.video_upload_to_story(
media_path,
"Credits @subzeroid",
mentions=[StoryMention(user=subzeroid, x=0.49892962, y=0.703125, width=0.8333333333333334, height=0.125)],
links=[StoryLink(webUri="https://github.com/subzeroid/instagrapi")],
hashtags=[StoryHashtag(hashtag=hashtag, x=0.23, y=0.32, width=0.5, height=0.22)],
medias=[StoryMedia(media_pk=media_pk, x=0.5, y=0.5, width=0.6, height=0.8)],
)Related Projects
If you need async Python, use aiograpi.
For other languages, see instagrapi-rest.
For hosted production Instagram API infrastructure, see HikerAPI.
Related services:
- Cloqly for premium rotating proxies and stable automation traffic
- DataLikers for Instagram MCP, Cache API, and datasets
- LamaTok for TikTok API access, automation, and data workflows
- InstaSurfBot for downloading Instagram media in Telegram
- OSINTagramBot for Instagram OSINT in Telegram
[HikerAPI Affiliate Program](https://hikerapi.com/help/affiliate)
Refer users to HikerAPI and earn a percentage of their API spending:
| Plan | Commission |
|---|---|
| Start trial plan ($0.02/req) | 50% |
| Standard ($0.001/req) | 25% |
| Business ($0.00069/req) | 15% |
| Ultra ($0.0006/req) | 10% |
Extras: 2-level referral system, no caps, lifetime attribution
Payouts: USDT / USDC (TRC-20 or ERC-20), minimum 20 USDT, request anytime from the dashboard
Contributing
For local setup, tests, linting, and pull request expectations, see CONTRIBUTING.md and the development guide.
Maintainer release commands:
git tag -a X.Y.Z -m "Release X.Y.Z"
git push origin X.Y.Z
git push codeberg X.Y.ZThe tag-based publish.yml workflow publishes to PyPI via trusted publishing and creates the GitHub release.
License
instagrapi is distributed under the MIT License.
File: docs/usage-guide/account.md
Account
Viewing and managing your profile
| Method | Return | Description |
|---|---|---|
| account_info() | Account | Get private info for the current account (email, phone number, birthday, biography, etc.) |
| account_edit(**data) | Account | Update profile fields such as username, full_name, biography, external_url, phone_number, and email |
| account_set_biography(biography: str) | bool | Update biography text, including entity-aware markup handling |
| account_change_picture(path: Path) | UserShort | Change profile picture |
| account_set_private() | bool | Switch account to private mode |
| account_set_public() | bool | Switch account to public mode |
| account_convert_to_business(category_id: str | int = "2347428775505624", should_show_category: bool = True, should_show_public_contacts: bool = False) | Account | Convert the current account to a business professional account |
| account_convert_to_creator(category_id: str | int = "2347428775505624", should_show_category: bool = True, should_show_public_contacts: bool = False) | Account | Convert the current account to a creator professional account |
| account_convert_to_professional(to_account_type: Literal[2, 3] = 3, category_id: str | int = "2347428775505624", should_show_category: bool = True, should_show_public_contacts: bool = False) | Account | Generic professional account conversion helper; 2 is business and 3 is creator |
| account_security_info() | dict | Return account security settings, backup codes, trusted devices, and 2FA state |
| set_external_url(external_url: str) | dict | Replace bio links with a single external URL |
| remove_bio_links(link_ids: List[int]) | dict | Remove one or more bio links by link ID |
| send_password_reset(identifier: str, recaptcha_challenge_field: str = "") | dict | Send an Instagram password reset link or code to the account email or phone |
| reset_password(username: str) | dict | Backward-compatible alias for send_password_reset() |
| change_password(old_password: str, new_password: str) | bool | Change account password |
| send_confirm_email(email: str) | dict | Send a confirmation code to a new email address |
| confirm_email(email: str, code: str) | dict | Confirm a new email address with the received code |
| send_confirm_phone_number(phone_number: str) | dict | Send a confirmation code to a new phone number |
| confirm_phone_number(phone_number: str, code: str, has_sms_consent: bool = False) | dict | Confirm a new phone number with the received SMS code |
Example:
/* Detailed source-code truncated for AI context efficiency. */Notes:
account_edit(**data)only applies supported fields and preserves missing required profile fields fromaccount_info().- Use
account_set_biography()when you want Instagram to re-process biography entities/markup explicitly. - Professional conversion uses Instagram's mobile conversion flow and may still be blocked by account-specific eligibility, category, contact, or Account Center requirements.
account_convert_to_business()sendsto_account_type=2;account_convert_to_creator()sendsto_account_type=3. The generic helper exposes this asProfessionalAccountType = Literal[2, 3].send_password_reset()starts Instagram's recovery flow by requesting a reset link or code. It does not set a new password by itself; continue with the link/code flow that Instagram sends to the account email or phone.account_security_info()andinsights_*style methods require an authenticated session and may depend on the account type or enabled security features.
Low level methods:
| Method | Return | Description |
|---|---|---|
| news_inbox_v1(mark_as_seen: bool = False) | dict | Get raw "Active recently" / inbox activity payload |
Example:
>>> cl.news_inbox_v1()
{'story_mentions': {'mentions_count_string': '0 stories mention you.',
'reels': [],
'product_stories_count': '0 stories mention your product.',
'product_stories_reels': []},
'counts': {'likes': 0,
'activity_feed_dot_badge': 0,
'relationships': 0,
'new_posts': 0,
'comments': 0,
'comment_likes': 0,
'shopping_notification': 0,
'fundraiser': 0,
'usertags': 0,
'campaign_notification': 0,
'photos_of_you': 0,
'story_mentions': 0,
'requests': 0},
'last_checked': 1625468461.1633658,
'friend_request_stories': [],
'new_stories': [{'story_type': 159,
'type': 13,
'args': {'rich_text': 'An unrecognized XiaoMi MI 5s just logged in near Moscow, Russia, RU',
'destination': 'login_activity',
'icon_url': 'https://i.instagram.com/static/images/activity/info-1.5.png/3385260677b8.png',
'should_icon_apply_filter': True,
'icon_should_apply_filter': True,
'extra': {'lat': 55.7522, 'long': 37.6156},
'actions': ['hide'],
'timestamp': 1625475888.805998,
'tuuid': '0ceff44c-dd70-11eb-8080-808080808080',
'clicked': False},
'counts': {},
'pk': 'xjQlWRMfNO+f739i2qZ1zf8HJTo='}],
'old_stories': [{'type': 3,
'story_type': 101,
'args': {'links': [{'start': 24,
'end': 33,
...
}File: docs/usage-guide/best-practices.md
Best Practices
This is a best practices guide around using the Instagram API so that you don't get rate limited or banned.
Use a Stable Proxy Identity
If you're getting errors like this:
- "The username you entered doesn't appear to belong to an account. Please check your username and try again."
BadPasswordeven though the same password works in another context
Or you notice that Instagram is sending suspicious login emails, review the network identity for that account. Instagram may be rate limiting the IP, distrusting the login location, or challenging the account because the device/session and IP history do not look consistent.
For production automation, the safest baseline is one account per stable proxy/IP. Reusing the same country, city, ASN/mobile carrier, device settings, and saved session is less suspicious than using a different IP address every time you make a request.
Avoid treating any proxy type as a universal fix. Residential, mobile, ISP, and datacenter proxies can all fail if they are abused, shared too widely, or rotated aggressively. The exact provider matters less than consistency, reputation, and whether your request pattern fits the account history.
Here is the shape of using a proxy with instagrapi:
from instagrapi import Client
cl = Client()
before_ip = cl._send_public_request("https://api.ipify.org/")
cl.set_proxy("http://username:[email protected]:8080")
after_ip = cl._send_public_request("https://api.ipify.org/")
print(f"Before: {before_ip}")
print(f"After: {after_ip}")Notes:
- Keep one stable proxy/IP per account whenever possible.
- Avoid rapidly rotating countries, cities, or carrier/mobile fingerprints for the same account.
- Prefer proxy hostnames that resolve through the proxy when using SOCKS, for example
socks5h://.... - Match
set_country(...),set_locale(...), device settings, and saved sessions to the account's normal environment. - Do not rotate proxy identity in the middle of a challenge, password reset, or relogin loop.
- Treat a shared IP pool as higher risk; reduce account count and request volume if you cannot dedicate an IP per account.
If BadPassword happens with a known-good password, use the BadPassword troubleshooting guide to separate real credential problems from Instagram trust/risk rejections.
Warm Accounts Gradually
New, restored, or previously challenged accounts should not immediately run high-volume actions.
Practical warmup rules:
- Start with read-heavy authenticated actions before write-heavy actions.
- Avoid repeated fresh password logins. Save settings and reuse the same device/session identity.
- Keep early write actions low and human-like: fewer follows, likes, comments, DMs, uploads, and profile edits.
- Increase volume slowly over days, not minutes.
- Stop and inspect the account manually after repeated challenges, forced password changes, or feedback blocks.
- Do not run the same workload across many accounts from the same fresh proxy pool at once.
Add Delays
It's recommended you try to mimic the behavior of a real user. This means you should add delays
between requests. The delays should be random and not too long.
The following is a good example of how to add delays between requests.
from instagrapi import Client
cl = Client()
# adds a random delay between 1 and 3 seconds after each request
cl.delay_range = [1, 3]Delays are only one layer. For larger jobs, also limit concurrency per account, per proxy, and per action type. A single account doing many parallel actions is more suspicious than the same work spread out with clear cooldowns.
Handle Rate Limits and Anti-Abuse Responses Explicitly
Instagram uses several different responses for throttling, suspicious behavior, or temporary restrictions. Treat them differently instead of retrying every error the same way.
`ClientThrottledError` / HTTP 429
This usually means the current IP or request pattern is too aggressive for that path right now.
Public web endpoints can hit this independently from private mobile endpoints; a 429 on a public GraphQL/web path does not necessarily mean the logged-in mobile session is broken.
Recommended response:
- stop the current burst of requests
- back off before retrying
- reduce concurrency for that account
- prefer authenticated/private helpers when a public web helper repeatedly fails
- try the optional
public_transport="curl"path only for public web endpoints, and treat it as a transport option, not a rate-limit bypass - if the same account keeps hitting
429, pause it or move it to a cleaner proxy/IP
`PleaseWaitFewMinutes`
This is usually more serious than a single 429. Instagram is telling you to slow down for that account, device, or IP combination.
Recommended response:
- stop write actions for that account for a while
- do not spam retries in a tight loop
- keep the same device settings and only retry later
- if it keeps happening, review proxy quality and account warmup
`FeedbackRequired`
This often means an action was blocked or the account is temporarily restricted.
Recommended response:
- inspect
client.last_json.get("feedback_message") - stop repeating the same action immediately
- freeze the account or action type for a cooldown window
- if you automate multiple action types, disable the offending one first
`LoginRequired`
This usually means the current private session is no longer valid.
Recommended response:
- relogin with the same saved device/session state
- validate the session with a lightweight authenticated call
- if relogin keeps failing, stop and inspect the account manually instead of forcing repeated fresh logins
`ChallengeRequired`
This means Instagram wants an additional verification step. Some flows can be automated, but newer challenge paths may still require manual review.
Recommended response:
- call
challenge_resolve(...)only if you have working handlers for codes/password changes - if you hit
/auth_platform/, UFAC web flows, or Bloks redirect checkpoints, expect manual handling in the official Instagram app or web flow - do not rotate identity aggressively in the middle of a challenge
Practical Rules
- Separate read-heavy jobs from write-heavy jobs whenever possible.
- Use one stable proxy/IP per account instead of hopping between locations.
- Persist sessions and device identifiers; avoid password login from scratch on every run.
- Freeze accounts that hit repeated anti-abuse responses instead of hammering them harder.
- Track errors per account, per proxy, and per action type so you can see which variable is actually causing trouble.
- Retry transport errors differently from account restrictions. A timeout may be retried; a challenge or feedback block needs cooldown and investigation.
Automation Warnings and Suspicious Login Screens
Instagram can show in-app warning screens or suspicious-login prompts even when the account is not fully challenged. These are usually trust signals tied to the account, device/session, proxy/IP history, and action pattern. They are not reliable to "bypass" generically from the library.
Recommended response:
- stop the affected workflow and inspect the account in the official app
- keep the same saved settings, device identifiers, locale, country, and proxy while investigating
- avoid repeated fresh password logins, aggressive proxy rotation, or replaying the same failed action
- capture sanitized diagnostics: method name, endpoint if visible, exception class,
client.last_json, status code, and whether the same account works in the official app - open an issue only when the same payload is reproducible; without
last_json, the project cannot safely add a named exception
Read-Only Monitoring Workloads
For small jobs that only check whether selected users posted new media or stories, keep the workflow boring:
- reuse one saved session with
dump_settings()/load_settings() - poll a small target set with a moderate interval, for example several minutes rather than seconds
- store the last seen media/story ids locally and compare new responses against that state
- keep this account separate from write-heavy actions such as follows, likes, comments, uploads, and Direct messages
- expect public/web paths to be opportunistic; reliable private-account story access requires that the logged-in account has permission to view the target
See examples/monitor_user_content.py for a minimal state-file based polling example.
Common Anti-Patterns
These patterns often create support issues that are not library bugs:
- logging in with username/password on every script run
- switching proxies after every request
- sharing one noisy IP across many unrelated accounts
- retrying
429,PleaseWaitFewMinutes,FeedbackRequired, or challenges in a tight loop - mixing browser
sessionidreuse with frequent private API password logins - treating a browser/web
sessionidas equivalent to a stable private mobile session after Instagram returnslogin_required - changing password, email, phone, profile, and posting behavior from a fresh proxy at the same time
- running write-heavy automation immediately after account creation or recovery
- ignoring
client.last_jsonand treating every exception as a generic transient failure
Separate Library Bugs from Operational Blocks
A library bug usually reproduces consistently for the same endpoint and payload, especially across accounts with healthy sessions. Useful reports include the method called, sanitized request/response data, stack trace, dependency versions, and whether the same account works in the official app.
An operational block is usually account, proxy, session, or action-pattern specific. Signs include BadPassword with a known-good password, 429, PleaseWaitFewMinutes, FeedbackRequired, suspicious login emails, challenges, forced password changes, or behavior that disappears after cooldown, cleaner proxy identity, or manual app verification.
When reporting an issue, include enough context to distinguish these cases. Remove cookies, session IDs, tokens, passwords, phone numbers, emails, and private user data before sharing logs.
Use Sessions
If you call .login() from scratch on every run, Instagram sees repeated fresh logins. That is much more suspicious than reusing a stable device session.
The normal pattern is:
- Login once
- Save settings with
.dump_settings() - Load them later with
.load_settings()or.set_settings() - Reuse the same device/session identifiers across runs
The first time you run your script
from instagrapi import Client
cl = Client()
cl.login(USERNAME, PASSWORD)
cl.dump_settings("session.json")And on the next run:
from instagrapi import Client
cl = Client()
cl.load_settings("session.json")
cl.login(USERNAME, PASSWORD)
cl.dump_settings("session.json")login() validates the loaded session before reusing it. When Instagram
returns login_required, instagrapi clears the rejected authorization and
logs in again with the supplied credentials. Other errors still propagate so
rate limits, challenges, and network failures are not mistaken for an expired
session. Save the settings again in case the session was refreshed.
If you want more explicit control over the loaded settings object:
from instagrapi import Client
cl = Client()
session = cl.load_settings("session.json")
cl.set_settings(session)
cl.login(USERNAME, PASSWORD)
cl.dump_settings("session.json")Putting this all together, you can write a reusable login helper like this:
from instagrapi import Client
from pathlib import Path
def login_user():
"""Login with a saved session and refresh it only when required."""
session_file = Path("session.json")
cl = Client()
if session_file.exists():
cl.load_settings(session_file)
cl.login(USERNAME, PASSWORD)
cl.dump_settings(session_file)
return clPrefer Read/Write Separation
If your workload is mostly data retrieval, keep that path separate from account-changing actions.
In practice:
- Reading data is usually easier to scale and safer to isolate.
- Writing actions such as posting, following, editing profile data, or sending DMs need much stricter operational hygiene.
- When possible, use official Instagram APIs for account-changing actions and keep private API automation focused on the gaps.
File: docs/usage-guide/challenge_resolver.md
Challenge Resolver
Instagrapi lets you attach handlers for common login challenge flows such as code verification and password reset.
New password challenge
You can automatically change your password to solve the challenge from Instagram.
Declare change_password_handler which will return a new password.
def change_password_handler(username):
# Simple way to generate a random string
chars = list("abcdefghijklmnopqrstuvwxyz1234567890!&ยฃ@#")
password = "".join(random.sample(chars, 8))
return password
cl = Client()
cl.change_password_handler = change_password_handler
cl.login(IG_USERNAME, IG_PASSWORD)Code verification challenge
You can automatically process the codes sent to you to solve the challenge from Instagram.
You need to declare challenge_code_handler which will return the code received from Instagram via Email or SMS:
from instagrapi.mixins.challenge import ChallengeChoice
def challenge_code_handler(username, choice):
if choice == ChallengeChoice.SMS:
return get_code_from_sms(username)
elif choice == ChallengeChoice.EMAIL:
return get_code_from_email(username)
return False
cl = Client()
cl.phone_number = "+15551234567" # required for submit_phone challenges
cl.challenge_code_handler = challenge_code_handler
cl.login(IG_USERNAME, IG_PASSWORD)Notes:
challenge_code_handler(username, choice)should return the received code as a string. Returning a falsey value means no code is available yet.- Login
submit_phonechallenges useclient.phone_number, then callchallenge_code_handler(username, ChallengeChoice.SMS)for the received code. - Signup SMS challenges use the
phone_numberpassed tosignup(...)and callchallenge_code_handler(username, ChallengeChoice.SMS)for the received code. - Phone-only signup is supported with
signup(username, password, email="", phone_number="+15551234567"). If bothemailandphone_numberare provided, instagrapi keeps the email signup flow and uses the phone number only for signup challenges. signup(...)emits aRuntimeWarningbecause it uses Instagram's legacy account-create flow and should be treated as experimental. On modern Instagram app versions this flow is often rejected withSignupSpamError/feedback_requiredbecause the official app uses additional signup checks that instagrapi does not currently generate. Treat this as a platform rejection, not as a malformed SMS/email code.- Current
masterraises a clearerChallengeRequiredfor/auth_platform/?apc=...flows. That path is not yet supported automatically and still requires manual verification. - Native challenge payloads with
challenge.native_flow=trueand an opaque/challenge/...api_pathare manual checkpoints. They do not expose an SMS/email/password step, sochallenge_code_handlerandchange_password_handlerwill not be called for them. - Bloks redirect checkpoints such as
bloks_action="com.bloks.www.ig.challenge.redirect.async"or placeholderstep_name="STEP_NAME"require manual confirmation in the official Instagram app or web flow on a trusted device; instagrapi raisesChallengeRequiredwith the sanitized challenge context instead of treating this as a legacy step. - For long-running automation, persist client settings around challenge handling so you can retry without rebuilding the entire device/session state.
Selfie and manual review challenges
ChallengeSelfieCaptcha and selfie/manual-review style flows are account review decisions by Instagram. instagrapi does not provide a generic bypass for these challenges. When they appear repeatedly during signup or login, stop the automated flow, keep the same account/device/proxy context, and resolve the account manually in the official app if possible.
For bug reports, include sanitized client.last_json, the exception class, and the flow that triggered it. Do not share cookies, session IDs, phone numbers, email addresses, passwords, or verification codes.
For example, you can get the code through the IMAP of Gmail:
def get_code_from_email(username):
mail = imaplib.IMAP4_SSL("imap.gmail.com")
mail.login(CHALLENGE_EMAIL, CHALLENGE_PASSWORD)
mail.select("inbox")
result, data = mail.search(None, "(UNSEEN)")
assert result == "OK", "Error1 during get_code_from_email: %s" % result
ids = data.pop().split()
for num in reversed(ids):
mail.store(num, "+FLAGS", "\\Seen") # mark as read
result, data = mail.fetch(num, "(RFC822)")
assert result == "OK", "Error2 during get_code_from_email: %s" % result
msg = email.message_from_string(data[0][1].decode())
payloads = msg.get_payload()
if not isinstance(payloads, list):
payloads = [msg]
code = None
for payload in payloads:
body = payload.get_payload(decode=True).decode()
if "([^>]*?({u})[^<]*?)<".format(u=username), body)
if not match:
continue
print("Match from email:", match.group(1))
match = re.search(r">(\d{6})<", body)
if not match:
print('Skip this email, "code" not found')
continue
code = match.group(1)
if code:
return code
return FalseAll challenges solved in the module challenge.py
Automatic submission code from SMS/Email in examples here
File: docs/usage-guide/collection.md
Collections
Saved posts, collections, and liked media.
| Method | Return | Description |
|---|---|---|
| collections() | List[Collection] | Get all collections for the authenticated account |
| collection_pk_by_name(name: str) | int | Resolve collection ID by collection name |
| collection_medias_by_name(name: str, amount: int = 21, last_media_pk: int = 0) | List[Media] | Get medias in a collection by collection name |
| collection_medias(collection_pk: str, amount: int = 21, last_media_pk: int = 0) | List[Media] | Get medias in a collection; use amount=0 to keep paginating |
| collection_medias_v1_chunk(collection_pk: str, max_id: str = "") | Tuple[List[Media], str] | Low-level chunk fetch with raw next_max_id cursor |
| liked_medias(amount: int = 21, last_media_pk: int = 0) | List[Media] | Get media liked by the current account |
| media_save(media_id: str, collection_pk: int = None) | bool | Save media, optionally into a specific collection |
| media_unsave(media_id: str, collection_pk: int = None) | bool | Remove media from saved posts or from a specific collection |
Example:
from instagrapi import Client
cl = Client()
cl.login(USERNAME, PASSWORD)
collections = cl.collections()
print([(item.id, item.name) for item in collections])
liked = cl.liked_medias(amount=10)
saved = cl.collection_medias("liked", amount=10)
travel_pk = cl.collection_pk_by_name("Travel")
travel_medias = cl.collection_medias(travel_pk, amount=0)
media_id = cl.media_id(cl.media_pk_from_url("https://www.instagram.com/p/CP5h-I1FuPr/"))
cl.media_save(media_id, collection_pk=travel_pk)
cl.media_unsave(media_id, collection_pk=travel_pk)Notes:
collection_pkcan be a numeric collection ID,"liked", or a saved-posts style collection path handled bycollection_medias_v1_chunk().liked_medias()is a convenience wrapper overcollection_medias("liked", ...).last_media_pkincollection_medias()is a resume point based on already seen media, not the raw server cursor.
File: docs/usage-guide/comment.md
Comment
Post comment, viewing, like and unlike comments
| Method | Return | Description |
|---|---|---|
| media_comment(media_id: str, text: str, replied_to_comment_id: Optional[int] = None) | Comment | Add a new comment to media or reply to an existing comment |
| media_comments(media_id: str, amount: int = 20) | List[Comment] | Get comments for media; pass amount=0 to keep paginating until exhaustion |
| media_comments_chunk(media_id: str, max_amount: int, min_id: str = None) | Tuple[List[Comment], str] | Get a paginated chunk of comments and the next min_id cursor |
| media_comments_v1(media_id: str, amount: int = 20) | List[Comment] | Get comments through the private mobile comments endpoint |
| media_comments_v1_chunk(media_id: str, min_id: str = "", max_id: str = "") | Tuple[List[Comment], str, str] | Get one private comments page and both cursors |
| media_stream_comments_v1_chunk(media_id: str, min_id: str = "", max_id: str = "") | Tuple[List[Comment], str, str] | Get one streamed comments page and both cursors |
| media_comments_gql(media_pk: str, amount: int = 50, max_requests: int = 0) | List[dict] | Get comments through the web GraphQL doc_id endpoint |
| media_comments_gql_chunk(media_pk: str, end_cursor: str = "") | Tuple[List[dict], str] | Get one web GraphQL comments page |
| media_comments_public_gql(code: str, amount: int = 50, max_requests: int = 0) | List[dict] | Get public web GraphQL comments by media shortcode |
| media_comments_public_gql_chunk(code: str, end_cursor: str = "") | Tuple[List[dict], str] | Get one public web GraphQL comments page by media shortcode |
| media_comments_threaded_gql(media_pk: str, comment_pk: str, amount: int = 0) | List[dict] | Get threaded replies through the web GraphQL doc_id endpoint |
| media_comments_threaded_gql_chunk(media_pk: str, comment_pk: str, end_cursor: str = "") | Tuple[List[dict], str] | Get one threaded GraphQL comments page |
| media_comment_infos(media_ids: List[str]) | dict | Bulk-fetch comment summaries for media ids |
| media_comment_replies(media_id: str, comment_id: str, amount: int = 0) | List[Comment] | Get replies for a parent media comment; pass amount=0 to keep paginating until exhaustion |
| media_comment_replies_chunk(media_id: str, comment_id: str, max_amount: int, min_id: str = None) | Tuple[List[Comment], str] | Get a paginated chunk of replies and the next child cursor |
| media_check_offensive_comment(media_id: str, text: str) | bool | Ask Instagram whether a comment text is considered offensive |
| media_check_offensive_comment_v2(media_id: str, comment: str) | dict | Lighter variant of media_check_offensive_comment โ same endpoint without with_action_data wrapping; returns the raw payload so callers can inspect category / confidence flags beyond is_offensive |
| comment_like(comment_pk: int, revert: bool = False) | bool | Like a comment |
| comment_unlike(comment_pk: int) | bool | Unlike a comment |
| comment_pin(media_id: str, comment_pk: int, revert: bool = False) | bool | Pin a comment on your media |
| comment_unpin(media_id: str, comment_pk: int) | bool | Unpin a previously pinned comment |
| comment_bulk_delete(media_id: str, comment_pks: List[int]) | bool | Delete one or more comments from your media |
| comment_likers_gql(comment_pk: str, amount: int = 0) | List[dict] | Get comment likers through public GraphQL |
| comment_likers_gql_chunk(comment_pk: str, end_cursor: str = "") | Tuple[List[dict], str] | Get one public GraphQL comment-likers page |
Example:
/* Detailed source-code truncated for AI context efficiency. */Notes:
media_comments()fetches both regular and headload comment pages untilamountis reached.media_comments_chunk()is the better choice when you want to store and resume the server cursor manually.media_comments_public_gql()accepts a media shortcode and automatically builds the current public GraphQLdoc_idrequest and post referer. Public web endpoints are still Instagram-gated and may return 401/403/429 depending on IP, TLS fingerprint, cookies, or session state.media_comment_replies()fetchesinline_child_commentsfor a parent comment and paginates with the returned child cursor.media_check_offensive_comment_v2()can be used as an explicit lightweight preflight beforemedia_comment().media_comment()does not run extra preflight requests automatically, so callers can choose the request volume and handle the raw response.comment_pin()/comment_unpin()only work on media owned by the authenticated account.- Reply creation is supported through
replied_to_comment_id; reply retrieval is supported throughmedia_comment_replies(). - Comment creation is a write action and can still trigger Instagram spam or trust checks. Reuse saved sessions, keep volume low on new accounts, and stop when Instagram returns feedback/challenge responses.
File: docs/usage-guide/direct.md
Direct
| Method | Return | Description |
|---|---|---|
direct_threads(amount: int = 20, selected_filter: Optional[Literal["flagged", "unread"]] = None, box: Optional[Literal["primary", "general"]] = None, thread_message_limit: Optional[int] = None) Note: omit selected_filter / box or pass None for the default inbox |
List[DirectThread] | Get all threads from inbox |
| direct_pending_inbox(amount: int = 20) | List[DirectThread] | Get all threads from pending inbox |
| direct_requests(amount: int = 20) | List[DirectThread] | Get message request threads (pending inbox / invitations) |
| direct_pending_requests_preview(pending_inbox_filters: Optional[List[str]] = None) | Dict | Get lightweight pending request counters |
| direct_request_approve(thread_id: int) | bool | Approve a message request thread |
| direct_has_interop_upgraded() | bool | Check Direct interop upgrade state |
| direct_search_gen_ai_bots(amount: int = 20) | List[UserShort] | Search generated AI bot suggestions |
| direct_channels(user_id: Optional[int] = None, thread_subtypes: Optional[List[int]] = None) | List[Dict] | Get Direct channels for a user |
| direct_set_e2ee_eligibility(e2ee_eligibility: int = 4) | bool | Set Direct E2EE eligibility state |
| direct_thread(thread_id: int, amount: int = 20) | DirectThread | Get Thread with Messages |
| direct_messages(thread_id: int, amount: int = 20) | List[DirectMessage] | Get only Messages in Thread |
| direct_message(thread_id: int, message_id: int, amount: int = 20) | DirectMessage | Get one Message from Thread by id |
| direct_answer(thread_id: int, text: str) | DirectMessage | Add Message to exist Thread |
| direct_send(text: str, user_ids: List[int] = [], thread_ids: List[int] = [], reply_to_message: Optional[DirectMessage] = None) | DirectMessage | Send Message to Users or Threads, optionally as a reply |
| direct_send_reaction(thread_id: int, message_id: int, emoji: str = "โค", client_context: Optional[str] = None) | bool | Send an emoji reaction to a message |
| direct_delete_reaction(thread_id: int, message_id: int, emoji: str = "โค", client_context: Optional[str] = None) | bool | Delete your emoji reaction from a message |
| direct_message_like(thread_id: int, message_id: int, client_context: Optional[str] = None) | bool | Like a message with a heart reaction |
| direct_message_unlike(thread_id: int, message_id: int, client_context: Optional[str] = None) | bool | Remove your heart reaction from a message |
| direct_search(query: str) | List[DirectShortThread] | Search threads (for example by username) |
| direct_thread_by_participants(user_ids: List[int]) | DirectThread | Get thread by user_id |
| direct_thread_create(user_ids: List[int], title: str = "") | str | Create a group thread and return its thread id |
| direct_thread_add_users(thread_id: int, user_ids: List[int]) | bool | Add users to a group thread |
| direct_thread_hide(thread_id: int) | bool | Delete (called "hide") |
| direct_thread_update_title(thread_id: int, title: str) | bool | Update a group thread title |
| direct_media_share(media_id: str, user_ids: List[int] = [], thread_ids: List[int] = [], send_attribute: SEND_ATTRIBUTE_MEDIA = "feed_timeline", media_type: DirectMediaType = "photo") | DirectMessage | Share a media to users or existing threads |
| direct_story_share(story_id: str, user_ids: List[int], thread_ids: List[int]) | DirectMessage | Share a story to list of users |
| direct_profile_share(user_id: str, user_ids: List[int], thread_ids: List[int]) | DirectMessage | Share a user profile to list of users |
| direct_thread_mark_unread(thread_id: int) | bool | Mark a thread as unread |
| direct_message_delete(thread_id: int, message_id: int) | bool | Delete a message from thread |
| direct_message_unsend(thread_id: int, message_id: int) | bool | Unsend a message from thread |
| direct_thread_mute(thread_id: int, revert: bool = False) | bool | Mute the thread |
| direct_thread_unmute(thread_id: int) | bool | Unmute the thread |
| direct_thread_mute_video_call(thread_id: int, revert: bool = False) | bool | Mute video call for the thread |
| direct_thread_unmute_video_call(thread_id: int) | bool | Unmute video call for the thread |
| direct_send_photo(path: Path, user_ids: List[int], thread_ids: List[int]) | DirectMessage | Send a direct photo to users in an existing thread or directly to thread ids |
| direct_send_video(path: Path, user_ids: List[int], thread_ids: List[int]) | DirectMessage | Send a direct video (.mp4 / H.264 + AAC) to list of users or threads |
| direct_send_voice(path: Path, user_ids: List[int], thread_ids: List[int], waveform: Optional[List[float]]) | DirectMessage | Send a direct voice (audio) message; path must be m4a/AAC |
| video_upload_to_direct(path: Path, caption: str, thumbnail: Path, mentions: List[StoryMention], thread_ids: List[int] = [], extra_data: Dict[str, str] = {}) | DirectMessage | Upload video to direct thread as a story and configure it |
Option types
Direct media type is exposed as DirectMediaType = Literal["photo", "video"].
| Type | Values | Used by |
|---|---|---|
DirectMediaType |
"photo", "video" |
direct_send_file(content_type=...), direct_media_share(media_type=...) |
Notes:
- For
direct_send(),direct_media_share(),direct_send_photo(),direct_send_video(), anddirect_send_voice(), pass exactly one ofuser_idsorthread_ids. - Direct recipient arguments accept either one id (
user_ids=123) or a list of ids (user_ids=[123]). - Direct photo, video, and voice attachments sent with
user_idsrequire an existing Direct thread. Usedirect_send()to start the conversation first, or preferthread_idswhen the thread id is already known. - Direct message requests / invitations are exposed as
direct_requests();direct_pending_inbox()remains as the older name. direct_pending_requests_preview()is the lightweight Android-app preview for request counters; usedirect_requests()when you need the actual threads.direct_channels()anddirect_search_gen_ai_bots()expose raw app surfaces whose response shape may vary by rollout.direct_thread()paginates internally until it collectsamountmessages or reaches the end of the thread.direct_message()scans the latestamountmessages in a thread and raisesDirectMessageNotFoundif the id is not present in that window.- Shared XMA items such as
xma_clip,xma_media_share,xma_story_share, andxma_profilekeep their original payload inmessage.raw_xma. When Instagram includestarget_url, the normalized link is also available throughmessage.xma_share. - Disappearing direct photos and videos, including incoming Direct quicksnaps with
item_type == "raven_media", are exposed throughmessage.visual_media. - Media-changing direct endpoints are more sensitive to session quality than read-only inbox calls. Stable sessions loaded via
dump_settings()/load_settings()are more reliable than browser-onlysessionidreuse.
Incoming quicksnaps
Instagram returns snaps sent to you through Direct threads, not through quicksnap_history().
Fetch the inbox or a known thread and filter Direct messages whose item_type is"raven_media" and whose visual_media field is present.
threads = cl.direct_threads(amount=20, thread_message_limit=20)
for thread in threads:
for message in thread.messages:
if message.item_type == "raven_media" and message.visual_media:
media = message.visual_media.media
image_versions = media.image_versions2
video_versions = media.video_versions or []
image_url = image_versions.candidates[-1].url if image_versions else None
video_url = video_versions[-1].url if video_versions else None
print(thread.id, message.id, media.media_type, image_url or video_url)For one known thread:
messages = cl.direct_messages(thread_id, amount=50)
incoming_snaps = [message for message in messages if message.item_type == "raven_media" and message.visual_media]Instagram may omit playable media URLs after a disappearing photo or video has
expired or already been viewed. In that case the message can still appear in the
thread as a placeholder or action summary. quicksnap_history() returns the
authenticated user's QuickSnap history payload and is not the Direct inbox for
snaps sent by other users.
Handling disabled message requests:
from instagrapi.exceptions import DirectMessageRequestsDisabled
try:
cl.direct_send("Hello", user_ids=[user_id])
except DirectMessageRequestsDisabled:
print("The recipient does not accept new Direct message requests.")Example of basic actions:
/* Detailed source-code truncated for AI context efficiency. */File: docs/usage-guide/fundamentals.md
Usage Guide
This section provides detailed descriptions of all the ways instagrapi can be used. If you are new to instagrapi, the
Getting Started page provides a gradual introduction of the basic functionality with examples.
Public vs Private Requests
Publicweb methods have a suffix_gql(InstagramGraphQL). Some_gqlhelpers use newerdoc_idqueries when Instagram disables legacyquery_hashendpoints. Legacy?__a=1helpers were removed because that public web response is no longer reliable.Private(authorized request via mobile api) methods have_v1suffix
Public web flows are opportunistic, not guaranteed. Instagram can change or block them independently of the library.
Many high-level helpers try a public/web path first and then use a private/authenticated fallback when that makes sense for the current session.
Not every high-level helper has a public and private twin. Some newer flows are private-only, while some lookup helpers
use an internal fallback chain and choose the best currently working path for the session.
Detailed Sections
- Index
- Getting Started
- Interactions
Media- Publication (also called post): Photo, Video, Album, IGTV and ReelsResource- Part of Media (for albums)MediaOembed- Short version of MediaAccount- Full private info for your account (e.g. email, phone_number)User- Full public user dataUserShort- Short public user data (used in Usertag, Comment, Media, Direct Message)Usertag- Tag user in Media (coordinates + UserShort)Location- GEO location (GEO coordinates, name, address)Hashtag- Hashtag object (id, name, picture)Collection- Collection of medias (name, picture and list of medias)Comment- Comments to MediaHighlight- HighlightsNotes- Direct NotesRealtime MQTT- Direct sync, lightweight Direct MQTT actions, and FBNS push callbacksPydroid and ffmpeg- Android/Pydroid video upload setupTermux- Termux install notes and optional video helpersStory- StoryStoryArchiveDay- Story archive day shellStoryLink- Story link stickerStoryLocation- Tag Location in Story (as sticker)StoryMention- Mention users in Story (user, coordinates and dimensions)StoryHashtag- Hashtag for story (as sticker)StorySticker- Tag sticker to story (for example from giphy)StoryBuild- StoryBuilder return path to photo/video and mention co-ordinatesDirectThread- Thread (topic) with messages in Direct MessageDirectMessage- Message in Direct MessageInsight- Insights for a postTrack- Music track (for Reels/Clips)
- Best Practices
- Development Guide
- Handle Exceptions
- Exceptions
File: docs/usage-guide/handle_exception.md
You can handle any exceptions using a generic handler:
/* Detailed source-code truncated for AI context efficiency. */In this way, you can centrally handle errors and not repeat handlers throughout your code. In a real application, you would usually extend this with your own proxy rotation, account freeze/backoff storage, or metrics hooks.
For BadPassword with a password that works elsewhere, see the BadPassword troubleshooting guide. Instagram can return bad_password for risky proxy/IP/device/session states, so avoid retry loops and first compare against the official app on the same network identity.
For a practical playbook around 429, feedback_required, PleaseWaitFewMinutes, and session/challenge handling, see Best Practices.
Full example here
File: docs/usage-guide/hashtag.md
Hashtag
Viewing hashtag info and medias by hashtag
Pass hashtag names without the leading #, for example pizza. If a leading # is provided, instagrapi strips it and emits a warning; an empty hashtag name raises ValueError.
| Method | Return | Description |
|---|---|---|
| hashtag_info(name: str) | Hashtag | Return hashtag info (id, name, media_count, profile_pic_url) |
| hashtag_medias_top(name: str, amount: int = 9) | List[Media] | Return top posts for a hashtag |
| hashtag_medias_recent(name: str, amount: int = 27) | List[Media] | Return recent posts for a hashtag |
| hashtag_medias_paginated(name: str, amount: int = 27, tab_key: str = "recent", end_cursor: str = None) | Tuple[List[Media], str] | Return one hashtag media page plus the next cursor; authenticated sessions use private/mobile pagination first |
| iter_hashtag_medias(name: str, amount: int = 0, page_size: int = 27, tab_key: str = "recent") | Iterator[Media] | Stream hashtag media page by page without building a full list |
| hashtag_medias_reels_v1(name: str, amount: int = 27) | List[Media] | Return reels/clips for a hashtag via private API |
| hashtag_follow(hashtag: str, unfollow: bool = False) | bool | Follow a hashtag |
| hashtag_following(amount: int = 0) | List[Hashtag] | Return hashtags followed by the authenticated account |
| hashtag_unfollow(hashtag: str) | bool | Unfollow a hashtag |
Example:
/* Detailed source-code truncated for AI context efficiency. */Stream hashtag media without storing every fetched page:
for media in cl.iter_hashtag_medias("downhill", amount=100, page_size=25, tab_key="recent"):
print(media.pk, media.code)Low level methods:
| Method | Return | Description |
|---|---|---|
| hashtag_info_gql(name: str, amount: int = 12, end_cursor: str = None) | Hashtag | Get information about a hashtag by Public Graphql API |
| hashtag_info_v1(name: str) | Hashtag | Get information about a hashtag by Private Mobile API |
| hashtag_medias_paginated_gql(name: str, amount: int = 27, end_cursor: str = None) | Tuple[List[Media], str] | Get one recent hashtag media page by Public GraphQL API |
| hashtag_medias_paginated_v1(name: str, amount: int = 27, tab_key: Literal["top", "recent", "clips"] = "recent", end_cursor: str = None) | Tuple[List[Media], str] | Get one hashtag media page by Private Mobile API |
| iter_hashtag_medias(name: str, amount: int = 0, page_size: int = 27, tab_key: str = "recent") | Iterator[Media] | Stream hashtag medias page by page through hashtag_medias_paginated() |
| hashtag_medias_v1_chunk(name: str, max_amount: int = 27, tab_key: Literal["top", "recent", "clips"] = "top", max_id: str = None) | Tuple[List[Media], str] | Get chunk of medias for a hashtag and max_id (cursor) by Private Mobile API |
| hashtag_medias_v1(name: str, amount: int = 27, tab_key: Literal["top", "recent", "clips"] = "top") | List[Media] | Get medias for a hashtag by Private Mobile API |
| hashtag_medias_top_v1(name: str, amount: int = 9) | List[Media] | Get top medias for a hashtag by Private Mobile API |
| hashtag_medias_recent_v1(name: str, amount: int = 27) | List[Media] | Get recent medias for a hashtag by Private Mobile API |
| hashtag_medias_reels_v1(name: str, amount: int = 27) | List[Media] | Get recent clips (reels) for a hashtag by Private Mobile API |
Example for Request for loading every next time new posts from hashtag:
>>> medias, cursor = cl.hashtag_medias_paginated('test', amount=32, tab_key='recent')
>>> next_medias, cursor = cl.hashtag_medias_paginated('test', amount=32, tab_key='recent', end_cursor=cursor)
>>> medias, cursor = cl.hashtag_medias_v1_chunk('test', max_amount=32, tab_key='recent')
>>> len(medias)
32
>>> cursor
QVFDR0dzT3FJT0V4amFjMaQ3czlGVzRKV3FNWDJqaE1mWmltWU5VWGYtbnV6RVpoOUlsR3dCN05RRmpLc2R5SVlCQTNaekV5bUVOV0F4Vno1MDkxN1Nndg==
# NEXT cursor:
>>> medias, cursor = cl.hashtag_medias_v1_chunk('test', max_amount=32, tab_key='recent', max_id=cursor)
>>> len(medias)
32
>>> cursor
QVFEUXpfM0RtaDdmMExPQ0k0UWRlaHFJa2RVdVlaX01LTzhkNF9Dd1N2UlhtVy1vSTZvMERfYW5XN205OTBRNFBCSVJ2ZTVfTG5ZMXVmY0VJbUM5TU9URQ==
>>> cl.hashtag_follow("python")
True
>>> cl.hashtag_following(amount=1)
[Hashtag(id='17841562401125985', name='python', media_count=12000000, profile_pic_url=None)]
>>> cl.hashtag_unfollow("python")
TrueNotes:
- Instagram's old public hashtag web page JSON (
?__a=1) is no longer reliable and the_a1helpers were removed. Use the high-level hashtag methods or the authenticated private/mobile_v1helpers. - High-level
hashtag_info(),hashtag_medias_top(), andhashtag_medias_recent()use the private/mobile helpers. hashtag_following()returns the authenticated following-list hashtag preview. Instagram may limit how many followed hashtags are included.- For resumable pagination, prefer
hashtag_medias_paginated()and persist the returned cursor. Usehashtag_medias_v1_chunk()only when you need the low-level private/mobile helper. - Use
iter_hashtag_medias()when you want to process large hashtag result sets incrementally. It uses the same pagination path ashashtag_medias_paginated(). hashtag_medias_v1_chunk()acceptstop,recent, orclipsastab_key.