## File: README.md

# instagrapi

> ⚠️ **Telegram support group moved to [aiograpi_support](https://t.me/aiograpi_support)** — the previous `@instagrapi` group has been restricted by Meta and is no longer maintained.

[](https://pypi.org/project/instagrapi/)
[](https://pypi.org/project/instagrapi/)
[](LICENSE)
[](https://github.com/subzeroid/instagrapi/actions/workflows/python-package.yml)
[](https://subzeroid.github.io/instagrapi/)

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](https://hikerapi.com/) 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](https://github.com/subzeroid/aiograpi) ✨

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](docs/usage-guide/public-transport.md) 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](https://docs.astral.sh/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](docs/usage-guide/pydroid.md) and [Termux](docs/usage-guide/termux.md).

## 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](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](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](docs/usage-guide/realtime.md) 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](https://subzeroid.github.io/instagrapi/usage-guide/fundamentals.html) and [Mobile API](https://subzeroid.github.io/instagrapi/usage-guide/fundamentals.html) flows where available
* Supports login by password, 2FA, 8-digit backup codes, `sessionid`, and [Bloks 2FA](https://subzeroid.github.io/instagrapi/usage-guide/totp.html#bloks-two-factor-flow) fallback/helpers for newer verification flows
* Includes email/SMS-based [challenge resolver](https://subzeroid.github.io/instagrapi/usage-guide/challenge_resolver.html) 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](https://subzeroid.github.io/instagrapi/):

* [Documentation index](https://subzeroid.github.io/instagrapi/)
* [Getting Started](https://subzeroid.github.io/instagrapi/getting-started.html)
* [Usage Guide](https://subzeroid.github.io/instagrapi/usage-guide/fundamentals.html)
* [Interactions reference](https://subzeroid.github.io/instagrapi/usage-guide/interactions.html)
* [Best Practices](https://subzeroid.github.io/instagrapi/usage-guide/best-practices.html) for sessions, proxies, and anti-abuse handling
* [Handle Exceptions](https://subzeroid.github.io/instagrapi/usage-guide/handle_exception.html) for centralizing `429`, challenge, and relogin logic
* [GitHub Discussions](https://github.com/subzeroid/instagrapi/discussions)
* Support chat in Telegram: [aiograpi_support](https://t.me/aiograpi_support) — the previous `@instagrapi` group was restricted by Meta and is no longer maintained

For other languages, consider [instagrapi-rest](https://github.com/subzeroid/instagrapi-rest). For async Python, see [aiograpi](https://github.com/subzeroid/aiograpi).

## Tutorials

Hands-on guides for real instagrapi work — login flows, sessions, proxies, scraping, posting, error handling — live at [instagrapi.com/guides](https://instagrapi.com/guides/):

* [Instagram Private API in Python](https://instagrapi.com/guides/instagram-private-api-python/) — pillar walkthrough: login, sessions, fetching, posting
* [2FA and `challenge_required`](https://instagrapi.com/guides/instagrapi-2fa-challenge/)
* [Session persistence: file, Redis, and Postgres patterns](https://instagrapi.com/guides/instagrapi-session-persistence/)
* [Configuring proxies (HTTP, SOCKS5, residential)](https://instagrapi.com/guides/instagrapi-proxy-setup/)
* [Instagram scraper in Python: a working setup](https://instagrapi.com/guides/instagram-scraper-python/)
* [Upload a photo from Python](https://instagrapi.com/guides/instagrapi-upload-photo-python/)
* [Download Instagram stories](https://instagrapi.com/guides/instagrapi-download-stories-python/)
* [Common errors reference](https://instagrapi.com/guides/errors/) — `bad_password`, `challenge_required`, `login_required`, `please_wait_a_few_minutes`, `feedback_required`, `proxy_address_is_blocked`
* [`BadPassword`: correct password rejected by Instagram](https://instagrapi.com/guides/errors/bad-password/) — proxy/IP/device/session trust troubleshooting
* [Framework integrations](https://instagrapi.com/guides/integrations/) — Django, FastAPI, Celery, Docker, AWS Lambda

Comparing instagrapi to other tools:

* [instagrapi vs Instaloader](https://instagrapi.com/compare/instaloader/) — download-only vs authenticated automation
* [instagrapi vs aiograpi](https://instagrapi.com/guides/instagrapi-vs-aiograpi/) — sync or async
* [Instagram API libraries by language](https://instagrapi.com/guides/instagram-api-libraries-by-language/) — what's actually maintained in 2026



    Additional example

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


## Related Projects

If you need async Python, use [aiograpi](https://github.com/subzeroid/aiograpi).

For other languages, see [instagrapi-rest](https://github.com/subzeroid/instagrapi-rest).
For hosted production Instagram API infrastructure, see [HikerAPI](https://hikerapi.com/).

Related services:

* [Cloqly](https://cloqly.com/register?ref=58dbf70f) for premium rotating proxies and stable automation traffic
* [DataLikers](https://datalikers.com/p/S9Lv5vBy) for Instagram MCP, Cache API, and datasets
* [LamaTok](https://lamatok.com/p/B9ScEYIQ) for TikTok API access, automation, and data workflows
* [InstaSurfBot](https://t.me/InstaSurfBot) for downloading Instagram media in Telegram
* [OSINTagramBot](https://t.me/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

[](https://github.com/subzeroid/instagrapi/graphs/contributors)

For local setup, tests, linting, and pull request expectations, see [CONTRIBUTING.md](CONTRIBUTING.md) and the [development guide](https://subzeroid.github.io/instagrapi/development-guide.html).

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](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 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:password@proxy.example.com: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](https://instagrapi.com/guides/errors/bad-password/) 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`](https://github.com/subzeroid/instagrapi/blob/master/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](https://github.com/subzeroid/instagrapi/blob/master/instagrapi/mixins/challenge.py)

Automatic submission code from SMS/Email in examples [here](https://github.com/subzeroid/instagrapi/blob/master/examples/challenge_resolvers.py)

---

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

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

```
/* 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](../getting-started.md) 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

* [Index](../index.md)
* [Getting Started](../getting-started.md)
* [Interactions](interactions.md)
  * [`Media`](media.md) - Publication (also called post): Photo, Video, Album, IGTV and Reels
  * [`Resource`](media.md) - Part of Media (for albums)
  * [`MediaOembed`](media.md) - Short version of Media
  * [`Account`](account.md) - Full private info for your account (e.g. email, phone_number)
  * [`User`](user.md) - Full public user data
  * [`UserShort`](user.md) - Short public user data (used in Usertag, Comment, Media, Direct Message)
  * [`Usertag`](user.md) - Tag user in Media (coordinates + UserShort)
  * [`Location`](location.md) - GEO location (GEO coordinates, name, address)
  * [`Hashtag`](hashtag.md) - Hashtag object (id, name, picture)
  * [`Collection`](collection.md) - Collection of medias (name, picture and list of medias)
  * [`Comment`](comment.md) - Comments to Media
  * [`Highlight`](highlight.md) - Highlights
  * [`Notes`](notes.md) - Direct Notes
  * [`Realtime MQTT`](realtime.md) - Direct sync, lightweight Direct MQTT actions, and FBNS push callbacks
  * [`Pydroid and ffmpeg`](pydroid.md) - Android/Pydroid video upload setup
  * [`Termux`](termux.md) - Termux install notes and optional video helpers
  * [`Story`](story.md) - Story
  * [`StoryArchiveDay`](story.md) - Story archive day shell
  * [`StoryLink`](story.md) - Story link sticker
  * [`StoryLocation`](story.md) - Tag Location in Story (as sticker)
  * [`StoryMention`](story.md) - Mention users in Story (user, coordinates and dimensions)
  * [`StoryHashtag`](story.md) - Hashtag for story (as sticker)
  * [`StorySticker`](story.md) - Tag sticker to story (for example from giphy)
  * [`StoryBuild`](story.md) - [StoryBuilder](https://github.com/subzeroid/instagrapi/blob/master/instagrapi/story.py) return path to photo/video and mention co-ordinates
  * [`DirectThread`](direct.md) - Thread (topic) with messages in Direct Message
  * [`DirectMessage`](direct.md) - Message in Direct Message
  * [`Insight`](insight.md) - Insights for a post
  * [`Track`](track.md) - Music track (for Reels/Clips)
* [Best Practices](best-practices.md)
* [Development Guide](../development-guide.md)
* [Handle Exceptions](handle_exception.md)
* [Exceptions](../exceptions.md)

---

## 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](https://instagrapi.com/guides/errors/bad-password/). 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](best-practices.md).

Full example [here](https://github.com/subzeroid/instagrapi/blob/master/examples/handle_exception.py)

---

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

``` 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](https://github.com/subzeroid/instagrapi/issues/79):

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