Scenario
You’re spending $50K/month across 30+ TikTok creatives but don’t know which ones should be winning. This job pulls all creatives with their TikTok performance metrics (CTR, CVR, CPA), runs Video Analysis on each, then builds a leaderboard that cross-references Mavera’s behavioral scores (hook, emotion, pacing) against actual performance. The result is a predictive scoring model: find high-behavioral-score / low-spend creatives to scale, and low-score / high-spend creatives to kill.Architecture
Code
import os, requests, time, tempfile
TT = os.environ["TIKTOK_ACCESS_TOKEN"]
ADV = os.environ["TIKTOK_ADVERTISER_ID"]
MV = os.environ["MAVERA_API_KEY"]
TT_BASE = "https://business-api.tiktok.com/open_api/v1.3"
MV_BASE = "https://app.mavera.io/api/v1"
TT_H = {"Access-Token": TT}
MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
# 1. Pull integrated report — creative-level performance
report = requests.post(f"{TT_BASE}/reports/integrated/get/",
headers=TT_H,
json={
"advertiser_id": ADV,
"report_type": "BASIC",
"data_level": "AUCTION_AD",
"dimensions": ["ad_id"],
"metrics": ["ad_name", "impressions", "clicks", "ctr", "conversion", "cost_per_conversion",
"spend", "video_play_actions", "video_watched_2s", "video_watched_6s",
"average_video_play_per_user"],
"start_date": "2025-01-01",
"end_date": "2025-12-31",
"page_size": 50,
"page": 1,
}).json()
if report.get("code") != 0:
raise SystemExit(f"TikTok report error: {report.get('message')}")
rows = report.get("data", {}).get("list", [])
# 2. For each creative, fetch video and run analysis
leaderboard = []
for row in rows[:15]:
metrics = row.get("metrics", {})
dims = row.get("dimensions", {})
ad_id = dims.get("ad_id", "")
impressions = int(metrics.get("impressions", 0))
if impressions < 500:
continue
# Fetch ad details for video URL
ad_detail = requests.get(f"{TT_BASE}/ad/get/",
headers=TT_H,
params={"advertiser_id": ADV, "filtering": f'{{"ad_ids": ["{ad_id}"]}}',
"fields": '["video_id", "ad_name"]'}).json()
ad_info = (ad_detail.get("data", {}).get("list") or [{}])[0]
video_id = ad_info.get("video_id")
if not video_id:
continue
vid_info = requests.get(f"{TT_BASE}/file/video/ad/info/",
headers=TT_H,
params={"advertiser_id": ADV, "video_ids": f'["{video_id}"]'}).json()
video_url = (vid_info.get("data", {}).get("list") or [{}])[0].get("video_url", "")
if not video_url:
continue
vid_bytes = requests.get(video_url).content
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp:
tmp.write(vid_bytes); tmp_path = tmp.name
upload = requests.post(f"{MV_BASE}/assets",
headers={"Authorization": f"Bearer {MV}"},
files={"file": (f"ad_{ad_id}.mp4", open(tmp_path, "rb"), "video/mp4")}).json()
analysis = requests.post(f"{MV_BASE}/video-analysis", headers=MV_H, json={
"asset_id": upload["id"],
"analysis_types": ["hook_score", "emotional_arc", "pacing", "cognitive_load"],
}).json()
for _ in range(30):
time.sleep(3)
status = requests.get(f"{MV_BASE}/video-analysis/{analysis['id']}", headers=MV_H).json()
if status.get("status") == "completed": break
results = status.get("results", {})
hook = results.get("hook_score", {}).get("score", 0)
emotion = results.get("emotional_arc", {}).get("intensity_avg", 0)
ctr = float(metrics.get("ctr", 0))
behavioral_score = (hook * 0.4 + emotion * 6 * 0.3 + (10 - results.get("cognitive_load", {}).get("average", 5)) * 10 * 0.3)
leaderboard.append({
"ad_id": ad_id,
"ad_name": metrics.get("ad_name", ad_info.get("ad_name", "")),
"impressions": impressions,
"ctr": ctr, "spend": float(metrics.get("spend", 0)),
"hook_score": hook, "emotion_avg": emotion,
"behavioral_score": round(behavioral_score, 1),
"verdict": "SCALE" if behavioral_score > 60 and ctr > 1.5 else
"TEST" if behavioral_score > 60 else
"KILL" if behavioral_score < 40 and ctr < 1.0 else "HOLD",
})
os.unlink(tmp_path)
time.sleep(1)
# 3. Print leaderboard
leaderboard.sort(key=lambda x: -x["behavioral_score"])
print(f"{'Rank':<5} {'Ad Name':<30} {'Hook':<6} {'Behavioral':<12} {'CTR':<8} {'Spend':<10} {'Verdict'}")
print("-" * 85)
for i, lb in enumerate(leaderboard, 1):
print(f"{i:<5} {lb['ad_name'][:28]:<30} {lb['hook_score']:<6} {lb['behavioral_score']:<12} {lb['ctr']:.2f}%{'':<4} ${lb['spend']:>8,.0f} {lb['verdict']}")
const TT = process.env.TIKTOK_ACCESS_TOKEN;
const ADV = process.env.TIKTOK_ADVERTISER_ID;
const MV = process.env.MAVERA_API_KEY;
const TT_BASE = "https://business-api.tiktok.com/open_api/v1.3";
const MV_BASE = "https://app.mavera.io/api/v1";
const TT_H = { "Access-Token": TT };
const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
// 1. Pull integrated report
const report = await fetch(`${TT_BASE}/reports/integrated/get/`, {
method: "POST", headers: { ...TT_H, "Content-Type": "application/json" },
body: JSON.stringify({
advertiser_id: ADV, report_type: "BASIC", data_level: "AUCTION_AD",
dimensions: ["ad_id"],
metrics: ["ad_name", "impressions", "clicks", "ctr", "conversion",
"cost_per_conversion", "spend", "video_play_actions",
"video_watched_2s", "video_watched_6s", "average_video_play_per_user"],
start_date: "2025-01-01", end_date: "2025-12-31", page_size: 50, page: 1,
}),
}).then(r => r.json());
if (report.code !== 0) throw new Error(`TikTok: ${report.message}`);
const rows = report.data?.list || [];
// 2. Analyze each creative
const leaderboard = [];
for (const row of rows.slice(0, 15)) {
const metrics = row.metrics || {};
const adId = row.dimensions?.ad_id || "";
const impressions = parseInt(metrics.impressions || "0", 10);
if (impressions < 500) continue;
const adDetail = await fetch(
`${TT_BASE}/ad/get/?advertiser_id=${ADV}&filtering=${encodeURIComponent(`{"ad_ids":["${adId}"]}`)}` +
`&fields=${encodeURIComponent('["video_id","ad_name"]')}`,
{ headers: TT_H }
).then(r => r.json());
const videoId = adDetail.data?.list?.[0]?.video_id;
if (!videoId) continue;
const vidInfo = await fetch(
`${TT_BASE}/file/video/ad/info/?advertiser_id=${ADV}&video_ids=${encodeURIComponent(`["${videoId}"]`)}`,
{ headers: TT_H }
).then(r => r.json());
const videoUrl = vidInfo.data?.list?.[0]?.video_url;
if (!videoUrl) continue;
const vidBytes = await fetch(videoUrl).then(r => r.arrayBuffer());
const formData = new FormData();
formData.append("file", new Blob([vidBytes], { type: "video/mp4" }), `ad_${adId}.mp4`);
const upload = await fetch(`${MV_BASE}/assets`, {
method: "POST", headers: { Authorization: `Bearer ${MV}` }, body: formData,
}).then(r => r.json());
const analysis = await fetch(`${MV_BASE}/video-analysis`, {
method: "POST", headers: MV_H,
body: JSON.stringify({
asset_id: upload.id,
analysis_types: ["hook_score", "emotional_arc", "pacing", "cognitive_load"],
}),
}).then(r => r.json());
let status;
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 3000));
status = await fetch(`${MV_BASE}/video-analysis/${analysis.id}`, { headers: MV_H }).then(r => r.json());
if (status.status === "completed") break;
}
const results = status.results || {};
const hook = results.hook_score?.score || 0;
const emotion = results.emotional_arc?.intensity_avg || 0;
const cogLoad = results.cognitive_load?.average || 5;
const ctr = parseFloat(metrics.ctr || "0");
const behavioral = hook * 0.4 + emotion * 6 * 0.3 + (10 - cogLoad) * 10 * 0.3;
leaderboard.push({
ad_id: adId, ad_name: metrics.ad_name || "",
impressions, ctr, spend: parseFloat(metrics.spend || "0"),
hook_score: hook, emotion_avg: emotion,
behavioral_score: parseFloat(behavioral.toFixed(1)),
verdict: behavioral > 60 && ctr > 1.5 ? "SCALE" :
behavioral > 60 ? "TEST" :
behavioral < 40 && ctr < 1.0 ? "KILL" : "HOLD",
});
await new Promise(r => setTimeout(r, 1000));
}
// 3. Leaderboard
leaderboard.sort((a, b) => b.behavioral_score - a.behavioral_score);
console.log("Rank Ad Name Hook Behavioral CTR Spend Verdict");
console.log("-".repeat(85));
leaderboard.forEach((lb, i) =>
console.log(`${i+1} ${lb.ad_name.slice(0,28).padEnd(30)} ${String(lb.hook_score).padEnd(6)} ${String(lb.behavioral_score).padEnd(12)} ${lb.ctr.toFixed(2)}% $${lb.spend.toLocaleString().padStart(8)} ${lb.verdict}`)
);
Example Output
Rank Ad Name Hook Behavioral CTR Spend Verdict
-------------------------------------------------------------------------------------
1 UGC Creator — Summer Haul 91 82.4 2.31% $4,200 SCALE
2 Founder Story — Behind Scene 78 71.0 1.82% $2,100 SCALE
3 Product Demo — Cinematic 85 68.3 0.94% $800 TEST
4 Transition Edit — Before/Aft 62 55.1 1.45% $3,600 HOLD
5 Logo Intro — Brand Awareness 28 31.7 0.52% $8,400 KILL
Insights:
- "Product Demo — Cinematic" has strong behavioral score (68.3) but low spend.
Recommendation: Increase budget — behavioral metrics predict CTR upside.
- "Logo Intro — Brand Awareness" burns $8.4K with weak hook (28/100).
Recommendation: Kill or re-cut with UGC-style opening.
Error Handling
Report API pagination
Report API pagination
TikTok’s integrated report endpoint paginates at
page_size max 200. For large accounts, loop with page increments until page_info.total_number is exhausted.Video Analysis timeout
Video Analysis timeout
Long-form TikTok ads (60s+) may take 45s+ to analyze. The 90s poll loop handles this. For batch jobs with 30+ creatives, parallelize with
asyncio.gather or Promise.all.Score calibration
Score calibration
Behavioral score weights (hook 40%, emotion 30%, cognitive load 30%) are starting points. Calibrate against your own CTR data after 50+ creatives to build a custom regression model.