Pagination
One opaque cursor, one envelope, one page per call.
Instagram paginates almost every list differently: an integer offset here, a
base64 blob there, a compound key-plus-page somewhere else. herascrape hides
most of it behind a single cursor.
The envelope
Most list endpoints return the same three fields:
{
"items": [],
"next_cursor": "eyJtYXhfaWQiOiIyNSJ9",
"has_more": true
}| Field | Meaning |
|---|---|
items | This page's objects, exactly as Instagram returned them. |
next_cursor | Pass back as ?cursor= for the next page. null on the last page. |
has_more | false means you have reached the end. Stop. |
The loop
cursor = None
while True:
params = {"cursor": cursor} if cursor else {}
page = http.get(f"/users/{user_id}/followers", params=params).json()
handle(page["items"])
if not page["has_more"]:
break
cursor = page["next_cursor"]Branch on has_more, not on whether items is empty. Instagram occasionally
returns a short or empty page mid-sequence and still has more to give.
The cursor is opaque
Pass it back verbatim. Never parse, build or edit one, and do not reuse a
cursor from one endpoint on another — both return
400 invalid_cursor.
Search cursors have one extra continuity rule. For /search/users,
/search/hashtags, /search/audio, and /search/top, repeat the exact
query, search_session_id, and serp_session_id from page one; audio also
repeats browse_session_id. The response's search_context echoes the
normalized values. A changed query or ID, an endpoint swap, or a cursor issued
before this contract returns 400 invalid_cursor; start a new search without a
cursor instead.
One page per call
One request fetches one Instagram page. There is no limit parameter and no
server-side aggregation, so N pages is always N calls. Page size is
Instagram's choice, typically 12 to 50 items.
Instagram caps how deep some lists can be paged, most visibly on followers and
following for large or business accounts: has_more goes false while the
account clearly has more. That ceiling is Instagram's.
The endpoints that don't use the envelope
A few endpoints hand back Instagram's own response rather than the envelope, because reshaping them would mean discarding structure the payload carries. They paginate on a token named in their own response, and their reference page says which one.
Two rules cover it. Take the paging token from the endpoint's own What comes
back rather than assuming next_cursor, and never feed back a field just
because its name looks like a cursor — a token the endpoint does not page on is
ignored rather than rejected, so the same page returns forever while
more_available stays true. Where that trap exists we now answer with a
400 naming the right parameter instead of looping.
Lists that never paginate
Some endpoints return everything at once and always report has_more: false
with a null cursor — active stories, a highlights tray, a single reel's
media, comment likers, and place search. The same loop works; it exits after
one pass.