Users
Profiles, followers, posts, reels, stories, highlights and reposts.
Users are addressed by numeric id on the base path. The one exception is
the username lookup, which resolves a handle and returns the profile — take
pk from it and use that everywhere else.
# Once, to resolve the handle:
GET /users/username/instagram # -> { "pk": "25025320", ... }
# From then on:
GET /users/25025320/followers
GET /users/25025320/storiesThere are no username-addressed sub-resources. Resolving costs double (two Instagram requests), so caching the id is worth doing — see Pricing.
Instagram caps pagination depth on followers and following, most visibly for
large and business accounts: has_more goes false well before the list is
exhausted. This is the platform's limit and no provider can page past it.
Profile by username
Resolves the handle via typeahead, then fetches the full profile. Returns
404 profile_not_found when the
typeahead hit does not match the requested handle exactly, so a near-miss is a
clean miss rather than the wrong account.
What comes back
The same profile object as by id below — this endpoint only differs in how it finds the user.
Profile by id
Path Parameters
Numeric Instagram user id (pk)
^\d+$curl -X GET "https://example.com/users/string"{}{}What comes back
A profile is Instagram's full user object — roughly 250 top-level fields. Abridged, with the ones most callers use:
{
"pk": "25025320",
"username": "instagram",
"full_name": "Instagram",
"biography": "Discover what's new on Instagram 🔎✨",
"biography_with_entities": { "raw_text": "Discover what's new on Instagram 🔎✨", "entities": [] },
"external_url": "https://about.instagram.com/",
"bio_links": [{ "url": "https://about.instagram.com/", "title": "" }],
"follower_count": 692000000,
"following_count": 176,
"media_count": 7712,
"is_private": false,
"is_verified": true,
"is_business": true,
"category": "Internet company",
"public_email": "",
"profile_pic_url": "https://scontent.cdninstagram.com/...",
"hd_profile_pic_versions": [{ "width": 1080, "height": 1080, "url": "..." }],
"has_highlight_reels": true,
"pronouns": []
}| Field | Note |
|---|---|
pk | The numeric id. This is what every other endpoint wants. |
biography | The bio as plain text. biography_with_entities.raw_text carries the same string, with any @mentions and #hashtags itemised alongside. |
bio_links | The link-in-bio entries, richer than external_url. |
is_private | If true, sub-resources return 403. |
hd_profile_pic_versions | Sized variants; pick the largest. URLs are signed and expire. |
The rest are Instagram's own flags — shopping eligibility, Threads state, moderation settings. They pass through untouched, so anything Instagram adds appears without us shipping a release.
Followers and following
Paginated on the standard cursor. Items are
Instagram's compact user objects, not full profiles — fetch
/users/{id} for the rest.
Path Parameters
Numeric Instagram user id (pk)
^\d+$Query Parameters
Opaque pagination cursor from a previous next_cursor
curl -X GET "https://example.com/users/string/followers"{}{}Path Parameters
Numeric Instagram user id (pk)
^\d+$Query Parameters
Opaque pagination cursor from a previous next_cursor
curl -X GET "https://example.com/users/string/following"{}{}What comes back
{
"items": [
{
"pk": "51065942432",
"username": "example",
"full_name": "Example Account",
"is_private": false,
"is_verified": false,
"profile_pic_url": "https://scontent.cdninstagram.com/...",
"latest_reel_media": 0,
"account_badges": []
}
],
"next_cursor": "eyJtYXhfaWQiOiIxMDAifQ",
"has_more": true
}Compact user objects — about 15 fields, not the ~250 of a full profile. There
is no follower/following count here; fetch /users/{pk} for that.
Posts
The profile grid. Instagram serves this as a streamed GraphQL response whose shape differs from the other list endpoints, and we pass it through as-is:
{
"feed": {
"more_available": true,
"next_max_id": "...",
"profile_grid_items": [{ "media": {} }],
"pinned_profile_grid_items_ids": ["<media id>"]
},
"posts": { "<media id>": {} }
}feed.profile_grid_items is the ordered list — that array is the ordering key,
and pins are called out in feed.pinned_profile_grid_items_ids. posts is a
lookup keyed by media id.
Page with cursor, taking the value from feed.profile_grid_items_cursor.
Keep going while feed.more_available is true:
GET /users/13460080/posts
GET /users/13460080/posts?cursor=<feed.profile_grid_items_cursor>
GET /users/13460080/posts?cursor=<next feed.profile_grid_items_cursor>feed.next_max_id is present in the response but is not a paging token for
this endpoint — Instagram's profile-timeline query keys on the cursor and
ignores max_id, so feeding next_max_id back returns the first page again.
Passing max_id here returns 400 invalid_parameter rather than looping.
Path Parameters
Numeric Instagram user id (pk)
^\d+$Query Parameters
Opaque pagination cursor from a previous next_cursor
Not supported here. Page with 'cursor', taking the value from feed.profile_grid_items_cursor.
curl -X GET "https://example.com/users/string/posts"{}{}What comes back
The {feed, posts} shape above — and it is not the same media object that
/posts/{id} returns. This endpoint is the only
one backed by Instagram's GraphQL profile timeline rather than their REST API,
and we pass that through rather than reshaping it, so the difference is real
and worth planning for.
The media arrives split across the two halves, and neither is complete alone:
| Where | Has | Does not have |
|---|---|---|
posts["<media id>"] | code, caption, like_count, comment_count, play_count, complete carousel_media children | image_versions2, video_versions, and only a stub user |
feed.profile_grid_items[].media | image_versions2, video_versions for reels, the full user (full_name, profile_pic_url) | code, and carousel children may be truncated |
Join the two on the media id to get one usable post. Which half holds the media depends on the post type, so take each from the side that actually carries it:
| Post type | Take the media from |
|---|---|
Single image (product_type: "feed") | either half's image_versions2 |
Reel / video (product_type: "clips") | feed.profile_grid_items[].media.video_versions — the posts half has no video renditions at all |
Carousel (product_type: "carousel_container") | posts["<id>"].carousel_media — the grid half sometimes drops the renditions on children after the first, and can report a video child as an image |
Measured on one profile across four pages: every one of the 23 reels carried
video_versions in the grid half and none in the posts half, while all 85
carousel children were complete in the posts half against 81 in the grid.
Nothing is missing from the response — but reading only one half loses video on
reels, or the trailing items of a carousel.
Some fields also arrive with a short type prefix — 1ltaken_at,
1fvideo_duration, 1flat, 1flng, 1ldevice_timestamp, 1lbrs_severity.
That is Instagram's own disambiguation in this GraphQL response, not a
transport artifact, and it is what the mobile client receives. We leave the
keys exactly as they arrive, because renaming them here would mean guessing on
your behalf and re-guessing every time Instagram changes something.
If you want the flat REST media shape for a user's videos,
/users/{user_id}/reels is served by Instagram's REST
clips endpoint and returns taken_at, image_versions2, video_versions and
video_duration unprefixed.
Reels and tagged
Reels is the clips tab; tagged is media the user was tagged in. Both use the standard cursor envelope.
Path Parameters
Numeric Instagram user id (pk)
^\d+$Query Parameters
Opaque pagination cursor from a previous next_cursor
Sort profile reels by view count
falsecurl -X GET "https://example.com/users/string/reels"{}{}Path Parameters
Numeric Instagram user id (pk)
^\d+$Query Parameters
Opaque pagination cursor from a previous next_cursor
curl -X GET "https://example.com/users/string/tagged"{}{}What comes back
The standard list envelope, with media objects in the same shape as
a post. Reels are all
product_type: "clips"; tagged mixes photos, carousels and reels.
Pass sort_by_views=true on the reels endpoint to order a profile's reels by
view count instead of newest-first — the app's "most viewed" sort. It pages
with the same cursor.
Reposts
Content the user has reposted. Standard cursor envelope.
Path Parameters
Numeric Instagram user id (pk)
^\d+$Query Parameters
Opaque pagination cursor from a previous next_cursor
curl -X GET "https://example.com/users/string/reposts"{}{}What comes back
The standard list envelope, with media objects in the same shape as
a post — the reposted originals, each carrying
its own author in user.
Stories and highlights
Both return a single page — has_more is always false and next_cursor is
null. Stories returns the currently-active reel, which is empty for a user
with nothing posted in the last 24 hours. Highlights returns the tray; fetch
one highlight's media through
GET /highlights/{id}.
Both are billed at double, because Instagram needs a second hop to serve them.
Path Parameters
Numeric Instagram user id (pk)
^\d+$curl -X GET "https://example.com/users/string/stories"{}{}Path Parameters
Numeric Instagram user id (pk)
^\d+$curl -X GET "https://example.com/users/string/highlights"{}{}What comes back
Stories returns media items (same shape as a post). Highlights returns tray entries:
{
"items": [
{
"id": "highlight:17913155365000000",
"title": "Behind the scenes",
"media_count": 12,
"created_at": 1699999999,
"is_pinned_highlight": false,
"cover_media": { "cropped_image_version": { "url": "..." } },
"user": { "pk": "25025320", "username": "instagram" }
}
],
"next_cursor": null,
"has_more": false
}Pass id straight to GET /highlights/{id} —
the highlight: prefix is accepted, and so is the bare number.
About this account
Instagram's "About this account" panel — join date, country, former usernames where shown.
Best-effort. Instagram ships this screen as a rendered UI tree rather than
JSON, so we extract the visible text: details holds every line, and
date_joined is pulled from the labelled one. Treat details as the reliable
field and the rest as convenience.
Path Parameters
Numeric Instagram user id (pk)
^\d+$curl -X GET "https://example.com/users/string/about"{}{}What comes back
Instagram does not serve this screen as JSON, so the response is assembled from the rendered panel:
{
"date_joined": "March 2010",
"former_usernames": [],
"former_usernames_count": 0,
"details": ["Date joined", "March 2010", "Account based in", "United States"]
}Errors
| Code | When |
|---|---|
profile_not_found | No such user, or deleted or banned. |
private_account | The profile exists; its content is not visible. |
invalid_cursor | The cursor was not one we issued. |
too_many_requests | Over 10 requests/second on this key. |
upstream_error | Transient. Retry. |