Most TikTok analysis treats a creator's profile as a flat list of videos ordered by date. That is the wrong model for a growing share of accounts. Creators who run series - tutorials, episodes, recurring segments, product lines - group those videos into playlists, and that grouping is public data you can read through the API. This guide covers the user-playlist endpoint: what input it needs, every field the playList array actually returns, how its paging differs from the feed endpoints, and how to combine it with user-posts to turn a raw upload history into a structured content map.
Why playlists are worth reading
A playlist is a deliberate act. Nobody accidentally creates one, and nobody maintains one for content they consider disposable. When a creator has organised twelve videos under a single title, they have told you three things at once: which topic they treat as a franchise, how many episodes deep that franchise runs, and what they call it in their own words. None of that is recoverable from an undifferentiated feed of captions.
That makes the playlist layer useful for competitor research, creator vetting and content planning. An account with six playlists averaging fifteen videos each is running a programmed channel. An account with zero playlists and four hundred uploads is posting reactively. Both can be large, and follower count will not tell you which is which - the structure will. If you are sizing accounts on raw audience numbers first, the walkthrough on reading a TikTok follower count via API pairs naturally with this one.
Step 1: get the secUid
The playlist endpoint does not take an @handle. It takes secUid, TikTok's opaque per-user identifier, which is the same key the rest of the User category expects. You get it from one call to userinfo-by-username:
curl -s "https://api.primeapi.co/userinfo-by-username?username=taylorswift" \
-H "X-PrimeAPI-Key: YOUR_API_KEY"
The user.secUid value in that response is what you store. Persist it alongside user.id and treat both as the stable keys for the account, because uniqueId - the handle - can change whenever the creator renames. If the identifier itself is unfamiliar, the explainer on what a TikTok secUid is covers where it comes from and why it is not interchangeable with the numeric ID.
Step 2: call user-playlist
The base URL is https://api.primeapi.co/ and authentication is a single header, X-PrimeAPI-Key. The endpoint requires secUid and accepts count and cursor as optional paging parameters.
curl
curl -s "https://api.primeapi.co/user-playlist?secUid=SEC_UID&count=20&cursor=0" \
-H "X-PrimeAPI-Key: YOUR_API_KEY"
Node.js (Axios)
const axios = require("axios");
const KEY = "YOUR_API_KEY";
async function getPlaylists(secUid) {
const out = [];
let cursor = "0";
let hasMore = true;
while (hasMore) {
const res = await axios.get("https://api.primeapi.co/user-playlist", {
params: { secUid, count: 20, cursor },
headers: { "X-PrimeAPI-Key": KEY }
});
const body = res.data;
out.push(...(body.playList || []));
cursor = body.cursor;
hasMore = body.hasMore === true;
console.log("balance:", res.headers["x-primeapi-balance"]);
}
return out;
}
getPlaylists("SEC_UID")
.then(list => list.forEach(p =>
console.log(p.mixId, p.videoCount, p.mixName)))
.catch(err => console.error(err.response ? err.response.data : err.message));
Python (Requests)
import requests
BASE = "https://api.primeapi.co"
HEADERS = {"X-PrimeAPI-Key": "YOUR_API_KEY"}
def get_playlists(sec_uid, page_size=20):
playlists, cursor = [], "0"
while True:
r = requests.get(
f"{BASE}/user-playlist",
params={"secUid": sec_uid, "count": page_size, "cursor": cursor},
headers=HEADERS,
timeout=15,
)
r.raise_for_status()
body = r.json()
playlists.extend(body.get("playList", []))
cursor = body.get("cursor")
if not body.get("hasMore"):
break
return playlists
if __name__ == "__main__":
for p in get_playlists("SEC_UID"):
print(p["mixId"], p["videoCount"], p["mixName"])
The playList fields
The response puts the collections in a top-level playList array. Every entry is small - there is no nested video list - which is what makes this a cheap call to run across a whole roster of creators.
| Field | Type | What it holds |
|---|---|---|
mixId | string | The playlist identifier. Use it as your primary key. |
id | string | Mirrors mixId in the responses captured here. |
mixName | string | The playlist title as the creator typed it, in their own language. |
name | string | Mirrors mixName. Read one, keep the other as a fallback. |
videoCount | int | How many videos the playlist groups. The series-depth signal. |
cover | string | CDN URL for the playlist cover image. |
creator | object | The owning account, repeated on every entry. |
The creator object is a compact profile block: id, uniqueId, nickname, secUid, signature, the three avatar sizes (avatarThumb, avatarMedium, avatarLarger) and the flags verified, privateAccount, secret, ftc, openFavorite, relation, isADVirtual and shortDramaCreator. It repeats identically on each playlist, so if you are storing rows, normalise it out rather than writing the same profile twenty times.
Alongside playList the envelope carries cursor, hasMore, statusCode, status_code, status_msg, an extra object with logid and now, and log_pb.impr_id. The two log identifiers are worth writing to your own request log - when you need to report an anomaly to support they are the fastest way to point at one specific upstream response.
Two different kinds of cursor
This catches people out when they reuse a paging helper across endpoints. On user-playlist the cursor comes back as a small counter - "20" after a twenty-item page - so it behaves like an offset. On user-posts the cursor is a millisecond timestamp such as "1713553237000", pointing at the oldest item served. Both are strings, both are opaque, and in both cases the correct behaviour is identical: send back whatever the previous response gave you, without parsing or incrementing it, and stop when hasMore turns false.
Combining playlists with user-posts
A playlist record tells you a series exists and how long it is. It does not hand you the videos. To get those you pull the creator's feed with user-posts, which takes the same secUid and its own count and cursor.
Note the shape difference before you write the parser: user-playlist puts playList, cursor and hasMore at the top level, while user-posts nests everything one level down under data - so you read data.itemList, data.cursor and data.hasMore. Each item in itemList carries id, desc (the caption), createTime, an author block, authorStats and authorStatsV2, the music object for the sound, plus flags like isAd, originalItem, privateItem, secret, collected, digged and duetEnabled.
import re, requests
from collections import defaultdict
def get_posts(sec_uid, pages=5, page_size=35):
items, cursor = [], "0"
for _ in range(pages):
r = requests.get(
f"{BASE}/user-posts",
params={"secUid": sec_uid, "count": page_size, "cursor": cursor},
headers=HEADERS,
timeout=20,
)
r.raise_for_status()
data = r.json()["data"]
items.extend(data.get("itemList", []))
cursor = data.get("cursor")
if not data.get("hasMore"):
break
return items
def series_map(sec_uid):
playlists = get_playlists(sec_uid)
posts = get_posts(sec_uid)
buckets = defaultdict(list)
for pl in playlists:
stem = re.sub(r"[^\w\s]", "", pl["mixName"]).strip().lower()
if len(stem) < 4:
continue
for item in posts:
if stem in item.get("desc", "").lower():
buckets[pl["mixId"]].append(item["id"])
for pl in playlists:
matched = len(buckets[pl["mixId"]])
print(f"{pl['mixName']}: {pl['videoCount']} in playlist, "
f"{matched} matched by caption")
return buckets
Be honest with yourself about what that join is. Since no field links an item back to its playlist, matching on the playlist title inside desc is a heuristic - it works well when creators prefix episodes with the series name, which many do precisely so viewers can find them, and it fails on playlists named generically. The useful output is not a perfect assignment but the gap between videoCount and your matched count, which tells you how consistently that creator labels their own series. When you need real per-video engagement figures for the items you did match, pass each id to post-detail, which returns the full stats block for one video. The companion guide on pulling every video from a TikTok user goes deeper on paging the feed itself, and the post on video data covers the detail call.
Reading the numbers
Once you have playlists for a set of accounts, a few derived figures do most of the work. Playlist count is a proxy for how programmed the channel is. Mean videoCount across playlists shows whether series get finished or abandoned after two episodes. Total videos inside playlists, divided by the account's overall video count, gives a structure ratio - the share of output the creator considered part of something. And mixName is free topic labelling: a creator has already written the category names for their own content, which is more reliable than clustering captions.
An empty playList is a result, not a failure. Most accounts have never made a playlist, and that absence is exactly the distinction you are measuring. What you should not do is read videoCount as a popularity metric - it counts videos, not views, and a twenty-part series can be twenty flops.
Credits, limits and errors
Each request costs 1 credit, whatever it returns, and the remaining balance comes back in X-PrimeAPI-Balance. The default rate limit is 100 requests per minute, which for playlist work is generous: most creators need a single page, so a thousand-account sweep is roughly a thousand credits and a handful of minutes. The feed pages are the expensive half - budget by pages of user-posts, not by playlists. New accounts start with 50 free credits, enough to test both endpoints against a real profile; paid tiers on the pricing page run from 2,500 credits ($9.90) to 500,000 ($279.00).
Four errors arrive as a JSON message rather than as a status code alone: "Please sign up to primeapi.co" when no key header was sent, "PrimeAPI-Key is not available" for a wrong or unactivated key, the per-minute limit message when you cross 100 requests in a window, and "Your balance has been exhausted..." when credits run out. Branch on that body. A private account is a separate case - it returns no usable data rather than an error, so check for an empty playList before assuming your paging broke. The API documentation keeps the full reference, and you can run either endpoint against a live profile in the playground before writing any code.
Putting it together
The pattern is short: resolve the handle to a secUid once, call user-playlist to read the creator's own structure, then page user-posts for the videos and join the two on whatever signal the creator gives you. Store mixId, mixName and videoCount per account and re-run it weekly - a playlist that gains entries is a series still in production, and one that has been static for months is finished. That is a cheap, honest signal about what a creator is actually building, and it costs one credit to read. PrimeApi is an independent service and is not affiliated with or endorsed by TikTok or ByteDance; create an account and the 50 free credits are enough to map a dozen creators before you decide.
Frequently asked questions
What input does the user-playlist endpoint need?
It needs the creator's secUid, not the @handle and not the numeric ID. Resolve the handle once with userinfo-by-username, store the secUid it returns, then pass it to user-playlist along with the optional count and cursor parameters.
Does the playlist response include the videos inside each playlist?
No. A playList entry describes the collection itself - mixId, mixName, name, cover, videoCount and a creator block. It does not carry the video IDs it groups, so to get the actual clips you pull the creator feed with user-posts and match them yourself.
Why does the cursor look different from the one on user-posts?
They are different kinds of value. user-playlist returns a cursor like "20", which is the number of playlist records already served, while user-posts returns a cursor like "1713553237000", a millisecond timestamp of the oldest item in the page. In both cases you should echo the cursor back verbatim rather than compute it.
What does it mean if a creator has no playlists?
The call succeeds and playList comes back empty with hasMore false. Playlists are optional on TikTok, so an empty array is a normal result for most accounts and not an error condition - it is itself a signal that the creator publishes unstructured, one-off content.
Do mixId and id ever differ?
In the live responses captured for this guide the playlist id and mixId hold the same string. Key your own storage on mixId, since that is the field TikTok uses for the collection, and treat name and mixName the same way - they mirror each other, so read one and keep the other as a fallback.
How many credits does a playlist crawl cost?
One credit per request, the same as every other endpoint, and your remaining balance comes back in the X-PrimeAPI-Balance header. Most creators fit into a single user-playlist page, so the cost of the playlist layer is usually one credit per account; the feed pages you pull with user-posts afterwards are what actually drive spend.