Pagination
Read pagination.next_cursor, send it back as cursor, stop when has_more is false. The same loop works on every list endpoint.
List endpoints return one page at a time. Walking a list is the same on every platform: take the cursor from the response, send it back, and stop when the response says there is nothing left.
The rule
Send pagination.next_cursor back as the cursor query parameter. Stop when pagination.has_more is false.
You never build a cursor or decode one. It is an opaque string that starts is2.. Pass it back exactly as you received it.
The pagination block
A list response carries a pagination object next to data:
{
"success": true,
"platform": "tiktok",
"endpoint": "/v1/tiktok/profile/videos",
"data": {
"items": [ /* this page's videos */ ]
},
"pagination": {
"next_cursor": "is2.eyJ2IjoyLCJjIjoiNzM4NTk…",
"has_more": true,
"page_size": 30
},
"credits_used": 20,
"credits_remaining": 9480,
"request_id": "req_1a2b3c4d5e6f",
"cached": false,
"idempotent_replay": false,
"charge_reason": "miss",
"free_call": false
}| Field | Meaning |
|---|---|
next_cursor | Token for the next page. Send it back as cursor. null on the last page. |
has_more | true while there are more pages. This is your stop signal. |
page_size | How many items this page returned. It reports; it does not control. |
stopped_at | Only when you sent since or stop_at_id. See Incremental sync. |
Do not use an empty page or a total count to decide you are done. has_more is the only reliable signal.
A round trip
Page 1, no cursor:
curl "https://api.insightsocial.app/v1/tiktok/profile/videos?handle=khaby.lame" \
-H "x-api-key: $INSIGHTSOCIAL_API_KEY"The response ends with "pagination": { "next_cursor": "is2.eyJ2Ijoy…", "has_more": true, "page_size": 30 }. Page 2 sends that value back:
curl "https://api.insightsocial.app/v1/tiktok/profile/videos?handle=khaby.lame&cursor=is2.eyJ2Ijoy…" \
-H "x-api-key: $INSIGHTSOCIAL_API_KEY"Repeat until has_more is false. URL-encode the cursor if your HTTP client does not do it for you.
The loop
#!/usr/bin/env bash
# Walk every page of a list and print each item as one JSON line. Needs jq.
url="https://api.insightsocial.app/v1/tiktok/profile/videos"
cursor=""
while :; do
if [ -n "$cursor" ]; then
page=$(curl -sG "$url" --data-urlencode "handle=khaby.lame" \
--data-urlencode "cursor=$cursor" -H "x-api-key: $INSIGHTSOCIAL_API_KEY")
else
page=$(curl -sG "$url" --data-urlencode "handle=khaby.lame" \
-H "x-api-key: $INSIGHTSOCIAL_API_KEY")
fi
echo "$page" | jq -c '.data.items[]'
[ "$(echo "$page" | jq -r '.pagination.has_more')" = "true" ] || break
cursor=$(echo "$page" | jq -r '.pagination.next_cursor')
doneTwo things matter:
- Send the cursor back byte for byte, under the name
cursor. Do not trim, decode or re-encode it. - Stop on
has_more: false, which is also whennext_cursorisnull.
Draining a whole list
A full walk is a series of normal calls, so plan for three things.
- Cost. Every page is billed as one call to that endpoint. Fifty pages of a 20-credit endpoint cost about 1,000 credits. Multiply the page count by the price in Endpoint pricing before you start, and cap the number of pages if you only need the most recent items. Re-walking the same pages inside the owned window is free (see Caching).
- Limits. Pages of one list are sequential, since each needs the previous cursor. You can walk several lists in parallel, but one key allows 10 calls in flight and 60 calls per minute. Past that you get
429(CONCURRENCY_LIMITorRATE_LIMITED) with aRetry-Afterheader, and nothing is charged. - Retries. On a
429or a503, wait and retry the same cursor. Never move past a page you did not read successfully.
import time
def get_page(path, query):
for attempt in range(5):
res = requests.get(f"{BASE}/{path}", params=query, headers=HEADERS)
if res.status_code in (429, 503):
time.sleep(float(res.headers.get("Retry-After", 2 ** attempt)))
continue # same query, same cursor
return res.json()
raise RuntimeError("gave up after 5 attempts")Several pages in one call: max_pages
Some endpoints take max_pages (1 to 5, default 1). The call walks up to that many pages itself and returns the rows from all of them. data.walk.stopped tells you why it stopped (end, max_pages, time_budget or page_error), and pagination.next_cursor continues from where it stopped.
Each page walked is billed as one call. The walk itself is still one request, so it takes a single slot of your rate limit however many pages it covers. The ceiling we hold before the call is the endpoint's listed maximum times max_pages, and the call never costs more than that. See Credits.
curl "https://api.insightsocial.app/v1/tiktok/search/top?query=basketball&max_pages=3" \
-H "x-api-key: $INSIGHTSOCIAL_API_KEY"This holds up to 60 credits (20 × 3) and charges for the pages actually walked.
Incremental sync
A daily poll only needs what was posted since the last run. Eight creator feeds take two parameters that end the walk where your previous poll stopped, so you pay only for the pages that have new posts.
| Platform | Endpoint |
|---|---|
GET /v1/facebook/profile/posts | |
GET /v1/instagram/profile/posts, GET /v1/instagram/profile/reels | |
| Threads | GET /v1/threads/user/posts |
| TikTok | GET /v1/tiktok/profile/videos |
| Twitter/X | GET /v1/twitter/user/tweets |
| YouTube | GET /v1/youtube/channel/videos, GET /v1/youtube/channel/shorts |
| Parameter | Value | Effect |
|---|---|---|
stop_at_id | post.id or post.url of the newest post you already have | The page ends just before that post. It and everything older are left off, no next_cursor is returned, and stopped_at is known_id. If the post is not on this page, the full page comes back with its cursor, so keep walking. |
since | YYYY-MM-DD (midnight UTC) or an ISO 8601 timestamp | Older posts are left off. The page that reaches one ends the walk, with stopped_at set to since. |
curl "https://api.insightsocial.app/v1/instagram/profile/posts?handle=nasa&since=2026-09-01" \
-H "x-api-key: $INSIGHTSOCIAL_API_KEY""pagination": {
"next_cursor": null,
"has_more": false,
"page_size": 6,
"stopped_at": "since"
}stopped_at | Meaning |
|---|---|
known_id | The walk reached the post named in stop_at_id. |
since | The walk reached a post older than since. |
end | The list ran out before either boundary. |
null | No boundary on this page yet. Send next_cursor back and keep going. |
Send the same stop_at_id or since on every page of the walk, and store the newest post you received for the next run.
Pinned posts
Pinned posts sit out of date order. They never end the walk and are never taken as the stop point, so do not store a pinned post as your newest.
These parameters do not change what a page costs. What they save is the pages you never fetch: if the post you already have shows up on the first page, the walk ends there and that first page is the whole bill.
Native paging parameters
Not every endpoint pages with cursor. Some use the platform's own parameter instead, such as page, continuationToken, max_cursor, next_page_id, pagination_token, next_max_id or max_id. The endpoints in the table below take cursor, and the table also names a native parameter where one is documented. For any endpoint, use the paging parameter its reference page lists, and stick to one style for the whole walk.
Bad or stale cursors
A cursor that is expired, edited, or taken from a different endpoint is rejected with an error. Errors are never charged. Start the walk again without a cursor.
limit is not always a page size
What limit does depends on the endpoint. Read its description on the endpoint page before you rely on it.
- Page size or row cap. It trims one page. On
GET /v1/linkedin/search/people,limittakes the top rows of a page and the cursor still moves past the whole page, so the skipped rows do not reappear on the next one. - Collect until N. The endpoint walks pages for you until it has
limititems, and bills per page it used. This is howGET /v1/instagram/profile/posts/full,GET /v1/instagram/profile/reels/fullandGET /v1/facebook/profile/reels/full(up to 50) andGET /v1/threads/search(up to 100) behave.
pagination.page_size in the response is a report of what arrived, never a control.
Every paginated endpoint
These 120 endpoints return the pagination block and take cursor. Each page is one call at the price shown; a range means the endpoint is metered. "Native param" is the platform's own paging parameter where the endpoint also documents one.
| Endpoint | Credits per call | Native param | Also takes |
|---|---|---|---|
/instagram/profile/posts | 20–340 | next_max_id | since, stop_at_id |
/instagram/post/comments | 100–380 | — | — |
/instagram/profile/reels | 20–360 | max_id | since, stop_at_id |
/instagram/search/reels | 20–180 | page | max_pages |
/instagram/audio/reels | 20 | — | — |
/instagram/search/hashtag | 100 | — | max_pages |
/instagram/search/profiles | 20–500 | — | — |
/instagram/followers | 100–200 | — | — |
/instagram/following | 100–200 | — | — |
/instagram/tagged | 100 | — | — |
/instagram/location/posts | 100 | — | — |
/instagram/search/popular | 20–260 | — | — |
/instagram/post/comment/replies | 20–100 | — | — |
/instagram/search/music | 100 | — | — |
/instagram/profile/full | 100 | — | — |
/instagram/profile/reels/full | 100–500 | — | — |
/instagram/profile/posts/full | 100–500 | — | — |
TikTok
| Endpoint | Credits per call | Native param | Also takes |
|---|---|---|---|
/tiktok/profile/videos | 20–100 | max_cursor | since, stop_at_id |
/tiktok/post/comments | 20–140 | — | — |
/tiktok/video/comment/replies | 20–100 | — | — |
/tiktok/search | 20–840 | — | max_pages |
/tiktok/search/hashtag | 20–100 | — | max_pages |
/tiktok/search/top | 20 | — | max_pages |
/tiktok/search/users | 20–2,000 | — | — |
/tiktok/user/followers | 20 | — | — |
/tiktok/user/following | 20 | — | — |
/tiktok/song/videos | 20 | — | — |
/tiktok/adlibrary/search | 100–340 | — | — |
/tiktok/collection/videos | 20 | max_cursor | — |
/tiktok/playlist/videos | 20 | — | — |
/tiktok/user/liked | 20 | — | — |
/tiktok/location/posts | 20 | — | — |
/tiktok/effect/videos | 20 | — | — |
/tiktok/search/music | 20 | — | — |
/tiktok/profile/full | 100 | — | — |
| Endpoint | Credits per call | Native param | Also takes |
|---|---|---|---|
/facebook/profile/posts | 20–80 | — | since, stop_at_id |
/facebook/post/comments | 20–100 | — | — |
/facebook/group/posts | 20 | — | — |
/facebook/profile/photos | 20–180 | next_page_id | — |
/facebook/profile/reels | 20 | next_page_id | — |
/facebook/adlibrary/company/ads | 100 | — | — |
/facebook/adlibrary/search/ads | 100 | — | — |
/facebook/profile/events | 20–180 | — | — |
/facebook/post/comment/replies | 20–100 | — | — |
/facebook/marketplace/search | 20 | — | — |
/facebook/events/search | 20 | — | — |
/facebook/events | 20–260 | — | — |
/facebook/profile/full | 100 | — | — |
/facebook/profile/reels/full | 100–500 | — | — |
/facebook/search/posts | 20–180 | — | max_pages |
/facebook/search/pages | 20 | — | — |
/facebook/search/people | 20 | — | — |
/facebook/search/videos | 20 | — | — |
/facebook/search/groups | 20–300 | page | — |
Twitter/X
| Endpoint | Credits per call | Native param | Also takes |
|---|---|---|---|
/twitter/user/tweets | 20–100 | — | since, stop_at_id |
/twitter/search/tweets | 20–180 | — | max_pages |
/twitter/tweet/replies | 20–100 | — | — |
/twitter/user/media | 20 | — | — |
/twitter/user/followers | 20 | — | — |
/twitter/user/following | 20 | — | — |
/twitter/tweet/retweeters | 20 | — | — |
/twitter/search/users | 20 | — | — |
/twitter/profile/full | 100 | — | — |
Threads
| Endpoint | Credits per call | Native param | Also takes |
|---|---|---|---|
/threads/search | 20–680 | — | max_pages |
YouTube
| Endpoint | Credits per call | Native param | Also takes |
|---|---|---|---|
/youtube/channel/videos | 20–40 | continuationToken | since, stop_at_id |
/youtube/video/comments | 20–100 | continuationToken | — |
/youtube/video/comment/replies | 20–100 | continuationToken | — |
/youtube/search | 20–380 | continuationToken | max_pages |
/youtube/channel/shorts | 20–40 | continuationToken | since, stop_at_id |
/youtube/playlist | 20–220 | — | — |
/youtube/search/hashtag | 20–220 | continuationToken | max_pages |
/youtube/channel/playlists | 20 | continuationToken | — |
/youtube/channel/lives | 20–140 | continuationToken | — |
/youtube/channel/community-posts | 20 | continuationToken | — |
/youtube/videos/trending | 20–120 | — | — |
/youtube/playlist/items | 20–220 | — | — |
/youtube/search/advanced | 20–220 | — | max_pages |
/youtube/profile/full | 100 | — | — |
| Endpoint | Credits per call | Native param | Also takes |
|---|---|---|---|
/reddit/subreddit | 20–100 | after | — |
/reddit/search | 20–680 | after | max_pages |
/reddit/post/comments | 100–180 | — | — |
/reddit/subreddit/search | 20–520 | — | — |
/reddit/profile/posts | 20 | — | — |
/reddit/search/comments | 20 | — | — |
/reddit/subreddits/search | 20–520 | — | — |
/reddit/search/media | 20 | — | — |
/reddit/omni-search | 100–180 | — | — |
| Endpoint | Credits per call | Native param | Also takes |
|---|---|---|---|
/pinterest/search | 20–520 | — | — |
/pinterest/board | 20–320 | — | — |