A TikTok video page shows you a caption, four counters and a sound name. The record behind that page is far bigger: encoding details and multiple play addresses, subtitle tracks, the full sound object, the author profile with their own statistics, every hashtag attached to the caption, and five separate engagement counters. This guide covers how to read that record with the post-detail endpoint - how to extract a post ID from a TikTok URL, what each block of the response holds, and how to collect IDs in bulk with user-posts when you need a whole catalogue instead of a single clip.
How do you get a post ID from a TikTok URL?
Everything starts with the numeric post ID, and almost every input you receive from a user will be a URL rather than an ID. There are three cases worth handling.
Standard web URLs
A canonical video URL looks like https://www.tiktok.com/@taylorswift/video/7330315169584778539. The post ID is the trailing numeric segment. Image slideshow posts use /photo/ in place of /video/ but follow the same shape, so one pattern covers both. Strip any query string first - share links routinely arrive with tracking parameters appended.
import re
def post_id_from_url(url: str):
m = re.search(r"/(?:video|photo)/(\d+)", url.split("?")[0])
return m.group(1) if m else None
print(post_id_from_url("https://www.tiktok.com/@taylorswift/video/7330315169584778539?is_from_webapp=1"))
# 7330315169584778539
Short links
Links shared from the mobile app look like https://vm.tiktok.com/ZMxxxxxxx/ or https://vt.tiktok.com/ZSxxxxxxx/. These carry no post ID at all - the ID only exists after the redirect resolves. Follow the redirect yourself with an ordinary HTTP client and read the Location header, then run the resulting canonical URL through the pattern above. This step costs no credits because it never touches the API.
IDs in bulk
If your input is a creator rather than a link, skip URL parsing entirely. Resolve the handle to a secUid once, then call user-posts: every item in the returned feed already exposes its post ID in the id field, ready to hand straight to post-detail. You get that secUid from a one-off handle lookup with userinfo-by-username; the identifier itself is explained in what a TikTok secUid is and how to get one.
Calling post-detail
The base URL is https://api.primeapi.co/, authentication is the single header X-PrimeAPI-Key, and post-detail takes one required query parameter: postId. There are no optional parameters, which makes it one of the simplest calls in the catalogue.
curl
curl -s "https://api.primeapi.co/post-detail?postId=7330315169584778539" \
-H "X-PrimeAPI-Key: YOUR_API_KEY"
Node.js (Axios)
const axios = require("axios");
async function getPost(postId) {
const res = await axios.get("https://api.primeapi.co/post-detail", {
params: { postId },
headers: { "X-PrimeAPI-Key": "YOUR_API_KEY" }
});
console.log("Remaining credits:", res.headers["x-primeapi-balance"]);
const item = res.data.itemInfo && res.data.itemInfo.itemStruct;
if (!item) throw new Error("No item struct - video unavailable");
return item;
}
getPost("7330315169584778539")
.then(item => console.log(item.desc, item.stats.playCount))
.catch(err => console.error(err.response ? err.response.data : err.message));
Python (Requests)
import requests
def get_post(post_id):
resp = requests.get(
"https://api.primeapi.co/post-detail",
params={"postId": post_id},
headers={"X-PrimeAPI-Key": "YOUR_API_KEY"},
timeout=15,
)
print("Remaining credits:", resp.headers.get("X-PrimeAPI-Balance"))
resp.raise_for_status()
return resp.json().get("itemInfo", {}).get("itemStruct")
item = get_post("7330315169584778539")
if item:
print(item["desc"])
print(item["stats"]["playCount"], "plays")
print(item["music"]["title"], "by", item["music"]["authorName"])
What the response contains
Everything sits under itemInfo.itemStruct. Alongside it the payload carries a small shareMeta object with title and desc - the text TikTok uses for link previews - plus statusCode and statusMsg. The item struct itself groups into five blocks: the post, the video, the music, the author, and the counters.
| Field | Holds |
|---|---|
id | The post ID, echoed back as a string. |
desc | The caption, hashtags and mentions included as written. |
createTime | Upload time as a Unix timestamp in seconds. Returned as a string here. |
video.duration | Clip length in seconds. |
video.width, video.height, video.ratio | Pixel dimensions plus the quality label TikTok assigns. |
video.cover, originCover, dynamicCover | Still thumbnail, unprocessed still, and the animated preview. shareCover, reflowCover and zoomCover cover other placements. |
video.playAddr, downloadAddr | Signed CDN addresses for streaming and for the download variant. |
video.bitrate, bitrateInfo, size | Default bitrate, the per-variant list, and file size in bytes. |
video.format, codecType, encodedType, definition, videoQuality | Container, codec and quality descriptors for the default rendition. |
video.subtitleInfos | Available caption tracks for the clip. |
video.volumeInfo, VQScore | Loudness normalisation data and TikTok's internal quality score. |
music | The sound: id, title, authorName, album, duration, preciseDuration, playUrl, cover art at three sizes, and the original, isCopyrighted, is_commerce_music flags. |
author | The creator: id, uniqueId, nickname, secUid, signature, verified, avatars, and interaction settings such as duetSetting, stitchSetting and downloadSetting. |
authorStats | The creator's own totals: followerCount, followingCount, heartCount, videoCount, diggCount, friendCount. |
stats / statsV2 | playCount, diggCount, commentCount, shareCount, collectCount; statsV2 adds repostCount. |
challenges | One record per hashtag on the post, each with its own ID and title. |
textExtra | Positioned entities inside the caption - hashtags and mentions with their offsets. |
| Flags | isAd, originalItem, officalItem, privateItem, secret, duetEnabled, stitchEnabled, shareEnabled, itemCommentStatus, takeDown. |
Play addresses, bitrate variants and subtitles
The video block is where this endpoint earns its keep. bitrate gives you the default rendition, while bitrateInfo enumerates the variants TikTok encoded, each with its own play address - useful if you are picking a rendition to match a bandwidth budget rather than always taking the default. subtitleInfos lists the caption tracks, which is the practical route into transcript work: you get the machine-generated captions without running speech recognition yourself.
Treat playAddr and downloadAddr as short-lived. They are signed URLs, they expire, and fetching them usually needs the right request headers. If your goal is a file you can store or serve, the watermark-free download walkthrough covers the endpoint built for that, which takes the post URL and hands back a plain address you can stream or store.
Music, hashtags and the author
Two nested blocks are useful as pivots rather than as display data. The music.id value identifies the sound, and feeding it to music-info or music-posts turns a single video into a trend question: is this track still spreading, and who else is filming over it? The challenges array does the same job for hashtags, and author.secUid lets you jump straight from a video to that creator's whole feed without a separate profile lookup.
From one video to a whole catalogue
For anything beyond a single clip, user-posts is the companion call. It takes a required secUid plus optional count and cursor, and returns data.itemList with data.cursor and data.hasMore for paging. Each feed item is a trimmed version of the same structure - id, desc, createTime, the author and authorStats blocks, music, and flags such as IsHDBitrate, collected, digged and isAd. When you need the full video block, subtitle tracks or the complete counter set, take the item's id and call post-detail on it.
import requests
HEADERS = {"X-PrimeAPI-Key": "YOUR_API_KEY"}
def all_post_ids(sec_uid, max_pages=10):
ids, cursor = [], "0"
for _ in range(max_pages):
r = requests.get(
"https://api.primeapi.co/user-posts",
params={"secUid": sec_uid, "count": 35, "cursor": cursor},
headers=HEADERS, timeout=20,
)
data = r.json().get("data", {})
ids += [i["id"] for i in data.get("itemList", [])]
if not data.get("hasMore"):
break
cursor = data.get("cursor")
return ids
Budget this properly: one page of the feed is 1 credit, and every enrichment call is another. Fetching a 200-video catalogue at 35 per page costs about 6 credits for the feed and 200 for the details. The full-catalogue guide works through the paging edge cases. Once you have the counters, engagement rate is straightforward arithmetic on stats and authorStats.
Credits, limits and errors
Every request costs 1 credit, including one that finds nothing, and the default rate limit is 100 requests per minute. Data is fetched live with no caching, so a play count reflects the moment you asked, and responses average around a second. Watch the X-PrimeAPI-Balance header rather than polling your dashboard.
Four failures return a JSON message and are worth branching on explicitly: "Please sign up to primeapi.co" means no key header was sent; "PrimeAPI-Key is not available" means the key is wrong or the account is not activated; the per-minute message means you crossed 100 requests in a window; and "Your balance has been exhausted..." means you are out of credits. Separately from those, a valid request for an unavailable video returns without a populated itemStruct - always check for it before reading fields. Full error details live in the API documentation, and you can try any endpoint against your own key in the interactive playground before writing code.
Putting it together
The pattern that holds up in production is short: normalise whatever arrives - short link, canonical URL or raw ID - into a post ID, call post-detail once per video, store the item struct as delivered rather than a handful of extracted fields, and re-fetch on a schedule if you are tracking growth. When you need more than one clip, drive it from user-posts and enrich each ID. Comment threads sit one call away too, via the comment scraping guide. Fifty free credits on a new account are enough to test the whole flow end to end before you commit to a plan. PrimeApi is an independent service and is not affiliated with or endorsed by TikTok or ByteDance.
Frequently asked questions
How do I find the post ID in a TikTok URL?
It is the numeric segment at the end of a standard video URL. In https://www.tiktok.com/@taylorswift/video/7330315169584778539 the post ID is 7330315169584778539. A regular expression such as /(?:video|photo)/(\d+)/ pulls it out reliably, and you should strip any ? query string first. Short vm.tiktok.com links do not contain the ID at all - follow the redirect to the full URL, then parse that.
Do I need a secUid or a login to call post-detail?
No. post-detail takes one required parameter, postId, and your X-PrimeAPI-Key header. The secUid identifier is only needed by profile-feed endpoints such as user-posts, and there is no TikTok login or OAuth step anywhere in the flow.
How many credits does one video lookup cost?
One credit per request, the same as every other endpoint, whether the video is a viral clip or an obscure one. New accounts start with 50 free credits and the remaining balance comes back in the X-PrimeAPI-Balance response header, so you can track spend without a second call.
What is the difference between stats and statsV2?
They carry the same five engagement counters - playCount, diggCount, commentCount, shareCount and collectCount - but stats returns them as numbers while statsV2 returns them as strings and adds repostCount. Read statsV2 when you want repost data, and cast its values before doing arithmetic.
Can I download the video file from the post-detail response?
The video block contains playAddr and downloadAddr, but those are signed CDN addresses that expire and normally require the right request headers to fetch. For a stable, watermark-free file link use the download-video endpoint instead, which takes the post URL and returns a plain playable address.
Why does a post return no item data?
Deleted, private, region-blocked and age-restricted videos will not come back with a populated itemInfo.itemStruct, even though the request still succeeds and still costs a credit. Check that itemStruct exists before reading fields from it, and log statusCode and statusMsg so you can tell an unavailable video apart from a malformed post ID.