instagrapi Documentation

The fastest and powerful Python library for Instagram Private API 2026 with HikerAPI SaaS

RAW Doc

File: README.md

instagrapi

โš ๏ธ Telegram support group moved to aiograpi_support โ€” the previous @instagrapi group 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

bash
pip install instagrapi

Optional public web TLS impersonation support is available as an extra:

bash
pip install "instagrapi[curl]"

Use it only for public web endpoints that are sensitive to browser TLS fingerprints:

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

bash
uv add instagrapi

Or install it into the active virtual environment:

bash
uv pip install instagrapi

Video 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:

bash
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

python
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

python
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 with
login_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:

python
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

python
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

python
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

python
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

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

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

python
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_latest and date_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:

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:

Comparing instagrapi to other tools:

python
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)],
)

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:

bash
git tag -a X.Y.Z -m "Release X.Y.Z"
git push origin X.Y.Z
git push codeberg X.Y.Z

The 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:

text
/* Detailed source-code truncated for AI context efficiency. */

Notes:

  • account_edit(**data) only applies supported fields and preserves missing required profile fields from account_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() sends to_account_type=2; account_convert_to_creator() sends to_account_type=3. The generic helper exposes this as ProfessionalAccountType = 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() and insights_* 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:

python
>>> 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."
  • BadPassword even 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:

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

python
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 sessionid reuse with frequent private API password logins
  • treating a browser/web sessionid as equivalent to a stable private mobile session after Instagram returns login_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_json and 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:

  1. Login once
  2. Save settings with .dump_settings()
  3. Load them later with .load_settings() or .set_settings()
  4. Reuse the same device/session identifiers across runs

The first time you run your script

python
from instagrapi import Client

cl = Client()
cl.login(USERNAME, PASSWORD)
cl.dump_settings("session.json")

And on the next run:

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

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

python
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 cl

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

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

python
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_phone challenges use client.phone_number, then call challenge_code_handler(username, ChallengeChoice.SMS) for the received code.
  • Signup SMS challenges use the phone_number passed to signup(...) and call challenge_code_handler(username, ChallengeChoice.SMS) for the received code.
  • Phone-only signup is supported with signup(username, password, email="", phone_number="+15551234567"). If both email and phone_number are provided, instagrapi keeps the email signup flow and uses the phone number only for signup challenges.
  • signup(...) emits a RuntimeWarning because it uses Instagram's legacy account-create flow and should be treated as experimental. On modern Instagram app versions this flow is often rejected with SignupSpamError / feedback_required because 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 master raises a clearer ChallengeRequired for /auth_platform/?apc=... flows. That path is not yet supported automatically and still requires manual verification.
  • Native challenge payloads with challenge.native_flow=true and an opaque /challenge/... api_path are manual checkpoints. They do not expose an SMS/email/password step, so challenge_code_handler and change_password_handler will not be called for them.
  • Bloks redirect checkpoints such as bloks_action="com.bloks.www.ig.challenge.redirect.async" or placeholder step_name="STEP_NAME" require manual confirmation in the official Instagram app or web flow on a trusted device; instagrapi raises ChallengeRequired with 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:

python
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 False

All 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:

python
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_pk can be a numeric collection ID, "liked", or a saved-posts style collection path handled by collection_medias_v1_chunk().
  • liked_medias() is a convenience wrapper over collection_medias("liked", ...).
  • last_media_pk in collection_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:

text
/* Detailed source-code truncated for AI context efficiency. */

Notes:

  • media_comments() fetches both regular and headload comment pages until amount is 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 GraphQL doc_id request 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() fetches inline_child_comments for a parent comment and paginates with the returned child cursor.
  • media_check_offensive_comment_v2() can be used as an explicit lightweight preflight before media_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 through media_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(), and direct_send_voice(), pass exactly one of user_ids or thread_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_ids require an existing Direct thread. Use direct_send() to start the conversation first, or prefer thread_ids when 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; use direct_requests() when you need the actual threads.
  • direct_channels() and direct_search_gen_ai_bots() expose raw app surfaces whose response shape may vary by rollout.
  • direct_thread() paginates internally until it collects amount messages or reaches the end of the thread.
  • direct_message() scans the latest amount messages in a thread and raises DirectMessageNotFound if the id is not present in that window.
  • Shared XMA items such as xma_clip, xma_media_share, xma_story_share, and xma_profile keep their original payload in message.raw_xma. When Instagram includes target_url, the normalized link is also available through message.xma_share.
  • Disappearing direct photos and videos, including incoming Direct quicksnaps with item_type == "raven_media", are exposed through message.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-only sessionid reuse.

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.

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

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

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

text
/* 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

  • Public web methods have a suffix _gql (Instagram GraphQL). Some _gql helpers use newer doc_id queries when Instagram disables legacy query_hash endpoints. Legacy ?__a=1 helpers were removed because that public web response is no longer reliable.
  • Private (authorized request via mobile api) methods have _v1 suffix

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


File: docs/usage-guide/handle_exception.md

You can handle any exceptions using a generic handler:

text
/* 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:

text
/* Detailed source-code truncated for AI context efficiency. */

Stream hashtag media without storing every fetched page:

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

python
>>> 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")
True

Notes:

  • Instagram's old public hashtag web page JSON (?__a=1) is no longer reliable and the _a1 helpers were removed. Use the high-level hashtag methods or the authenticated private/mobile _v1 helpers.
  • High-level hashtag_info(), hashtag_medias_top(), and hashtag_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. Use hashtag_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 as hashtag_medias_paginated().
  • hashtag_medias_v1_chunk() accepts top, recent, or clips as tab_key.