InsightSocial API

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 v2c.. Pass it back exactly as you received it, under the name cursor, with the same parameters as the first page, within 24 hours.

The pagination block

A list response carries a pagination object next to data:

Response
{
  "success": true,
  "platform": "tiktok",
  "endpoint": "/v1/tiktok/profile/videos",
  "schema_version": "2",
  "data": {
    "items": [ /* this page's videos, each { "post": { ... } } */ ],
    "total": null,
    "truncated": null
  },
  "pagination": {
    "next_cursor": "v2c.pvp_XLAfnPv6WqqWVPeenDzzgzIG5pSI…",
    "has_more": true,
    "page_size": 10
  },
  "unavailable": [],
  "credits_used": 10,
  "credits_remaining": 9490,
  "request_id": "req_1a2b3c4d5e6f",
  "cached": false,
  "idempotent_replay": false,
  "charge_reason": "miss",
  "free_call": false
}
FieldMeaning
next_cursorToken for the next page. Send it back as cursor. null on the last page.
has_moretrue while there are more pages. This is your stop signal.
page_sizeHow many items this page returned. It reports; it does not control.
stopped_atOnly 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. data.total, when present, is the platform's own total (all comments on a post, for example), not the number of pages left.

The cursor is only in pagination.next_cursor. There is no copy inside data.

A round trip

Page 1, no cursor:

cURL
curl "https://api.insightsocial.app/v1/tiktok/profile/videos?handle=khaby.lame" \
  -H "x-api-key: $INSIGHTSOCIAL_API_KEY"

The response carries "pagination": { "next_cursor": "v2c.pvp_XLAf…", "has_more": true, "page_size": 10 }. Page 2 sends that value back, with the same handle:

cURL
curl "https://api.insightsocial.app/v1/tiktok/profile/videos?handle=khaby.lame&cursor=v2c.pvp_XLAf…" \
  -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.

A cursor is bound to the request that produced it: the same endpoint and the same parameters. Only limit, max_pages, dry_run and fresh may change between pages. Change anything else, such as the handle or a filter, and the cursor is refused with CURSOR_INVALID; start a new walk instead.

The loop

cURL
#!/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')
done

Two things matter:

  1. Send the cursor back byte for byte, under the name cursor, with the same parameters. Do not trim, decode or re-encode it.
  2. Stop on has_more: false, which is also when next_cursor is null.
  3. Finish a walk within 24 hours. An older cursor is refused with CURSOR_EXPIRED; start again from page 1.

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 10-credit endpoint cost about 500 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 is charged again, so keep the pages you already have.
  • 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_LIMIT or RATE_LIMITED) with a Retry-After header, and nothing is charged.
  • Retries. On a 429 or a 503, wait and retry the same cursor. Never move past a page you did not read successfully.
Python
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 in data.items. pagination.next_cursor continues from where it stopped, and has_more says whether there is more. The walk can stop before max_pages when the list runs out.

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. What we hold before the call is the most one page can cost with your parameters, times max_pages, and the call never costs more than that. See Credits.

cURL
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 30 credits (10 × 3) and charges for the pages actually walked. To see the hold before you run it, add dry_run=1: the response carries data.dry_run with credits_min and credits_max, and costs nothing.

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.

PlatformEndpoint
FacebookGET /v1/facebook/profile/posts
InstagramGET /v1/instagram/profile/posts, GET /v1/instagram/profile/reels
ThreadsGET /v1/threads/user/posts
TikTokGET /v1/tiktok/profile/videos
Twitter/XGET /v1/twitter/user/tweets
YouTubeGET /v1/youtube/channel/videos, GET /v1/youtube/channel/shorts
ParameterValueEffect
stop_at_idpost.id or post.url of the newest post you already haveThe 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.
sinceYYYY-MM-DD (midnight UTC) or an ISO 8601 timestampOlder posts are left off. The page that reaches one ends the walk, with stopped_at set to since.
cURL
curl "https://api.insightsocial.app/v1/instagram/profile/posts?handle=nasa&since=2026-09-01" \
  -H "x-api-key: $INSIGHTSOCIAL_API_KEY"
Response
"pagination": {
  "next_cursor": null,
  "has_more": false,
  "page_size": 6,
  "stopped_at": "since"
}
stopped_atMeaning
known_idThe walk reached the post named in stop_at_id.
sinceThe walk reached a post older than since.
endThe list ran out before either boundary.
nullNo 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.

Date filters on other lists

Over a hundred more lists of posts and comments take since and until as filters, in the same formats (until includes its whole UTC day), and the eight feeds above take until too. Each endpoint's reference shows whether it has them. On ten of them since also ends the walk as above and sorts the list newest first: comments on Instagram, LinkedIn, TikTok and YouTube posts, LinkedIn company posts, X search, and Reddit search, subreddits and user posts. Elsewhere the list is not in date order, so rows outside the window are left off each page but the walk does not stop: keep following next_cursor. A page is charged even when every row on it was filtered out. A row with no date is kept.

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

Every endpoint pages with cursor and nothing else, with one exception. /v1/youtube/video/comment/replies requires continuationToken, and its value is not a page token: it is the id of a top-level comment from /v1/youtube/video/comments, which names the thread to open. Page through that thread's replies with cursor like any other list.

A response never hands you a platform page token: the only cursor it carries is pagination.next_cursor. Sending next_cursor under any other name, such as continuationToken, or a cursor from the previous schema (is. or is2.), is refused with CURSOR_INVALID instead of quietly returning page 1 again.

A few search endpoints also take page, a page number. It picks the page a walk starts on; it is not a token. Walk on from there with cursor.

Bad or stale cursors

A bad cursor is refused before anything is fetched, with error.param naming the parameter that carried it. Errors are never charged.

error.typeCauseWhat to do
CURSOR_INVALIDThe cursor was edited or truncated, was not issued by this API, was sent under a name other than cursor, or belongs to a different endpoint or different parameters.Send pagination.next_cursor back unchanged with the original parameters, or start again without a cursor.
CURSOR_EXPIREDThe cursor is more than 24 hours old.Start again without a cursor.
Response
{
  "success": false,
  "error": {
    "type": "CURSOR_INVALID",
    "message": "This cursor was not issued by this API. Send `pagination.next_cursor` back unchanged. Nothing was charged.",
    "param": "cursor"
  },
  "request_id": "req_a7f5ccf4ce56",
  "credits_used": 0,
  "credits_remaining": null
}

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, limit takes 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 limit items, and bills per page it used. This is how GET /v1/instagram/profile/posts/full, GET /v1/instagram/profile/reels/full and GET /v1/facebook/profile/reels/full (up to 50) and GET /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.

Instagram

TikTok

Facebook

LinkedIn

Twitter/X

Threads

EndpointCredits per callAlso takes
/threads/search20–680max_pages

YouTube

Reddit

Pinterest

EndpointCredits per callAlso takes
/pinterest/search20–520—
/pinterest/board20–320—

See also

On this page