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:
- Stop on
more_available, not on an empty page. That's the API's explicit "you've reached the end" signal. - Treat a missing cursor as the end too. Raw JSON means you code defensively.
- A
max_pagescap protects you from accidentally crawling a 10,000-post account when you only wanted a sample.
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:
- On Pro ($59/mo with 150,000 requests included), 84 requests is 0.06% of your monthly quota — effectively free within the base fee. You could crawl ~1,700 such accounts inside the included quota.
- Counted at the Mega overage rate of $0.10 per 1,000 requests, 84 requests ≈ $0.008 — call it a cent.
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:
- Add the
fieldsparameter once you know which keys you need — every tier includes 10 GB/month of bandwidth, and trimmed responses keep a big crawl well inside it. - Persist cursors if you crawl incrementally (store the newest post ID you've seen; on the next run, stop paginating when you reach it instead of re-crawling the whole account).
Common pitfalls
- Reusing old cursors. Cursors are opaque and tied to a crawl session; if a stored cursor stops working, restart from a blank first page.
- Assuming a fixed page size. ~12 is typical, but treat it as approximate — always loop on the "has more" signal, never on item counts.
- Parsing brittle paths. Raw Instagram JSON can change shape; prefer
dict.get()chains or a recursive finder likefind_keyover six-level hardcoded lookups. - Ignoring the rate limit. Pagination loops are tight loops; the
time.sleep()is not optional on smaller plans.
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 you — next_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.