Instagram API Pagination: How to Get All Posts from a User

One API call to user_media returns about 12 posts. That's by design — Instagram serves feeds in pages, and so does any API built on top of it. If you want every post from an account (or every comment on a post, or every Reel), you need to paginate with cursors.

This guide documents exactly how pagination works on the Instagram Cheapest API: which cursor parameter each endpoint takes, where to find the cursor in the response, how to pace your loop against your plan's rate limit, and what a full crawl actually costs (spoiler: scraping a 1,000-post account costs about a cent in request fees).

The cursor cheat sheet

Five of the nine endpoints are paginated. Each one accepts an optional cursor parameter — omit it on the first call, then pass the cursor value you find in the previous response:

Endpoint Cursor parameter Where the next cursor appears in the response
user_media next_max_id Top-level next_max_id; keep going while more_available is true
user_reels after page_info.end_cursor; keep going while page_info.has_next_page is true
user_tag_media after page_info.end_cursor
media_comments after page_info.end_cursor
reels_audio max_id Paging cursor in the response (e.g. paging_info.max_id)

The non-paginated endpoints — user/{username}, user_by_user_id, username_by_uid, and media_by_code2 — return a single resource, so there's nothing to page through.

Responses are raw Instagram JSON, so the exact nesting around page_info differs per endpoint and can shift when Instagram reshuffles its payloads. The defensive pattern further down handles that without hardcoding deep paths.

Setup

You'll need a RapidAPI key (the free Basic plan gives you 30 requests/month to test) and the numeric user ID of the account. If you only have the username, resolve it first — that's covered in how to get an Instagram user ID from a username.

import os
import time
import requests

BASE = "https://instagram-cheapest.p.rapidapi.com/api/v1/instagram"
HEADERS = {
    "x-rapidapi-key": os.environ["RAPIDAPI_KEY"],
    "x-rapidapi-host": "instagram-cheapest.p.rapidapi.com",
}

Fetching every post with next_max_id

The user_media endpoint has the friendliest pagination contract: the response carries items (the posts), more_available (whether another page exists), and next_max_id (the cursor for the next page) at the top level.

def fetch_all_user_media(user_id, max_pages=None, pause=3.1):
    """Fetch every post from a public account, page by page."""
    items, cursor, page = [], "", 0

    while True:
        params = {"user_id": user_id}
        if cursor:
            params["next_max_id"] = cursor

        resp = requests.get(f"{BASE}/user_media", headers=HEADERS,
                            params=params, timeout=30)
        resp.raise_for_status()
        data = resp.json()

        batch = data.get("items", [])
        items.extend(batch)
        page += 1
        print(f"page {page}: +{len(batch)} posts ({len(items)} total)")

        if not data.get("more_available"):
            break                       # reached the oldest post
        cursor = data.get("next_max_id") or ""
        if not cursor:
            break                       # no cursor — stop defensively
        if max_pages and page >= max_pages:
            break                       # optional safety cap

        time.sleep(pause)               # stay inside your plan's rate limit

    return items


posts = fetch_all_user_media("25025320")   # e.g. resolve "nike" -> 25025320 first
print(f"Done: {len(posts)} posts")

Three details worth copying even if you rewrite the rest:

Pacing the loop: rate limits per plan

Each plan has its own request rate limit, and a pagination loop is exactly the kind of code that slams into it. Set your sleep interval from your tier:

Plan Rate limit Safe pause between calls
Basic ($0)1 req/sec≥ 1.1 s
Pro ($59/mo)20 req/min≥ 3.1 s
Ultra ($119/mo)40 req/min≥ 1.6 s
Mega ($249/mo)120 req/min≥ 0.6 s

If you get a 429 anyway, back off and retry — don't tighten the loop. (Latency on this API is ~4.5s per call, so a crawl is a batch job by nature; schedule it, don't make a user wait on it.)

Paginating comments with after

The GraphQL-backed endpoints (user_reels, user_tag_media, media_comments) use Instagram's cursor convention instead: a page_info object containing end_cursor and has_next_page. Because the raw JSON nests that object at different depths per endpoint, the robust move is a small recursive finder instead of a hardcoded path:

def find_key(obj, key):
    """Depth-first search for the first occurrence of `key` in nested JSON."""
    if isinstance(obj, dict):
        if key in obj:
            return obj[key]
        for value in obj.values():
            found = find_key(value, key)
            if found is not None:
                return found
    elif isinstance(obj, list):
        for value in obj:
            found = find_key(value, key)
            if found is not None:
                return found
    return None


def fetch_all_comments(shortcode, max_pages=20, pause=3.1):
    """Fetch comment pages for a post/Reel until exhausted (or max_pages)."""
    pages, cursor = [], None

    for _ in range(max_pages):
        params = {"code": shortcode}
        if cursor:
            params["after"] = cursor

        resp = requests.get(f"{BASE}/media_comments", headers=HEADERS,
                            params=params, timeout=30)
        resp.raise_for_status()
        data = resp.json()
        pages.append(data)

        page_info = find_key(data, "page_info") or {}
        if not page_info.get("has_next_page"):
            break
        cursor = page_info.get("end_cursor")
        if not cursor:
            break

        time.sleep(pause)

    return pages

The same loop works for user_reels and user_tag_media — swap the endpoint and the required parameter (user_id instead of code). For reels_audio, the cursor parameter is max_id and the response's paging cursor lives in its paging info; print one response and adapt the two cursor lines.

Where do the actual comments live in each page? Inspect one page with print(json.dumps(page, indent=2)[:2000]) and pull what you need — the structure is Instagram's own, which is exactly the point: nothing is stripped away. For a gentler introduction to this endpoint, see the Instagram comments API tutorial.

What does a full crawl cost?

At ~12 posts per page, a 1,000-post account is about 84 requests. In request fees:

That per-record economics is the whole pitch of this API — the 2026 comparison shows the same crawl costs ~15× more per record on Bright Data. Two cost-control habits to keep it that way:

Common pitfalls

Conclusion

Pagination on the Instagram Cheapest API comes down to one rule: omit the cursor on the first call, then echo back the cursor the response gives younext_max_id for posts, after (from page_info.end_cursor) for Reels, tagged media, and comments, and max_id for Reels audio. With a paced loop and the fields parameter, crawling entire public accounts costs pennies, not dollars.

The machine-readable version of everything above — every endpoint, parameter, and cursor — is in the OpenAPI spec, with a ready-to-import Postman collection to match.

Get your free API key on RapidAPI →

Compliance note: this API returns public Instagram data only. You are responsible for complying with Instagram's terms and applicable privacy law (GDPR/CCPA). Not affiliated with or endorsed by Meta/Instagram.

Start Building Today

Get 30 free requests per month on the Basic plan. No commitment required.

Get Started on RapidAPI