Home > Blog > How to Get All Videos From a TikTok User via API
User Data

How to Get All Videos From a TikTok User via API

By PrimeApi·August 15, 2026·8 min read·Updated August 15, 2026

Pulling one TikTok video is easy. Pulling every video a creator has ever posted, in order, without gaps or duplicates, is a paging problem - and paging is where most integrations quietly break. This guide covers the whole job with PrimeApi: resolving a handle into the secUid that the feed endpoint requires, reading the fields that come back on each item, running the hasMore and cursor loop until the catalogue is exhausted, and deciding which videos are worth a follow-up call to post-detail. Every field named below was taken from live responses, not from a wish list.

The two-call shape of the job

There is no endpoint that takes a username and hands back a complete video list. The work is always two calls deep:

  1. Call userinfo-by-username once with the public handle. Read user.secUid from the response.
  2. Call user-posts repeatedly with that secUid, walking the cursor forward until the feed says there is nothing left.

The first call also gives you user.id (the permanent numeric ID), user.nickname, user.verified, user.privateAccount and a stats block containing followerCount, followingCount, heartCount, diggCount and videoCount. That last one is worth keeping: stats.videoCount tells you roughly how many uploads to expect, which makes it a useful sanity check against the number of items your loop actually collected. It is a check, not a stop condition - deleted, private and region-blocked posts mean the two numbers rarely match exactly.

If the secUid concept is new to you, the dedicated write-up on what a TikTok secUid is and how to get one explains why TikTok uses an opaque identifier here instead of the numeric ID.

Step 1: handle to secUid

Authentication is one header, X-PrimeAPI-Key, on every request. The base URL is https://api.primeapi.co/.

curl -s "https://api.primeapi.co/userinfo-by-username?username=taylorswift" \
  -H "X-PrimeAPI-Key: YOUR_API_KEY"

The value you want is at user.secUid - a long opaque string beginning MS4wLjABAAAA. Persist it alongside user.id. Handles change; both of these identifiers survive a rename, so a stored secUid keeps a long-running collection job pointing at the same account.

Step 2: the user-posts request

user-posts takes one required parameter and two optional ones:

  • secUid (required) - the value from step 1.
  • count (optional) - requested page size.
  • cursor (optional) - 0 for the first page, then whatever the previous response returned.
curl -s "https://api.primeapi.co/user-posts?secUid=MS4wLjABAAAAqB08cUbXaDWqbD6MCga2RbGTuhfO2EsHayBYx08NDrN7IE3jQuRDNNN6YwyfH6_6&count=35&cursor=0" \
  -H "X-PrimeAPI-Key: YOUR_API_KEY"

One thing to internalise before you write the loop: count is advisory. The call above, asking for 35, came back with 15 entries in data.itemList. Page size moves around depending on the account and how deep into the history you are. Code that assumes itemList.length === count, or that treats a short page as the end of the feed, will truncate catalogues at random.

What the response envelope looks like

user-posts nests everything under a top-level data object. Miss that and your first attempt reads undefined.

PathTypeWhat it is
data.itemListarrayThe videos on this page, newest first
data.cursorstringFeed it to the next request, e.g. "1713553237000"
data.hasMorebooleanWhether another page exists
data.statusCode / data.status_codeintUpstream status, both spellings present
data.status_msgstringEmpty string on a healthy response
data.extra.logidstringUpstream request identifier, handy in support tickets
data.log_pb.impr_idstringMirrors the log id

The cursor deserves a warning. It is returned as a string that looks like a millisecond epoch - 1713553237000 is roughly the upload time of the last item on the page. It is tempting to treat that as arithmetic you can do yourself, subtracting a day to skip ahead. Do not. Treat it as opaque, store it as a string, and echo back exactly what you were given. Other feed endpoints in the same family return cursors that are plain counters instead, so any code that tries to interpret the value will not port.

The fields on each itemList entry

Each entry is a full TikTok item record. The fields you will actually build on:

FieldTypeNotes
idstringThe post ID. This is what post-detail wants.
descstringThe caption, hashtags included as literal text
createTimeintUnix seconds. Note the type - post-detail returns the same value as a string.
authorobjectid, uniqueId, nickname, secUid, signature, verified, privateAccount, avatarThumb / avatarMedium / avatarLarger
authorStatsobjectfollowerCount, followingCount, heart, heartCount, diggCount, friendCount, videoCount
musicobjectid, title, authorName, playUrl, duration, original, isCopyrighted, cover variants
contentsarrayCaption broken into text segments
isAdbooleanFilter these out of organic performance reports
privateItem, secretbooleanVisibility flags on the individual post
originalItem, officalItembooleanThe misspelling of "official" is TikTok's, not a typo here
duetEnabled, shareEnabled, itemCommentStatusbool / intInteraction permissions set by the creator
CategoryType, diversificationId, IsHDBitrateint / boolClassification and quality flags

Per-video engagement counters

The counters ride in the item's stats object: playCount, diggCount (likes), commentCount, shareCount and collectCount (saves). A parallel statsV2 object carries the same five plus repostCount. Two practical notes. First, statsV2 values arrive as strings while stats values are numeric, so cast before you sum anything. Second, these are per-item blocks and TikTok does not guarantee that every field of the full item record survives into a list response - read them defensively with optional chaining and fall back to post-detail for any post where a counter you need is missing.

With playCount, diggCount, commentCount and shareCount in hand you already have everything the standard formulas need, which is the starting point for calculating a TikTok engagement rate across a creator's whole catalogue rather than from a handful of cherry-picked posts.

The paging loop

The loop is the same shape in any language: start at cursor 0, append data.itemList, carry data.cursor forward, and keep going while data.hasMore is true.

Node.js (Axios)

const axios = require("axios");

const KEY = "YOUR_API_KEY";

async function getAllVideos(secUid, { pageSize = 35, maxPages = 200 } = {}) {
  const out = [];
  let cursor = "0";
  let pages = 0;

  while (pages < maxPages) {
    const res = await axios.get("https://api.primeapi.co/user-posts", {
      params: { secUid, count: pageSize, cursor },
      headers: { "X-PrimeAPI-Key": KEY },
      timeout: 20000
    });

    const data = res.data && res.data.data;
    if (!data || !Array.isArray(data.itemList)) break;

    out.push(...data.itemList);
    pages++;
    console.log(`page ${pages}: +${data.itemList.length} (total ${out.length}), credits left ${res.headers["x-primeapi-balance"]}`);

    // stop conditions: no more pages, empty page, or a cursor that stopped moving
    if (!data.hasMore || data.itemList.length === 0 || String(data.cursor) === cursor) break;
    cursor = String(data.cursor);

    await new Promise(r => setTimeout(r, 700)); // stay well inside 100 req/min
  }

  return out;
}

getAllVideos("MS4wLjABAAAAqB08cUbXaDWqbD6MCga2RbGTuhfO2EsHayBYx08NDrN7IE3jQuRDNNN6YwyfH6_6")
  .then(videos => {
    console.log("collected", videos.length);
    videos.slice(0, 3).forEach(v => {
      const s = v.stats || {};
      console.log(v.id, new Date(v.createTime * 1000).toISOString(), s.playCount, s.diggCount, v.desc);
    });
  })
  .catch(err => console.error(err.response ? err.response.data : err.message));

Python (Requests)

import time
import requests

KEY = "YOUR_API_KEY"
BASE = "https://api.primeapi.co"


def get_all_videos(sec_uid, page_size=35, max_pages=200):
    videos, cursor, pages = [], "0", 0

    while pages < max_pages:
        resp = requests.get(
            f"{BASE}/user-posts",
            params={"secUid": sec_uid, "count": page_size, "cursor": cursor},
            headers={"X-PrimeAPI-Key": KEY},
            timeout=20,
        )
        resp.raise_for_status()
        data = (resp.json() or {}).get("data") or {}
        items = data.get("itemList") or []

        videos.extend(items)
        pages += 1
        print(f"page {pages}: +{len(items)} (total {len(videos)}), "
              f"credits left {resp.headers.get('X-PrimeAPI-Balance')}")

        new_cursor = str(data.get("cursor", ""))
        if not data.get("hasMore") or not items or new_cursor == cursor:
            break
        cursor = new_cursor
        time.sleep(0.7)

    return videos


if __name__ == "__main__":
    vids = get_all_videos(
        "MS4wLjABAAAAqB08cUbXaDWqbD6MCga2RbGTuhfO2EsHayBYx08NDrN7IE3jQuRDNNN6YwyfH6_6"
    )
    print("collected", len(vids))
    for v in vids[:3]:
        st = v.get("stats") or {}
        print(v.get("id"), v.get("createTime"), st.get("playCount"), st.get("diggCount"))

Three details in there are not decoration. The maxPages ceiling stops a malformed response from turning into an infinite loop. The cursor-equality check catches a feed that has stalled but is still reporting hasMore: true. And deduplicating on id before you write to a database is worth adding, because a creator who uploads while your loop is running can shift the window and hand you the same post twice.

When to follow up with post-detail

The feed gives you a lot, but it is a list view. post-detail takes a single postId - the id straight off an itemList entry - and returns the complete item record at itemInfo.itemStruct. Reach for it when you need:

  • The video block: duration, width, height, ratio, definition, size, bitrate, and the cover, originCover, dynamicCover and playAddr URLs.
  • A guaranteed stats and statsV2 pair, including collectCount and repostCount.
  • challenges and textExtra, for mapping which hashtags and mentions a post used.
  • shareMeta.title and shareMeta.desc, the share-card text.
  • Flags a list view will not always expose, such as takeDown, stitchEnabled and stickersOnItem.

Watch the type change: createTime is an integer in the feed and a string such as "1706722070" under itemStruct. Normalise on ingest or your date sorting will behave strangely. The economics matter too - one credit per page versus one credit per video. Enriching all 900 posts of a catalogue costs 900 credits; enriching the top 50 by playCount costs 50. Rank first, then enrich. The deeper tour of that record lives in the guide to getting TikTok video data via API.

Credits, limits and honest caveats

Every request costs 1 credit and returns your remaining balance in X-PrimeAPI-Balance. Because cost tracks pages rather than videos, mirroring a large catalogue is cheap: roughly 15 items per page means about 60 requests for 900 uploads. The default rate limit is 100 requests per minute, which a sequential loop with a short sleep will never approach - it only becomes a concern when you run many creators in parallel. Credit bundles start at 2,500 for $9.90 on the pricing page, and a new account gets 50 free credits, enough to page a mid-sized profile end to end before you spend anything.

Some limits are structural, not billing-related. Private accounts return no feed - check user.privateAccount from step 1 before you start looping. Media URLs such as playAddr are signed and expire, so store the post id as your key and re-resolve the URL when you need the file. Responses are fetched live with no caching, average around a second, and reflect counts at the moment of the call, so two runs an hour apart will legitimately disagree. And PrimeApi is an independent service, not affiliated with or endorsed by TikTok or ByteDance - what you get is the public data an anonymous visitor could see, nothing private.

Since the same itemList plus cursor plus hasMore pattern drives the other profile feeds, the loop above is reusable almost verbatim - the walkthrough on reading a user's liked videos uses an identical structure against a different endpoint.

Putting it together

Resolve the handle once and cache the secUid. Loop user-posts on data.cursor until data.hasMore goes false, with an empty-page check and a page ceiling as guards. Deduplicate on id. Keep stats from the feed for bulk reporting, and spend post-detail credits only on the posts that earned the attention. If you would rather see the shape of a live response before writing any code, the API playground runs both endpoints against your own key in the browser, and the documentation keeps the parameter and error reference in one place. Registration takes a minute, and the 50 free credits it comes with are enough to page a full catalogue and see whether the data fits your pipeline.

Frequently asked questions

Do I pass a username or a secUid to user-posts?

A secUid. The endpoint will not accept a plain @handle. Resolve the handle once with userinfo-by-username, read user.secUid from that response, and pass it as the secUid query parameter. Store it, because the secUid does not change when the creator renames their account.

Why did I get 15 videos back when I asked for 35?

count is a ceiling, not a guarantee. A live call with count=35 returned 15 items in data.itemList. Page size varies with the account and the position in the feed, so never treat the length of itemList as a signal that you have finished - read data.hasMore instead.

How do I know when I have reached the end of a creator's catalogue?

Stop when data.hasMore is false. Add two safety conditions: stop if data.itemList comes back empty, and stop if the new data.cursor equals the one you just sent, which means the feed is no longer advancing.

Can I skip ahead by making up a cursor value?

No, and you should not try. The cursor returned by user-posts is an opaque string that happens to look like a millisecond timestamp, such as 1713553237000. Always echo back the exact data.cursor the previous page returned. The first request uses cursor=0.

Do I need to call post-detail for every video in the feed?

Usually not. The feed items already carry the caption, the sound, the author block and the engagement counters, which covers most reporting jobs. Call post-detail only for the videos you actually need extra depth on - the full video block with playAddr, duration and cover variants, or a fresh stats read for a single post.

How many credits does a full catalogue cost?

One credit per request, so the cost is the number of pages, not the number of videos. At roughly 15 items per page, a creator with about 900 uploads costs around 60 credits to mirror in full. New accounts start with 50 free credits, and every response returns the remaining balance in the X-PrimeAPI-Balance header.

Start building with the PrimeApi TikTok API
50 free credits when you sign up. No card required - you only pay when you need more.

← Back to the PrimeApi Blog