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"const response = await fetch(
"https://www.shortsintel.com/v1/creators/tiktok/someone/videos?limit=100",
{
method: "GET",
headers: {
"Authorization": "Bearer sk_live_YOUR_KEY",
},
},
);
if (!response.ok) {
throw new Error(`ShortsIntel ${response.status}`);
}
const data = await response.json();import requests
response = requests.get(
"https://www.shortsintel.com/v1/creators/tiktok/someone/videos?limit=100",
headers={
"Authorization": "Bearer sk_live_YOUR_KEY",
},
)
response.raise_for_status()
data = response.json()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
returnKeyset 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"const response = await fetch(
"https://www.shortsintel.com/v1/runs/run_8f2c1ad04b/items?limit=25&type=videos",
{
method: "GET",
headers: {
"Authorization": "Bearer sk_live_YOUR_KEY",
},
},
);
if (!response.ok) {
throw new Error(`ShortsIntel ${response.status}`);
}
const data = await response.json();import requests
response = requests.get(
"https://www.shortsintel.com/v1/runs/run_8f2c1ad04b/items?limit=25&type=videos",
headers={
"Authorization": "Bearer sk_live_YOUR_KEY",
},
)
response.raise_for_status()
data = response.json()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.