How to Get Instagram User Posts with Python and Paginate the Results
A recurring integration question — "Instagram user posts API Python pagination" — has a simple shape on the Instagram Data API: pass a username, read posts from data.items, and pass the payload's top-level pagination_token into the next request. This tutorial builds that loop with the safety properties a real collection job needs: termination when no usable token comes back, protection against repeated tokens, deduplication by media ID, collection timestamps, and a page budget you control.
The service reads publicly visible content only. It is not Meta's official Instagram API: there is no OAuth flow, no account Insights, and no guarantee of complete historical exports. Like and comment counts are collection-time snapshots.
Endpoints used in this guide
| Operation | Required input | Purpose |
|---|---|---|
user-info | username_or_id | Optional preflight: resolve the profile and its numeric id |
user-posts | username_or_id | Recent post feed with token pagination |
post-detail | code_or_url | Enrich one post by shortcode or URL |
user-reels | username_or_id | Reels-only variant of the feed |
All four are GET requests. The observed page size for user-posts was 12 items in a live check; treat that as an example, not a contract.
Step 1 — Prepare an authorized platform key
Sign in and copy an active key from your dashboard. Python and JavaScript below read it from the OPENDATA_API_KEY environment variable; the curl examples use the YOUR_API_KEY placeholder. Query URLs contain your key, so keep them out of shared links, screenshots and logs, and run production calls server-side.
Two status layers apply to every response: the HTTP status, and the gateway's code/message/data business status. code=200 indicates gateway business success; HTTP 200 alone does not.
Step 2 — Optional preflight: confirm the profile exists
curl --get 'https://api.opendata-api.com/api/project/instagram/user-info?key=YOUR_API_KEY' \
--data-urlencode 'username_or_id=instagram'
A missing username returns HTTP 200 with exactly {"detail":"Not found"} in the payload. That is a business-level miss, not a partial profile — branch on it instead of retrying. On a hit, the payload carries id, username, follower_count, is_verified, is_private and related fields; the numeric id is a stable join key for every other endpoint.
Step 3 — Request the first posts page
Omit pagination_token on the first request:
curl --get 'https://api.opendata-api.com/api/project/instagram/user-posts?key=YOUR_API_KEY' \
--data-urlencode 'username_or_id=instagram'
Read posts from data.items, the author summary from data.user, and the continuation token from the payload's top-level pagination_token.
Step 4 — Paginate in Python with a budget and deduplication
import os
import time
import requests
BASE = "https://api.opendata-api.com/api/project/instagram"
API_KEY = os.environ["OPENDATA_API_KEY"]
def call(operation, **params):
response = requests.get(
f"{BASE}/{operation}",
params={k: v for k, v in {**params, "key": API_KEY}.items() if v},
timeout=30,
)
if not response.ok:
raise RuntimeError(f"HTTP {response.status_code}")
gateway = response.json()
if gateway.get("code") != 200:
raise RuntimeError(f"Gateway business failure: {gateway.get('message')}")
return gateway.get("data")
def iter_user_posts(username_or_id, max_pages=5):
"""Yield unique posts, following opaque pagination tokens safely."""
token = None
seen_tokens = set()
seen_media = set()
for _ in range(max_pages): # user-configurable page budget
payload = call("user-posts", username_or_id=username_or_id,
pagination_token=token or "")
if isinstance(payload, dict) and payload.get("detail"):
break # business-level miss, e.g. detail Not found
for item in (payload or {}).get("items") or []:
media_id = item.get("id")
if media_id and media_id not in seen_media:
seen_media.add(media_id)
item["_collected_at"] = int(time.time())
yield item
token = (payload or {}).get("pagination_token")
if not token or token in seen_tokens:
break # no usable continuation, or the token repeated
seen_tokens.add(token)
posts = list(iter_user_posts("instagram", max_pages=3))
print(f"collected {len(posts)} unique posts")
Three properties matter here. First, tokens are opaque: never parse, fabricate or reuse them across endpoints — just pass back what you received. Second, repeated-token protection stops the loop if the same token comes back unchanged, which would otherwise re-fetch the same page forever. Third, media-ID deduplication protects your dataset when successive collection windows overlap. The _collected_at stamp records when each item was observed, because counts change between runs.
The equivalent JavaScript core:
const BASE = "https://api.opendata-api.com/api/project/instagram";
const key = process.env.OPENDATA_API_KEY;
async function postsPage(usernameOrId, token = "") {
const params = new URLSearchParams({ key, username_or_id: usernameOrId });
if (token) params.set("pagination_token", token);
const res = await fetch(`${BASE}/user-posts?${params}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const gateway = await res.json();
if (gateway.code !== 200) throw new Error(`Business failure: ${gateway.message}`);
return gateway.data; // { count, items, user, pagination_token }
}
Response fields worth keeping
| Field | Meaning |
|---|---|
id | Numeric media ID — your dedupe and join key |
code | Shortcode for URL building and post-detail |
taken_at | Unix seconds timestamp of the post |
caption.text | Caption with extracted hashtags and mentions |
media_type | 1 = photo, 2 = video, 8 = carousel |
like_count, comment_count, play_count | Collection-time engagement snapshots |
image_versions2, video_versions, carousel_media | Media renditions; URLs are time-limited |
data.user | Author summary confirming ownership |
Step 5 — Enrich one post from its shortcode
Feed a returned code into post-detail to add media URLs, location tags and Reels audio metadata:
curl --get 'https://api.opendata-api.com/api/project/instagram/post-detail?key=YOUR_API_KEY' \
--data-urlencode 'code_or_url=DeM-xDCA2W5'
A full instagram.com/p/... URL works too. In a live check this shortcode resolved to a media record owned by the same account; verify data.user before trusting attribution in a mixed pipeline. Keep the numeric id — it joins cleanly to comments and likes from the engagement endpoints.
Use cases
- Creator discovery — enrich shortlisted handles with recent posts and collection-time engagement samples.
- Competitor monitoring — watch brand feeds in scheduled windows, deduplicating by media ID.
- Content research — sample Reels formats and caption hashtags across a niche.
- Pipeline seeding — resolve fresh shortcodes for detail, comment and audio follow-ups.
Collection-time likes and comments are not official account Insights, and private-account content, complete historical exports and real-time guarantees are out of scope.
FAQ
How do I get Instagram posts by username using Python and an API?
Pass the handle as username_or_id to user-posts and read data.items; the full loop is in Step 4.
How do I paginate Instagram posts without repeatedly collecting the same items?
Pass each returned pagination_token into the next request, stop when it is absent or repeats, and deduplicate items by media id.
How can I resolve an Instagram username to a user ID?
user-info returns the numeric id for a handle; user-search returns ranked candidates with IDs for ambiguous names.
What is the difference between an Instagram follower count and a follower list?
follower_count on the profile is one number; user-followers pages through the accessible audience records, which are a sample rather than a complete export.
How do I get Instagram post details from a URL or shortcode?
Pass the /p/, /reel/ or /tv/ segment — or the full URL — as code_or_url to post-detail.
How do I collect Instagram comments and their reply threads?
post-comments lists comments with each comment's pk; pass that value as comment_id together with the same code_or_url to post-comment-replies.
Can I search Instagram hashtag posts by recent, top, or Reels feed?
Yes — hashtag-posts takes a bare keyword plus feed_type of top, recent or reels; recent and reels paginate with pagination_token, top with next_page.
How do I find Instagram location IDs and retrieve posts tagged at a place?
Resolve a name with search-location or coordinates with search-coordinates, then pass a returned location_id to posts-location.
How can I find Instagram Reels using a particular audio track?
Resolve the sound via search-music-audio or a Reel's clips_metadata.audio_canonical_id, then list Reels with posts-music.
What should my application do when an API returns HTTP 200 with a not-found message?
Treat detail: Not found as a business-level miss: record it, skip enrichment and move on rather than retrying or fabricating a profile.
What data is available from private accounts, and what remains restricted?
Profile-level flags such as is_private can be visible, but a private account's posts, Stories, followers and other content are not returned.
How does an Instagram public-data service differ from Meta's official Instagram API?
The official API is OAuth-scoped and oriented to managed accounts and Insights; this service reads publicly visible surfaces over simple GETs, with snapshot metrics instead of official analytics.
Try the flow on the Instagram Data API page, or read the user-posts, user-info, post-detail and user-reels references. APIs for similar apps such as TikTok, YouTube, Facebook, Threads, X, Pinterest, Snapchat, Reddit, Xiaohongshu / RED and Douyin can be added on request via the contact page, and private source-code deployment is available. See pricing for per-request costs.