herascrape
API reference

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/stories

There 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.

Path Parameters

username*Username
GET
/users/username/{username}
curl -X GET "https://example.com/users/username/string"
{}
{}

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

user_id*User Id

Numeric Instagram user id (pk)

Match^\d+$
GET
/users/{user_id}
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": []
}
FieldNote
pkThe numeric id. This is what every other endpoint wants.
biographyThe bio as plain text. biography_with_entities.raw_text carries the same string, with any @mentions and #hashtags itemised alongside.
bio_linksThe link-in-bio entries, richer than external_url.
is_privateIf true, sub-resources return 403.
hd_profile_pic_versionsSized 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

user_id*User Id

Numeric Instagram user id (pk)

Match^\d+$

Query Parameters

cursor?|

Opaque pagination cursor from a previous next_cursor

GET
/users/{user_id}/followers
curl -X GET "https://example.com/users/string/followers"
{}
{}

Path Parameters

user_id*User Id

Numeric Instagram user id (pk)

Match^\d+$

Query Parameters

cursor?|

Opaque pagination cursor from a previous next_cursor

GET
/users/{user_id}/following
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

user_id*User Id

Numeric Instagram user id (pk)

Match^\d+$

Query Parameters

cursor?|

Opaque pagination cursor from a previous next_cursor

max_id?|Deprecated

Not supported here. Page with 'cursor', taking the value from feed.profile_grid_items_cursor.

GET
/users/{user_id}/posts
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:

WhereHasDoes not have
posts["<media id>"]code, caption, like_count, comment_count, play_count, complete carousel_media childrenimage_versions2, video_versions, and only a stub user
feed.profile_grid_items[].mediaimage_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 typeTake 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

user_id*User Id

Numeric Instagram user id (pk)

Match^\d+$

Query Parameters

cursor?|

Opaque pagination cursor from a previous next_cursor

sort_by_views?Sort By Views

Sort profile reels by view count

Defaultfalse
GET
/users/{user_id}/reels
curl -X GET "https://example.com/users/string/reels"
{}
{}

Path Parameters

user_id*User Id

Numeric Instagram user id (pk)

Match^\d+$

Query Parameters

cursor?|

Opaque pagination cursor from a previous next_cursor

GET
/users/{user_id}/tagged
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

user_id*User Id

Numeric Instagram user id (pk)

Match^\d+$

Query Parameters

cursor?|

Opaque pagination cursor from a previous next_cursor

GET
/users/{user_id}/reposts
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

user_id*User Id

Numeric Instagram user id (pk)

Match^\d+$
GET
/users/{user_id}/stories
curl -X GET "https://example.com/users/string/stories"
{}
{}

Path Parameters

user_id*User Id

Numeric Instagram user id (pk)

Match^\d+$
GET
/users/{user_id}/highlights
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

user_id*User Id

Numeric Instagram user id (pk)

Match^\d+$
GET
/users/{user_id}/about
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

CodeWhen
profile_not_foundNo such user, or deleted or banned.
private_accountThe profile exists; its content is not visible.
invalid_cursorThe cursor was not one we issued.
too_many_requestsOver 10 requests/second on this key.
upstream_errorTransient. Retry.

On this page