How to Get Instagram User Posts with Python and Paginate the Results

Published October 7, 2026

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

OperationRequired inputPurpose
user-infousername_or_idOptional preflight: resolve the profile and its numeric id
user-postsusername_or_idRecent post feed with token pagination
post-detailcode_or_urlEnrich one post by shortcode or URL
user-reelsusername_or_idReels-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

FieldMeaning
idNumeric media ID — your dedupe and join key
codeShortcode for URL building and post-detail
taken_atUnix seconds timestamp of the post
caption.textCaption with extracted hashtags and mentions
media_type1 = photo, 2 = video, 8 = carousel
like_count, comment_count, play_countCollection-time engagement snapshots
image_versions2, video_versions, carousel_mediaMedia renditions; URLs are time-limited
data.userAuthor 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.