Skip to content

Concepts

Pagination

How lists page with items and a cursor, and how to read every page without paying twice.

Every list in this API pages the same way: an items array and a next_cursor. Pass the cursor back to get the next page; when it comes back null you have everything. There is no page number, no total count and no offset.

The shape

200 · `{ items, next_cursor }`.

{
  "items": [
    {
      "platform": "tiktok",
      "id": "7300000000000000000",
      "url": "https://www.tiktok.com/@someone/video/7300000000000000000",
      "description": "3 minutes at 4°C every morning for 30 days. Here is what actually changed. #coldplunge #recovery",
      "posted_at": "2026-09-05T06:32:11.000Z",
      "duration_seconds": 41,
      "thumbnail_url": "https://p16-sign.tiktokcdn.com/obj/tos-cover-7300000000000000000.jpeg",
      "hashtags": [
        "coldplunge",
        "recovery",
        "morningroutine"
      ],
      "sound": {
        "id": "7180000000000000000",
        "title": "original sound - someone"
      },
      "creator": {
        "id": "cre_9d4b2a7f1c",
        "handle": "someone",
        "name": "Someone",
        "follower_count": 184300
      },
      "snapshot": {
        "at": "2026-09-08T09:14:04.219Z",
        "views": 1284000,
        "likes": 143200,
        "comments": 2810,
        "shares": 9040,
        "saves": 21600
      },
      "scores": {
        "outlier": 4.21,
        "outlier_p90": 1.6,
        "percentile": 95,
        "velocity": {
          "views_per_hour": 6698.6,
          "window": "snapshot"
        },
        "reason": null,
        "baseline": {
          "median_views": 305000,
          "p90_views": 802000,
          "engagement_rate": 0.1342,
          "posts_per_week": 4.1,
          "sample": 20,
          "confidence": "high",
          "source": "videos",
          "computed_at": "2026-09-08T09:14:03.902Z",
          "stale": false
        }
      }
    },
    {
      "platform": "tiktok",
      "id": "7300000000000000042",
      "url": "https://www.tiktok.com/@anotherone/video/7300000000000000042",
      "description": "I tried cold plunging before every workout for a week. #coldplunge",
      "posted_at": "2026-09-06T13:04:52.000Z",
      "duration_seconds": 28,
      "thumbnail_url": "https://p16-sign.tiktokcdn.com/obj/tos-cover-7300000000000000042.jpeg",
      "hashtags": [
        "coldplunge",
        "gym"
      ],
      "sound": {
        "id": "7180000000000000099",
        "title": "Winter Air"
      },
      "creator": {
        "id": "cre_1a77c0b3de",
        "handle": "anotherone",
        "name": "Another One",
        "follower_count": 21400
      },
      "snapshot": {
        "at": "2026-09-08T09:14:04.219Z",
        "views": 96200,
        "likes": 8140,
        "comments": 212,
        "shares": 401,
        "saves": 1180
      },
      "scores": null
    }
  ],
  "next_cursor": "eyJpZCI6ImNsdjRxOHkifQ"
}

next_cursor is a string when there is more and null when there is not. Do not test the length of items to decide whether to stop: a full page can still be the last one, and an empty page can still carry a cursor. The cursor is the only signal.

limit sets the page size — 25 by default, 100 at most, and it is the same 25 and 100 everywhere lists appear. Ask for more than the ceiling and you get an invalid_request problem rather than a silently clamped page.

GET /v1/creators/{platform}/{handle}/videos·Free. Counts toward the per-key request limit.

curl "https://www.shortsintel.com/v1/creators/tiktok/someone/videos?limit=100" \
  -H "Authorization: Bearer sk_live_YOUR_KEY"

A cursor is opaque

Treat it as a token you were handed, never as data. It is not an id, not an offset and not a timestamp, and what it encodes differs by operation: a keyset position for a simple list, several positions at once for a cross-platform search, since one page there is one request per platform. Do not construct one, do not decode one, and do not edit one; a cursor we did not issue is an invalid_request problem.

Cursors do belong to the list they came from. A cursor from GET /v1/runs means nothing to GET /v1/runs/{id}/items. Keep them with the loop that produced them and discard them when the loop ends.

Walking every page

The whole pattern is a loop that stops on null. Take the largest page size the endpoint allows: fewer round trips, and on the paid lists, fewer pages to pay for.

const key = process.env.SHORTSINTEL_API_KEY ?? "sk_live_YOUR_KEY";

async function* pages(url: string) {
  let cursor: string | null = null;
  do {
    const query = new URLSearchParams({ limit: "100" });
    if (cursor) query.set("cursor", cursor);

    const response = await fetch(`${url}?${query}`, {
      headers: { Authorization: `Bearer ${key}` },
    });
    if (!response.ok) throw new Error(`${response.status} from ${url}`);

    const page = (await response.json()) as {
      items: unknown[];
      next_cursor: string | null;
    };
    yield page.items;
    cursor = page.next_cursor;
  } while (cursor !== null); // null means this was the last page
}
import os

import httpx

key = os.environ.get("SHORTSINTEL_API_KEY", "sk_live_YOUR_KEY")


def pages(url: str):
    cursor = None
    while True:
        params = {"limit": 100}
        if cursor:
            params["cursor"] = cursor

        response = httpx.get(url, params=params,
                             headers={"Authorization": f"Bearer {key}"})
        response.raise_for_status()

        page = response.json()
        yield page["items"]

        cursor = page["next_cursor"]
        if cursor is None:  # None means that was the last page
            return

Keyset paging is stable under inserts: rows added while you are walking do not shuffle the pages you have already read past, which an offset would. New rows arriving at the top of a newest-first list simply are not in your walk — start again if you want them.

Re-reading finished work is free

Free: the charge happened once, when the work did. So is GET /v1/runs, and so is paging back through a Run you already paid for: the work happened once, when the Run ran, and reading its items again does not repeat it. Walk a fifty-item Run from the first page to the last, then walk it again tomorrow, and the total charge is unchanged.

GET /v1/runs/{id}/items·Free: the charge happened once, when the work did.

curl "https://www.shortsintel.com/v1/runs/run_8f2c1ad04b/items?limit=25&type=videos" \
  -H "Authorization: Bearer sk_live_YOUR_KEY"

That is the general rule for lists over stored results — a Run, its items, a creator’s videos, a Tracker’s Snapshots and Change Reports, your Runs, your webhook deliveries. Page them as often as you like.

The exception is a list that is itself a fetch. $0.02 per page. Paging a hashtag’s or a sound’s videos asks the platform for its current ordering, which cannot be answered from stored rows without answering a different question, so each page is a Run and each page is charged. The price line on the operation tells you which kind of list you are holding.

See Runs for why re-reads are free, and pricing and estimates for pricing a paid page before you ask for it.