A TikTok sound page shows a video count and an endless scroll. Neither answers the question that actually matters: is this track still spreading, or did it peak three weeks ago and leave behind a large number that no longer means anything? This guide covers how to page the full video list attached to a sound with the music-posts endpoint, what each video record in that list contains, and how to turn those records into a defensible measure of a sound's reach instead of quoting a headline number.
Where the music ID comes from
Every call in this workflow is keyed on a numeric music ID, so the first job is getting one. There are two reliable routes.
From a sound URL
A TikTok sound page lives at a URL like https://www.tiktok.com/music/MAKING-MY-WAY-7224128604890990593. The trailing numeric segment is the music ID. The slug in front of it is a human-readable title and can change, so parse from the end rather than matching the whole path.
import re
def music_id_from_url(url: str):
m = re.search(r"-(\d+)/?$", url.split("?")[0])
return m.group(1) if m else None
print(music_id_from_url("https://www.tiktok.com/music/MAKING-MY-WAY-7224128604890990593"))
# 7224128604890990593
From a video that already uses the track
More often you start from a video rather than a sound. Every item structure in the catalogue carries the sound it was filmed over. Call post-detail on a post ID and read itemInfo.itemStruct.music.id; the same music.id field is present on the items returned by user-posts, challenge-posts and place-posts, so a feed you are already collecting doubles as a source of sound IDs at no extra cost. The full item structure is broken down in the video data guide.
Confirm the sound first with music-info
Before you spend credits paging, spend one on music-info. It takes a single required musicId and returns musicInfo, which holds three useful blocks. musicInfo.music carries id, title, album, authorName, duration, shoot_duration, playUrl, cover art at three sizes, and the original, isCopyrighted, is_commerce_music and is_unlimited_music flags. musicInfo.artist (with musicInfo.artists for tracks credited to more than one account) gives the account behind the sound, including uniqueId, nickname, secUid and signature. And musicInfo.stats.videoCount is TikTok's own count of videos using the track.
curl -s "https://api.primeapi.co/music-info?musicId=7224128604890990593" \
-H "X-PrimeAPI-Key: YOUR_API_KEY"
Two of those fields change how you read everything that follows. original tells you whether this is a creator's own recording or a licensed release - an original sound with 40,000 videos behind it is a genuinely viral audio meme, while a commercial track with the same number is mostly a distribution artefact of being in the music library. And videoCount is your denominator: it is the number you will compare your sample against, and the number you should stop quoting on its own once you have finished reading this. The sound metadata itself is covered in more depth in the sound and music info guide.
Paging the video list with music-posts
The base URL is https://api.primeapi.co/ and authentication is the single header X-PrimeAPI-Key. music-posts requires musicId and accepts count and cursor as optional parameters.
curl -s "https://api.primeapi.co/music-posts?musicId=7224128604890990593&count=30&cursor=0" \
-H "X-PrimeAPI-Key: YOUR_API_KEY"
The response envelope is worth reading closely, because it is not the one the profile feeds use. At the top level you get code, msg and processed_time, and the payload sits under data with three keys: videos (the array of records), cursor (an integer to send on the next call) and hasMore (also an integer, not a boolean). If you reuse a parser written for user-posts it will find nothing, so give this endpoint its own adapter.
Node.js (Axios)
const axios = require("axios");
const HEADERS = { "X-PrimeAPI-Key": "YOUR_API_KEY" };
async function soundPage(musicId, cursor = 0, count = 30) {
const res = await axios.get("https://api.primeapi.co/music-posts", {
params: { musicId, count, cursor },
headers: HEADERS
});
console.log("Remaining credits:", res.headers["x-primeapi-balance"]);
const data = res.data.data || {};
return {
videos: data.videos || [],
cursor: data.cursor,
hasMore: Boolean(data.hasMore)
};
}
(async () => {
let cursor = 0;
let all = [];
for (let i = 0; i < 5; i++) {
const page = await soundPage("7224128604890990593", cursor);
all = all.concat(page.videos);
if (!page.hasMore) break;
cursor = page.cursor;
}
console.log(all.length, "videos");
console.log(all[0].aweme_id, all[0].play_count);
})();
Three practical notes on paging. Always advance with the cursor the API returned rather than incrementing an offset yourself. Stop on hasMore falling to 0, but also stop on an empty videos array, because a page that comes back empty while still claiming more will otherwise spin. And set a hard page ceiling in code - a sound with hundreds of thousands of videos will happily consume your entire credit balance one page at a time if nothing tells the loop to give up.
What a video record contains
The records in data.videos are video-centric and use snake_case names. These are the fields returned by a live call.
| Field | Holds |
|---|---|
aweme_id | The post ID. This is the key that unlocks everything else - hand it to post-detail. |
video_id | The internal video asset identifier, distinct from the post ID. |
region | Region code the post is filed under, useful for splitting reach by market. |
title, content_desc | Caption text. Both appear on the record; read whichever is populated. |
duration | Clip length in seconds. |
cover, origin_cover, ai_dynamic_cover | Still thumbnail, unprocessed still, and the animated preview. |
play, wmplay | Playable video addresses - the plain rendition and the watermarked variant. |
size, wm_size | File sizes in bytes for those two renditions. |
music | The sound object attached to the clip, echoed back per video. |
play_count, digg_count, comment_count, share_count, collect_count, download_count | Six engagement counters. This set is what makes reach measurable. |
create_time | Upload time as a Unix timestamp in seconds. |
is_ad, commerce_info, commercial_video_info | Paid and commercial content markers. |
anchors, anchors_extras | Anchor cards attached to the post. |
mentioned_users | Accounts tagged in the post. |
item_comment_settings | Comment permission state for the clip. |
A few more fields ride along beyond this list, so store the raw record rather than a hand-picked subset - re-paging a sound later to recover a field you discarded is the expensive mistake here. Note that download_count is a counter TikTok exposes on this endpoint and not on the item structure returned by post-detail, which is a small argument for keeping both.
Measuring reach instead of quoting a number
A sound with 80,000 videos sounds enormous. It is meaningless until you know how those videos are distributed. Three calculations, all of which run on fields you already have, do most of the work.
Concentration
Sum play_count across your sample, then compare the mean against the median. When the mean is many times the median, one or two large videos are carrying the whole track and the sound is not spreading - it is being watched. A sound where the top clip accounts for 60% of total plays is a single viral video with an audio credit attached, not an audio trend, and building a campaign on it means competing with that one video for attention.
Creation velocity
Bucket create_time by day. Because music-posts returns newest first, the timestamps on your first page tell you how fast the sound is currently being used. If page one spans four hours, the track is live. If page one spans five months, videoCount is a historical total and nothing more.
Engagement per play
Add digg_count, comment_count, share_count and collect_count per video and divide by play_count. Plays are the cheapest signal on the platform; saves and shares are the expensive ones. A sound whose videos collect a high collect_count relative to plays is one people intend to use themselves, which is the closest thing to a leading indicator you will find in this data.
import requests
from statistics import median
HEADERS = {"X-PrimeAPI-Key": "YOUR_API_KEY"}
def sound_videos(music_id, max_pages=5, count=30):
videos, cursor = [], 0
for _ in range(max_pages):
r = requests.get(
"https://api.primeapi.co/music-posts",
params={"musicId": music_id, "count": count, "cursor": cursor},
headers=HEADERS, timeout=20,
)
r.raise_for_status()
data = r.json().get("data", {})
batch = data.get("videos", [])
if not batch:
break
videos += batch
if not data.get("hasMore"):
break
cursor = data.get("cursor")
return videos
vids = sound_videos("7224128604890990593")
plays = sorted((v.get("play_count") or 0) for v in vids)
total = max(sum(plays), 1)
engaged = sum(
(v.get("digg_count") or 0) + (v.get("comment_count") or 0)
+ (v.get("share_count") or 0) + (v.get("collect_count") or 0)
for v in vids
)
print("sampled videos:", len(vids))
print("total plays:", total)
print("mean / median:", round(total / len(plays)), "/", median(plays))
print("top video share:", round(plays[-1] / total * 100, 1), "%")
print("engagement per play:", round(engaged / total * 100, 2), "%")
print("newest / oldest:", max(v["create_time"] for v in vids),
min(v["create_time"] for v in vids))
Report the sample size next to every one of these figures. You are measuring a sample of the newest videos on the sound, not the whole population, and a number quoted without its denominator is exactly the problem you set out to fix. Comparing your sampled totals against musicInfo.stats.videoCount tells you what fraction you actually read. For running this on a schedule across a watchlist of tracks, the trending sounds guide covers the sampling cadence.
Enriching the clips that matter
Once you have ranked a sound's videos by plays or by engagement per play, the top slice is usually worth a second call. Passing aweme_id to post-detail returns the full item structure: the author block with uniqueId, nickname and secUid, the creator's own authorStats totals, the challenges array of hashtags used alongside the sound, textExtra for caption entities, and both stats and statsV2 - the latter adding repostCount. That is how a sound audit becomes a creator shortlist: the accounts winning with a track are the accounts to approach about it.
Keep the enrichment set small and deliberate. Ten post-detail calls on the top ten videos cost 10 credits and answer the question. Ten thousand calls on the full tail cost 10,000 and answer the same question. If you also want the audio itself, download-music turns a post URL into a single play link, walked through in the audio download guide.
Credits, limits and errors
Every request costs 1 credit, including a page that comes back empty, and the default rate limit is 100 requests per minute. Data is fetched live with no caching, so counters reflect the moment you asked and responses average around a second. Read X-PrimeAPI-Balance on each response instead of polling a dashboard - it is the cheapest way to notice a runaway paging loop.
Four failures return a JSON message worth branching on: "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 the rate limit, and "Your balance has been exhausted..." means you are out of credits. A valid request against a sound with no reachable videos still succeeds and still costs a credit, so check data.videos before reading it. Full details are in the API documentation, and both endpoints can be run against your own key in the interactive playground before you write any code.
Putting it together
The workflow that holds up is four steps: resolve a music ID from a sound URL or from a video's music.id, spend one credit on music-info to confirm the track and capture videoCount, page music-posts with a hard ceiling and store the raw records, then enrich only the top slice with post-detail. Run it twice a week and the deltas in creation velocity will tell you far more than any single scrape did. Fifty free credits on a new account are enough to audit a sound 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
Where do I find a TikTok music ID?
A sound page URL ends in a numeric segment - in https://www.tiktok.com/music/MAKING-MY-WAY-7224128604890990593 the music ID is 7224128604890990593. If you do not have the sound URL, take any video that uses the track and read itemInfo.itemStruct.music.id from a post-detail response, or the music.id field on any feed item returned by user-posts, challenge-posts or place-posts.
How do I page through all the videos using a sound?
music-posts takes a required musicId plus optional count and cursor. Start at cursor=0, read data.videos, then pass data.cursor back on the next call and stop when data.hasMore is 0. Note that hasMore is an integer here rather than a boolean, so test it as truthy rather than comparing it to true.
Why does music-posts look different from the other feed endpoints?
It returns a different envelope. Instead of itemList with camelCase fields, you get code, msg, processed_time and a data object holding videos, cursor and hasMore, and the video records use snake_case names such as play_count and create_time. Write a small adapter for it rather than reusing the parser you built for profile feeds.
Does music-posts tell me who made each video?
The records are video-centric: they carry aweme_id, the caption, the counters, the cover images and the playable addresses, plus mentioned_users. When you need the creator behind a clip, take its aweme_id and call post-detail, whose author block returns uniqueId, nickname, secUid and the avatars, alongside authorStats.
How many credits does auditing one sound cost?
One credit per request. A single music-info lookup is 1 credit and each page of music-posts is another, so sampling 300 videos at 30 per page costs about 11 credits in total. Enriching every one of those videos with post-detail would add 300 more, which is why it is worth enriching only the clips that matter. New accounts start with 50 free credits and the balance comes back in the X-PrimeAPI-Balance header.
Can I get the audio file for the sound itself?
Yes, two ways. music-info returns musicInfo.music.playUrl, a direct address for the track. Alternatively download-music takes a post URL and answers with a single play field holding a playable link to the audio behind that video, which is the cheaper option when you are working from video URLs rather than music IDs.