To find which TikTok videos move your Spotify streams, pull every video using your sound with Scavio's TikTok search/videos endpoint, then read each video's comments to count how many people ask where to stream the song. An API gives you TikTok-side signals (views, shares, saves, and the actual comment text), not a Spotify attribution number. So the honest answer to "why did a viral clip add only 168 streams?" is that views do not equal listening intent, and the only TikTok-measurable proxy for intent is comments asking for the song. This tutorial ranks your own posts and the user-generated clips that use your track by intent-per-view, so you stop guessing which format works and copy the one whose comments are full of "what's this song?". You will not get a clean view-to-stream ratio. What you get is a ranked list of videos and styles that correlate with people actively looking to listen.
Prerequisites
- A Scavio API key (sk_live_...) from your dashboard; new accounts get 50 free credits. TikTok endpoints cost 1 credit each.
- Python 3.9+ with requests, or Node.js 18+ for the JavaScript version.
- Your TikTok username and the exact sound or hashtag name your track is published under.
- An LLM (Claude or similar) is optional but helps classify comment intent at scale.
Walkthrough
Step 1: Resolve your username to a sec_uid
TikTok's posts endpoint needs a sec_user_id, not a handle. Hit the profile endpoint with your username first and read data.user.sec_uid out of the response. This is the two-step pattern for every per-user call in the TikTok API. Cache the sec_uid; it does not change, so you never need to repeat this lookup.
import requests
HEADERS = {
"Authorization": "Bearer sk_live_...",
"Content-Type": "application/json",
}
resp = requests.post(
"https://api.scavio.dev/api/v1/tiktok/profile",
headers=HEADERS,
json={"username": "your_artist_handle"},
)
sec_uid = resp.json()["data"]["user"]["sec_uid"]
print(sec_uid)Step 2: Pull your own posts and their stats
Send the sec_uid to user/posts with a count to get your recent videos plus per-video stats: play_count, share_count, comment_count, and digg_count (likes). Saves and shares matter more than raw views here, because a save is the closest TikTok signal to "I want to come back to this song". Sort your posts by share and comment rate, not by views, and you will already see which of your own clips behave differently.
resp = requests.post(
"https://api.scavio.dev/api/v1/tiktok/user/posts",
headers=HEADERS,
json={"sec_user_id": sec_uid, "count": 30},
)
posts = resp.json()["data"]["videos"]
for v in posts:
s = v["statistics"]
print(v["video_id"], s["play_count"], s["share_count"], s["comment_count"])Step 3: Find every video using your sound
Most of your reach is other people's videos using your track, not your own. Query search/videos with the sound name or a branded hashtag to collect the UGC. Each result carries the same statistics block. This is where an API beats manual scrolling: you get a structured list of clips you would never find by hand, ranked candidates for which style is spreading your song.
resp = requests.post(
"https://api.scavio.dev/api/v1/tiktok/search/videos",
headers=HEADERS,
json={"query": "your sound name or #yourtrackhashtag", "count": 50},
)
ugc = resp.json()["data"]["videos"]
print(f"found {len(ugc)} videos using the track")Step 4: Read comments for listening intent
This is the step that approximates conversion. For the top videos by reach, pull video/comments and count comments that ask where to stream: "what's this song", "name of the song", "add to my playlist", "on Spotify?". That ratio of intent-comments to views is the best TikTok-only proxy you have for streams. A clip with 50k views and 40 "what song?" comments is doing more for you than a 2M-view clip with none.
def intent_comments(video_id):
resp = requests.post(
"https://api.scavio.dev/api/v1/tiktok/video/comments",
headers=HEADERS,
json={"video_id": video_id, "count": 50},
)
comments = resp.json()["data"]["comments"]
cues = ["what song", "name of", "spotify", "playlist", "where can i"]
hits = [c["text"] for c in comments
if any(q in c["text"].lower() for q in cues)]
return len(hits), hitsStep 5: Rank by intent-per-view and copy the winner
Score every video as intent_comments divided by views. Sort descending. The top of that list is your template: the hook length, the on-screen text, the part of the song used, the call to action. Be honest about the ceiling here. The API cannot tell you a single one of those people actually streamed the track. What it tells you, repeatably, is which video styles make people ask for the song, and that is the variable you can control next time you post.
scored = []
for v in posts + ugc:
vid = v["video_id"]
views = v["statistics"]["play_count"] or 1
n_intent, _ = intent_comments(vid)
scored.append((vid, n_intent / views, n_intent, views))
scored.sort(key=lambda x: x[1], reverse=True)
for vid, rate, n, views in scored[:5]:
print(f"{vid}: {n} intent / {views} views = {rate:.5f}")Python Example
import requests
API_KEY = "sk_live_..."
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
BASE = "https://api.scavio.dev/api/v1/tiktok"
INTENT_CUES = ["what song", "name of", "spotify", "playlist", "where can i"]
def get_sec_uid(username):
resp = requests.post(f"{BASE}/profile", headers=HEADERS,
json={"username": username})
resp.raise_for_status()
return resp.json()["data"]["user"]["sec_uid"]
def get_my_posts(sec_uid, count=30):
resp = requests.post(f"{BASE}/user/posts", headers=HEADERS,
json={"sec_user_id": sec_uid, "count": count})
resp.raise_for_status()
return resp.json()["data"]["videos"]
def search_sound(query, count=50):
resp = requests.post(f"{BASE}/search/videos", headers=HEADERS,
json={"query": query, "count": count})
resp.raise_for_status()
return resp.json()["data"]["videos"]
def intent_comments(video_id, count=50):
resp = requests.post(f"{BASE}/video/comments", headers=HEADERS,
json={"video_id": video_id, "count": count})
resp.raise_for_status()
comments = resp.json()["data"]["comments"]
hits = [c["text"] for c in comments
if any(q in c["text"].lower() for q in INTENT_CUES)]
return len(hits)
def rank_videos(username, sound_query):
sec_uid = get_sec_uid(username)
videos = get_my_posts(sec_uid) + search_sound(sound_query)
scored = []
for v in videos:
vid = v["video_id"]
views = v["statistics"]["play_count"] or 1
n_intent = intent_comments(vid)
scored.append({
"video_id": vid,
"views": views,
"intent": n_intent,
"intent_per_view": n_intent / views,
})
scored.sort(key=lambda r: r["intent_per_view"], reverse=True)
return scored
if __name__ == "__main__":
rows = rank_videos("your_artist_handle", "#yourtrackhashtag")
for r in rows[:5]:
print(f"{r['video_id']}: {r['intent']} intent / "
f"{r['views']} views = {r['intent_per_view']:.5f}")JavaScript Example
const API_KEY = "sk_live_...";
const HEADERS = {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
};
const BASE = "https://api.scavio.dev/api/v1/tiktok";
const INTENT_CUES = ["what song", "name of", "spotify", "playlist", "where can i"];
async function post(path, body) {
const resp = await fetch(`${BASE}${path}`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify(body),
});
if (!resp.ok) throw new Error(`${path} ${resp.status}`);
return resp.json();
}
async function getSecUid(username) {
const data = await post("/profile", { username });
return data.data.user.sec_uid;
}
async function getMyPosts(secUid, count = 30) {
const data = await post("/user/posts", { sec_user_id: secUid, count });
return data.data.videos;
}
async function searchSound(query, count = 50) {
const data = await post("/search/videos", { query, count });
return data.data.videos;
}
async function intentComments(videoId, count = 50) {
const data = await post("/video/comments", { video_id: videoId, count });
const comments = data.data.comments;
const hits = comments.filter((c) =>
INTENT_CUES.some((q) => c.text.toLowerCase().includes(q))
);
return hits.length;
}
async function rankVideos(username, soundQuery) {
const secUid = await getSecUid(username);
const videos = [
...(await getMyPosts(secUid)),
...(await searchSound(soundQuery)),
];
const scored = [];
for (const v of videos) {
const views = v.statistics.play_count || 1;
const intent = await intentComments(v.video_id);
scored.push({
videoId: v.video_id,
views,
intent,
intentPerView: intent / views,
});
}
scored.sort((a, b) => b.intentPerView - a.intentPerView);
return scored;
}
rankVideos("your_artist_handle", "#yourtrackhashtag").then((rows) => {
for (const r of rows.slice(0, 5)) {
console.log(
`${r.videoId}: ${r.intent} intent / ${r.views} views = ` +
`${r.intentPerView.toFixed(5)}`
);
}
});Expected Output
7389201144556677889: 41 intent / 52310 views = 0.00078
7388110233445566778: 12 intent / 48902 views = 0.00025
7390455667788990011: 6 intent / 211487 views = 0.00003
7387009988776655443: 2 intent / 1842009 views = 0.00000
7386554433221100998: 0 intent / 73620 views = 0.00000