Welcome to TikTool Docs
Everything you need to connect to any TikTok LIVE stream - real-time WebSocket events, REST endpoints, and SDKs for Node.js, Python, Go, Java, C# and Rust. Start on the free Sandbox tier, no credit card.
SDKs
High-level clients for Node.js and Python. Stream chat and gifts in 20 lines, with built-in reconnect and live captions.
Browse SDKsREST API
Signed HTTP endpoints for live-state checks, room data and JWT minting. Tier-aware, with copy-paste examples in six languages.
Browse endpointsEvents
30+ real-time WebSocket events - chat, gifts, battles, likes, follows and more - with full field reference and payload shapes.
Browse eventsQuick Start
Connect to any TikTok LIVE stream and receive real-time events in under 5 lines of code. Demo key your_api_key works without signup.
npm install @tiktool/liveimport WebSocket from 'ws';
const ws = new WebSocket('wss://api.tik.tools?uniqueId=streamer_name&apiKey=YOUR_KEY');
ws.on('message', (data) => {
const event = JSON.parse(data);
if (event.event === 'chat') {
const lvl = event.data.user.level ?? event.data.user.payGrade ?? 0;
console.log(`${event.data.user.uniqueId} lv${lvl}: ${event.data.comment}`);
}
});
// Raw WS exposes the gifter/donator level as user.level.
// The @tiktool/live npm SDK exposes the same value as user.payGrade.
// Either present on every chat / gift / like / follow / member / share.Sign up at tik.tools/pricing to get a free API key. No credit card required.
Try It Now - Live Demo
Copy-paste this script, run it, and see real-time TikTok LIVE events in your terminal. Works on the free Sandbox tier - no credit card needed.
Replace YOUR_API_KEY with your free key from tik.tools and LIVE_USERNAME with any currently live TikTok user. Runs on the free Sandbox tier - no time limit.
// demo.mjs - TikTok LIVE in 5 minutes
// npm install ws
import WebSocket from 'ws';
const API_KEY = 'YOUR_API_KEY'; // Get free key → https://tik.tools
const LIVE_USERNAME = 'tv_asahi_news'; // Any live TikTok username
const ws = new WebSocket(wss://api.tik.tools?uniqueId=${LIVE_USERNAME}&apiKey=${API_KEY}`);
let events = 0;
ws.on('open', () => console.log(`\n✅ Connected to @${LIVE_USERNAME} - listening for 5 min...\n`));
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
events++;
const d = msg.data || {};
const user = d.user?.uniqueId || '';
// d.user.payGrade = sender's TikTok gifter/donator level (1-50).
// Exposed verbatim from proto on every user-bearing event.
const lvl = (d.user?.payGrade ?? d.user?.level) ? ` lv${d.user.payGrade ?? d.user.level}` : '';
switch (msg.event) {
case 'chat': console.log(`💬 ${user}${lvl}: ${d.comment}`); break;
case 'gift': console.log(`🎁 ${user}${lvl} sent ${d.giftName} (${d.diamondCount}💎)`); break;
case 'like': console.log(`❤️ ${user}${lvl} liked × ${d.likeCount}`); break;
case 'member': console.log(`👋 ${user}${lvl} joined`); break;
case 'roomUserSeq': console.log(`👀 Viewers: ${d.viewerCount}`); break;
case 'roomInfo': console.log(`📡 Room: ${msg.roomId}`); break;
default: console.log(`📦 ${msg.event}`); break;
}
});
ws.on('close', () => console.log(`\n📊 Done! Received ${events} events.\n`));
setTimeout(() => ws.close(), );Node.js SDK
The @tiktool/live npm package provides a high-level client and utility functions for working with TikTok LIVE data.
npm install @tiktool/liveConnection Modes
Two ways to receive TikTok LIVE events. Pick based on scale and whether you want TikTok to see your IP.
Mode 1 - Direct (default, via @tiktool/live SDK)
Your app handshakes with the TikTools sign server, then opens a WebSocket from your runtime to TikTok using the signed URL. The event stream never traverses our infrastructure.
- Best for low- to mid-volume workloads from a single host.
- Cost 1×
ws_credentialsrequest per (re)connect on your API key. - Egress uses your host's default network interface. If your environment already routes outbound traffic through a corporate gateway, set the optional
proxyoption.
import { TikTokLive } from '@tiktool/live';
const live = new TikTokLive({
uniqueId: 'creator_username',
apiKey: 'YOUR_TIKTOOLS_KEY',
// OPTIONAL: route outbound WS + HTTP through your corporate / egress gateway.
// Requires: npm install https-proxy-agent
// proxy: 'http://USER:PASS@gateway.your-company.com:port',
});
live.on('chat', e => console.log(`${e.user.uniqueId} [lv${e.user.payGrade ?? 0}]: ${e.comment}`));
live.on('gift', e => console.log(`${e.user.uniqueId} [lv${e.user.payGrade ?? 0}] sent ${e.giftName} (${e.diamondCount} diamonds)`));
// e.user.payGrade exposes the sender's TikTok gifter / donator level
// (1-50, higher = more coins spent platform-wide). Present on every
// user-bearing event: chat, gift, like, follow, member, share.
await live.connect();Mode 2 - Relayed (via TikTools managed edge)
The SDK connects to wss://api.tik.tools/.... The TikTools service handles the upstream TikTok session and forwards decoded events to your client over a single WebSocket. Your application talks only to api.tik.tools.
- Best for production at scale, multi-tenant deployments, environments that centralize outbound traffic.
- Cost 1× request per event forwarded (chat / gift / battle / etc.).
- Egress single, stable endpoint (
api.tik.tools).
Easiest - same SDK, one option flip (since v2.10.0):
import { TikTokLive } from '@tiktool/live';
const live = new TikTokLive({
uniqueId: 'creator_username',
apiKey: 'YOUR_KEY',
mode: 'relayed', // ← only change vs Direct mode
});
live.on('chat', e => console.log(`${e.user.uniqueId}: ${e.comment}`));
live.on('gift', e => console.log(`${e.user.uniqueId} sent ${e.giftName}`));
live.on('battleArmies', e => console.log(e));
await live.connect();Advanced - raw WebSocket (no SDK) for non-Node runtimes (Python, Go, Bun, browser):
import WebSocket from 'ws';
const ws = new WebSocket(
`wss://api.tik.tools/?uniqueId=creator_username&apiKey=YOUR_KEY`
);
ws.on('message', raw => {
const msg = JSON.parse(raw.toString());
// user.level is the raw-WS name for the same value the SDK exposes as
// user.payGrade. Both come from proto badge {category=20, sub=8}.
const lvl = msg.data.user.level ?? msg.data.user.payGrade ?? 0;
if (msg.event === 'chat') console.log(msg.data.user.uniqueId, `lv${lvl}`, msg.data.comment);
if (msg.event === 'gift') console.log(msg.data.user.uniqueId, `lv${lvl}`, msg.data.giftName, msg.data.diamondCount);
if (msg.event === 'battleArmies') console.log(msg.data);
});Event payloads are identical to Direct mode. Toggle modes by changing one line - your event handlers stay the same.
User object shape
Every user-bearing event (chat, gift, like, follow, member, share) carries the same user object. Fields surfaced verbatim from the TikTok proto:
| Field | Type | Description |
|---|---|---|
id | string | TikTok numeric user id. |
uniqueId | string | Public @handle (without the @). |
nickname | string | Display name. |
profilePictureUrl | string | Avatar URL (TikTok CDN). |
secUid | string? | TikTok's permanent secure user id. Always starts with MS4wLjAB (e.g. MS4wLjABAAAARsyn...). Survives a user changing their uniqueId handle, so it's the canonical key for long-term gifter identity across rename events. Empty when TikTok ships a lightweight User payload (rare; ~28% of gifts in production today carry it). Basic+ tier required - free / sandbox keys receive an upgrade-hint string. Also persisted on every gift in our ClickHouse store as gifter_sec_uid. |
isAnonymous | boolean? | True when TikTok flags the gifter as anonymous (their "Enigma" feature - paid gifts where the gifter hides identity). Empirically, anonymous gifts surface with nickname = "Enigma XXXXX" (5-char random session suffix, e.g. "Enigma TUYUL", "Enigma 91124") - the suffix changes per session, NOT per user. Use this flag (not nickname-matching) to detect anonymity. A small fraction (~0.6% in production) of Enigma gifts STILL leak the real secUid - if present, you can resolve the actual sender despite the masked nickname. |
badges | Array<{url, name, colors?}> | Active badges with full visual metadata. Each badge has:url (string) - PNG icon URL on TikTok CDN;name (string) - the label TikTok renders inside the gem (numeric like "30" for grade gems, text like "BEST" / "AURA" for fans badges, empty for subscriber / moderator icons);colors.primary (string?) - 8-char alpha-prefix ARGB hex (e.g. "#BF2A19EE") - the main gem fill color, ready to drop into CSS;colors.secondary (string?) - second ARGB hex when TikTok ships a gradient (e.g. "#FFFFBF83"). Use both to render the gem's gradient fill. Extracted from the proto CombineBadge color struct at field 11 / 12. |
payGrade / level | number? | Gifter / donator level (1-50). Higher = more coins spent platform-wide. Extracted from the proto badge category 20 / sub-category 8, priv field 12 -> level string field 5. The @tiktool/live npm SDK exposes it as user.payGrade; the raw managed-edge WebSocket (wss://api.tik.tools/?...) exposes the same value as user.level. Renders as the small tier-colored badge on user avatars in the Battles.app dashboard. Absent when the user has never gifted. Verified live values: e.g. rongnomber1 = 40, sagarshrestha500 = 22. |
Side-by-side comparison
| Aspect | Direct (Mode 1) | Relayed (Mode 2) |
|---|---|---|
| WebSocket opens from | Your runtime | TikTools managed edge |
| Setup | new TikTokLive(...) | new TikTokLive({ ..., mode: 'relayed' }) |
| Egress | Your host's network | Single endpoint: api.tik.tools |
| Recommended for | Low- to mid-volume | Production scale, multi-tenant |
Python SDK
The tiktok-live-api PyPI package provides a high-level Python client for TikTok LIVE streams - sync and async support, decorator-based event handling, and built-in live captions.
pip install tiktok-live-apiBasic Example
from tiktok_live_api import TikTokLive
client = TikTokLive("streamer_username")
@client.on("connected")
def on_connected(event):
print(f"Connected to @{event['uniqueId']}")
@client.on("chat")
def on_chat(event):
lvl = event['user'].get('payGrade', 0)
print(f"[chat] {event['user']['uniqueId']} lv{lvl}: {event['comment']}")
@client.on("gift")
def on_gift(event):
diamonds = event.get("diamondCount", 0)
lvl = event['user'].get('payGrade', 0)
print(f"[gift] {event['user']['uniqueId']} lv{lvl} sent {event['giftName']} ({diamonds} diamonds)")
# event['user']['payGrade'] = senders gifter/donator level (1-50). On every event.
@client.on("like")
def on_like(event):
print(f"[like] {event['user']['uniqueId']} liked (total: {event['totalLikes']})")
@client.on("follow")
def on_follow(event):
print(f"[follow] {event['user']['uniqueId']} followed")
@client.on("roomUserSeq")
def on_viewers(event):
print(f"[viewers] {event['viewerCount']} watching")
client.run()Live Captions Example
from tiktok_live_api import TikTokCaptions
captions = TikTokCaptions(
"streamer_username",
translate="en",
diarization=True,
)
@captions.on("connected")
def on_connected(event):
print(f"Listening to @{event['uniqueId']}")
@captions.on("caption")
def on_caption(event):
speaker = event.get("speaker", "")
text = event["text"]
is_final = event.get("isFinal", False)
status = "FINAL" if is_final else "partial"
print(f"[{status}] [{speaker}] {text}")
@captions.on("translation")
def on_translation(event):
print(f" -> {event['text']}")
captions.run()TikTokLiveThe main SDK class for connecting to TikTok LIVE streams via WebSocket. Handles authentication, reconnection, and event parsing automatically.
new TikTokLive({ uniqueId, apiKey, ... })
Parameters
| Name | Type | Description |
|---|---|---|
uniqueId | string | TikTok username (without @) |
apiKey | string | Your TikTool API key |
sessionId | string? | Optional TikTok session ID for authenticated features |
Returns
TikTokLive instance
Example
import { TikTokLive } from '@tiktool/live';
const client = new TikTokLive({
uniqueId: 'streamer_name',
apiKey: process.env.TIKTOOL_API_KEY
});
client.on('chat', (event) => {
console.log(`${event.nickname}: ${event.comment}`);
});
client.on('gift', (event) => {
console.log(`${event.nickname} sent ${event.giftName} x${event.repeatCount}`);
});
await client.connect();
// client.disconnect() to closecallApiHigh-level utility that handles the full sign-and-return API flow automatically: resolves room ID → calls API → fetches signed URL → returns parsed TikTok data. Ideal for server-side integrations.
callApi(options: CallApiOptions): Promise<any>
Parameters
| Name | Type | Description |
|---|---|---|
apiKey | string | Your TikTool API key |
endpoint | string | API endpoint path (e.g. '/webcast/room_info') |
uniqueId | string | TikTok username to query |
method | 'GET'|'POST' | HTTP method (default: POST) |
serverUrl | string? | API server URL (default: https://api.tik.tools) |
extraBody | Record? | Additional body fields for POST requests |
Returns
Parsed TikTok data object or null if user is not live
Example
import { callApi } from '@tiktool/live';
// Get room info
const roomInfo = await callApi({
apiKey: 'YOUR_KEY',
endpoint: '/webcast/room_info',
uniqueId: 'streamer_name'
});
console.log(roomInfo?.data?.title);
// Get stream video URLs
const video = await callApi({
apiKey: 'YOUR_KEY',
endpoint: '/webcast/room_video',
uniqueId: 'streamer_name'
});
console.log(video?.data?.stream_urls?.origin?.hls);resolveLivePageRetrieves metadata from a TikTok live page including the room ID, session cookie (ttwid), and cluster region. Results are cached for 5 minutes. Used internally by TikTokLive.connect().
resolveLivePage(uniqueId: string): Promise<LivePageInfo | null>
Parameters
| Name | Type | Description |
|---|---|---|
uniqueId | string | TikTok username (with or without @) |
Returns
LivePageInfo { roomId, ttwid, clusterRegion } or null
Example
import { resolveLivePage } from '@tiktool/live';
const info = await resolveLivePage('streamer_name');
if (info) {
console.log('Room ID:', info.roomId);
console.log('Region:', info.clusterRegion);
console.log('Cookie:', info.ttwid);
} else {
console.log('User is not live');
}resolveRoomIdResolves a TikTok username to a numeric room ID. Thin wrapper around resolveLivePage() - returns just the room ID string. Results are cached for 5 minutes.
resolveRoomId(uniqueId: string): Promise<string | null>
Parameters
| Name | Type | Description |
|---|---|---|
uniqueId | string | TikTok username (with or without @) |
Returns
Room ID string or null if not live
Example
import { resolveRoomId } from '@tiktool/live';
const roomId = await resolveRoomId('streamer_name');
if (roomId) {
console.log('Room ID:', roomId);
// Use with REST API endpoints that require room_id
}fetchSignedUrlExecutes a signed-URL response from the API server. When the API returns action: "fetch_signed_url", pass the response to this function to fetch the actual TikTok data. Used internally by callApi().
fetchSignedUrl(response: SignedUrlResponse): Promise<any>
Parameters
| Name | Type | Description |
|---|---|---|
response | SignedUrlResponse | The API response containing signed_url, headers, and cookies |
Returns
Parsed JSON data from TikTok
Example
import { fetchSignedUrl } from '@tiktool/live';
// Low-level usage (callApi handles this automatically)
const apiResponse = await fetch(
'https://api.tik.tools/webcast/room_info?apiKey=KEY',
{ method: 'POST', body: JSON.stringify({ room_id: '123' }) }
).then(r => r.json());
if (apiResponse.action === 'fetch_signed_url') {
const tiktokData = await fetchSignedUrl(apiResponse);
console.log(tiktokData);
}Authentication
All API requests require authentication via an API key or JWT token.
API Key
Pass your API key as a query parameter or header:
# Query parameter
GET /webcast/check_alive?apiKey=YOUR_KEY&unique_id=username
# Header
GET /webcast/check_alive?unique_id=username
x-api-key: YOUR_KEYJWT Token
For frontend WebSocket connections, generate a JWT token server-side and pass it as jwtKey:
wss://api.tik.tools?uniqueId=username&jwtKey=YOUR_JWTJWTs can be scoped to specific creators and have configurable expiry. See /authentication/jwt.
WebSocket Connection
Connect via WebSocket to receive real-time events from any TikTok LIVE stream.
Connection URL
wss://api.tik.tools?uniqueId=USERNAME&apiKey=YOUR_KEYMessage Format
Events are sent as JSON with the following structure:
{ "event": "chat", "data": { "type": "chat", "user": { ... }, "comment": "Hello!" } }First Message
Upon connection, the server sends a roomInfo event:
{ "event": "roomInfo", "roomId": "7123456789", "uniqueId": "streamer", "connectedAt": "..." }import WebSocket from 'ws';
const ws = new WebSocket('wss://api.tik.tools?uniqueId=streamer&apiKey=YOUR_KEY');
ws.on('open', () => console.log('Connected!'));
ws.on('message', (raw) => {
const { event, data } = JSON.parse(raw);
// data.user.level (raw WS) / data.user.payGrade (SDK) = gifter/donator
// level (1-50). Always on the wire when the sender has gifted before.
const lvlVal = data?.user?.level ?? data?.user?.payGrade ?? 0;
const lvl = lvlVal ? ` lv${lvlVal}` : '';
switch (event) {
case 'roomInfo': console.log('Room:', data.roomId); break;
case 'chat': console.log(`${data.user.uniqueId}${lvl}:`, data.comment); break;
case 'gift': console.log(`${data.user.uniqueId}${lvl}`, 'sent', data.giftName); break;
case 'like': console.log(`${data.user.uniqueId}${lvl}`, 'liked ×' + data.likeCount); break;
case 'member': console.log(`${data.user.uniqueId}${lvl}`, 'joined'); break;
}
});
ws.on('close', (code, reason) => console.log('Closed:', code, reason.toString()));REST API Reference
HTTP endpoints for signing, fetching room data, and managing authentication.
/webcast/sign_urlSign any TikTok webcast URL with the required X-Bogus and X-Gnarly anti-automation parameters. This is the foundation of the API - TikTok rejects unsigned requests, so this endpoint adds the cryptographic signatures needed for valid requests.
How it works: Send any raw TikTok webcast URL in the request body. The API returns the signed URL with all required parameters appended, plus the User-Agent and cookies you must use when fetching.
Use cases: Building custom TikTok integrations, signing WebSocket URLs for direct connections, or signing any TikTok API endpoint that requires X-Bogus validation.
Response data: Returns signed_url (the URL with signatures), x_bogus, x_gnarly, user_agent, and cookies fields. Always use the returned User-Agent when making the final request to TikTok.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The TikTok URL to sign |
Request
const res = await fetch('https://api.tik.tools/webcast/sign_url?apiKey=YOUR_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: 'https://webcast.tiktok.com/...' })
});
const data = await res.json();
console.log(data.data.signed_url);Response
{ "status_code": 0, "data": { "signed_url": "...", "x_bogus": "...", "x_gnarly": "...", "user_agent": "...", "cookies": "..." } }/webcast/check_aliveCheck if one or more TikTok users are currently live streaming. Returns live status, room ID, stream title, and viewer count for each user.
Lookup methods: Pass unique_id (username) for single-user lookups, or room_ids (comma-separated) to check multiple rooms at once. At least one parameter is required.
Response data: Each entry includes room_id, alive (boolean), title, and userCount. Use this to build monitoring dashboards, trigger alerts when creators go live, or validate a stream before connecting via WebSocket.
Performance: This endpoint is lightweight and cached - ideal for polling. Combine with bulk_live_check for monitoring large lists of creators.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
room_ids | string | No | Comma-separated room IDs |
unique_id | string | No | TikTok username (without @) |
Request
const res = await fetch('https://api.tik.tools/webcast/check_alive?apiKey=YOUR_KEY&unique_id=username');
const { data } = await res.json();
console.log(data[0].alive ? 'Live!' : 'Offline');Response
{ "status_code": 0, "data": [{ "room_id": "...", "alive": true, "title": "...", "userCount": 1234 }] }/webcast/live_statusInstant zero-latency liveness check sourced from the relay's internal state - no TikTok API call required. Use this before opening a WebSocket connection to avoid connecting to channels that are offline.
How it works: Checks the relay's Dragonfly cache for any channel currently connected to the relay or recently seen by the leaderboard poller (90s TTL). Returns a result in <5ms.
Limitation: Only covers channels actively tracked by the relay or the live leaderboard. If a channel is live but has never been connected to the relay, it may return alive: false. For a definitive check, use /webcast/check_alive.
Best practice: Use live_status as a fast pre-flight check. Only fall back to check_alive (which calls TikTok) when live_status returns offline for a channel you expect to be live.
Single channel per request. To check many creators in one call, use bulk_live_check (POST with a unique_ids array) - it returns is_live + room_id for each. Do NOT fire many live_status calls in parallel from one server IP; that trips upstream burst throttling and they time out. Batch with bulk_live_check (e.g. 50/call on Pro) and poll every couple of minutes.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
uniqueId | string | Yes | TikTok username (single, no @ prefix). Accepts uniqueId or unique_id. For many creators, use bulk_live_check. |
Request
// Single channel (use bulk_live_check for many - do NOT loop live_status in parallel)
const res = await fetch('https://api.tik.tools/webcast/live_status?apiKey=YOUR_KEY&uniqueId=username');
const { data } = await res.json();
if (data.is_live) {
// Open WebSocket immediately - relay already has this room
const ws = new WebSocket(wss://api.tik.tools?uniqueId=username&apiKey=YOUR_KEY`);
}
// Many creators? ONE batched call instead of parallel live_status:
const bulk = await fetch('https://api.tik.tools/webcast/bulk_live_check?apiKey=YOUR_KEY', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_ids: ['user1', 'user2', 'user3'] })
});
const live = (await bulk.json()).data.filter(c => c.is_live);Response
{ "status_code": 0, "data": { "unique_id": "username", "is_live": true, "room_id": "7637157...", "cached": false } }/webcast/rate_limitsCheck your current API key's rate limit status, tier information, and remaining quota across all features.
What's returned: Your current tier (free/pro/ultra), API request limits (per-minute sliding window), WebSocket connection limits, bulk check capacity, and feed daily quota. Also includes reset_at timestamp for when limits refresh.
Use cases: Monitor quota consumption in production, display remaining capacity in dashboards, or implement client-side throttling to avoid hitting limits.
Headers: Every API response also includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers - but this endpoint gives a comprehensive overview of all your limits in one call.
Request
const res = await fetch('https://api.tik.tools/webcast/rate_limits?apiKey=YOUR_KEY');
const { data } = await res.json();
console.log(`${data.api.remaining}/${data.api.limit} requests remaining`);Response
{ "status_code": 0, "data": { "tier": "basic", "api": { "limit": 30, "remaining": 28, "reset_at": 1234567890 }, "websocket": { "limit": 10, "current": 2 } } }/webcast/fetchFetch live stream events via HTTP long-polling. Returns the same real-time data as WebSocket (chat, gifts, likes, battles) but over HTTP. Uses TikTok's binary protobuf protocol and returns decoded events.
How it works: The API signs and fetches TikTok's internal polling endpoint, decodes the protobuf response, and returns structured event data. Pass a cursor from the previous response for continuous polling.
Identification: Provide either unique_id (username) or room_id. If using unique_id, the server resolves the room ID automatically.
WebSocket vs Fetch: WebSocket connections are preferred for real-time use - they deliver events instantly with lower latency. Use /webcast/fetch when WebSocket is not possible (serverless environments, HTTP-only infrastructure) or for one-off data snapshots.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
unique_id | string | No | TikTok username |
room_id | string | No | Room ID (alternative to unique_id) |
cursor | string | No | Pagination cursor from previous response |
Request
const res = await fetch('https://api.tik.tools/webcast/fetch?apiKey=YOUR_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: 'username' })
});
const { data } = await res.json();
console.log(`Messages: ${data.message_count}`);Response
{ "status_code": 0, "data": { "room_id": "...", "alive": true, "message_count": 5, "raw_data": "base64...", "cursor": "" } }/webcast/room_infoGet comprehensive information about a live stream room including the host's profile, stream title, viewer count, start time, and stream configuration.
Response data: Returns owner profile (nickname, uniqueId, profilePictureUrl, follower count), room metadata (title, user_count, like_count, create_time), stream URLs, cover images, and room status.
Identification: Provide either unique_id (username) or room_id. Returns an error if the user is not currently live.
Use cases: Pre-flight checks before WebSocket connection, building stream info cards/embeds, monitoring dashboards, or collecting the room ID for use with other endpoints like rankings or gift_info.
Server-side / datacenter IPs - use mode: "fetch" (Pro+): By default this endpoint returns a signed_url you fetch yourself. TikTok blocks datacenter egress to its webcast host, so that fetch returns an empty body from a server. Instead pass mode: "fetch" with a room_id and TikTool fetches TikTok on our infrastructure and returns the parsed JSON directly - user_count (live viewers), total_user, title, like_count, live_duration_seconds, owner, status - and it works from any datacenter IP. mode: "fetch" needs a room_id, so resolve it first server-side with live_status (GET /webcast/live_status?unique_id=<handle> returns is_live + room_id), then call room_info with { "room_id": "...", "mode": "fetch" }. No page-scraping, no signed_url, no residential IP needed. (title may be empty if the creator set none; total_user may be null.)
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
unique_id | string | No | TikTok username (resolves to room_id; for mode=fetch resolve via live_status first) |
room_id | string | No | Room ID (required when mode=fetch) |
mode | string | No | Set to "fetch" (Pro+) to have TikTool fetch + parse the room data server-side and return user_count/title/total_user directly - works from datacenter IPs. Requires room_id. Without it you get a signed_url you must fetch yourself (browser/residential only). |
Request
const res = await fetch('https://api.tik.tools/webcast/room_info?apiKey=YOUR_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: 'username' })
});
const { data } = await res.json();
console.log(`${data.title} - ${data.user_count} viewers`);Response
{ "status_code": 0, "data": { "room_id": "...", "alive": true, "title": "...", "user_count": 500, "owner": {...}, "like_count": 1234, "share_count": 56 } }
// With mode:"fetch" (Pro+, server-side, datacenter-friendly):
{ "status_code": 0, "mode": "fetch", "data": { "room_id": "...", "status": 2, "user_count": 2924, "total_user": null, "title": "...", "like_count": 0, "live_duration_seconds": 13362, "owner": { "nickname": "...", "display_id": "..." } } }/webcast/room_videoGet live stream video playback URLs in multiple formats and quality levels. Returns HLS (.m3u8), FLV, and direct origin URLs suitable for embedding or processing.
Stream formats: hls_pull_url for adaptive bitrate (best for web players), flv_pull_url for low-latency playback, and origin URLs for the highest quality source stream. Multiple resolutions are available (SD, HD, FULL_HD).
Use cases: Building custom live stream players, recording/archiving streams, feeding audio to transcription services (used internally by our Live Captions feature), or creating multi-stream monitoring walls.
Important: Stream URLs are time-limited and will expire. Re-fetch periodically if you need to maintain long-running playback. The hls format provides the most reliable playback across browsers and devices.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
unique_id | string | No | TikTok username |
room_id | string | No | Room ID |
Request
const res = await fetch('https://api.tik.tools/webcast/room_video?apiKey=YOUR_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: 'username' })
});
const { data } = await res.json();
const hlsUrl = data.stream_urls?.origin?.hls;Response
{ "status_code": 0, "data": { "room_id": "...", "alive": true, "stream_urls": { "origin": { "hls": "...", "flv": "..." }, "sd": {...} }, "default_quality": "origin" } }/webcast/bulk_live_checkCheck live status for multiple TikTok users in a single request. Returns the same data as check_alive but for up to 100 users simultaneously.
Batch limits: Free: 1 user, Pro: up to 50 users, Ultra: up to 100 users per request. Pass usernames as a JSON array in the request body.
Response data: Returns an array with each user's unique_id, room_id, alive status, title, and userCount. Users who are offline return alive: false with an empty room_id.
Use cases: Creator monitoring dashboards that track dozens of streamers, automated alert systems, multi-stream aggregators, or periodically scanning talent rosters to detect who's live.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
unique_ids | string[] | Yes | Array of TikTok usernames (or use "room_ids" array of room IDs) |
Request
const res = await fetch('https://api.tik.tools/webcast/bulk_live_check?apiKey=YOUR_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_ids: ['user1', 'user2', 'user3'] })
});
const { data } = await res.json();
data.forEach(row => console.log(`${row.unique_id}: ${row.is_live ? 'LIVE' : 'offline'} (room ${row.room_id})`));Response
{ "status_code": 0, "data": [
{ "unique_id": "user1", "is_live": true, "alive": true, "room_id": "7234567890123456789" },
{ "unique_id": "user2", "is_live": false, "alive": false, "room_id": "7234567890123456790" },
{ "unique_id": "user3", "is_live": true, "alive": true, "room_id": "7234567890123456791" }
] }/webcast/ws_credentialsGet pre-signed WebSocket connection credentials for establishing a direct connection to TikTok's live stream WebSocket servers. Returns everything needed to connect without using the managed relay.
What's returned: A signed websocket_url (with X-Bogus), cookies (ttwid, session), user_agent, room_id, and cluster_region. Use these to create your own WebSocket connection directly to TikTok.
When to use: Advanced users building custom WebSocket clients who need direct TikTok connections (without using our managed relay). Most users should use the managed WebSocket connection (wss://api.tik.tools) or the Node.js SDK instead.
Important: Direct connections count toward TikTok's per-IP WebSocket limit (~4 concurrent). If you need more, use our managed relay which distributes connections across multiple IPs.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
unique_id | string | Yes | TikTok username |
Request
const res = await fetch('https://api.tik.tools/webcast/ws_credentials?apiKey=YOUR_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: 'username' })
});
const { data } = await res.json();
console.log('WS URL:', data.ws_url);Response
{ "status_code": 0, "data": { "ws_url": "wss://...", "cookies": "ttwid=...", "room_id": "...", "ws_host": "...", "cluster_region": "US_TTP" } }/webcast/sign_websocketSign a TikTok WebSocket URL with X-Bogus parameters for direct connection to TikTok's webcast-ws servers. This is the WebSocket-specific equivalent of sign_url.
How it works: Provide a room_id and the API generates a fully signed WebSocket URL targeting the correct regional TikTok WebSocket server (webcast-ws.tiktok.com, webcast-ws.us.tiktok.com, etc.).
Response: Returns signed_url (the complete wss:// URL), cookies, and user_agent. Connect using these credentials with the provided cookies in the WebSocket handshake headers.
Use case: Building custom WebSocket clients in languages without our SDK, or when you need fine-grained control over the WebSocket lifecycle. The SDK handles this internally - most users won't need this endpoint directly.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
room_id | string | Yes | TikTok room ID |
cursor | string | No | Pagination cursor |
Request
const res = await fetch('https://api.tik.tools/webcast/sign_websocket?apiKey=YOUR_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ room_id: '7123456789' })
});
const { data } = await res.json();
// Connect with: new WebSocket(data.signed_url)Response
{ "status_code": 0, "data": { "signed_url": "wss://...", "x_bogus": "...", "x_gnarly": "...", "user_agent": "...", "cookies": "..." } }/webcast/resolve_user_idsResolve numeric TikTok user IDs to their corresponding usernames (unique_id/display_id). Accepts up to 20 user IDs per request with server-side caching for fast lookups.
When you need this: TikTok's internal APIs and WebSocket events often send numeric user IDs (like 6892636847263982593) instead of readable usernames. This endpoint converts them back to @username format.
Caching: Results are cached server-side for 24 hours. Repeated lookups for the same user IDs are instant and don't count toward rate limits.
Response data: Returns a data object keyed by the userId, each value { userId, username, nickname, avatarUrl, cached }. Users whose IDs can't be resolved (deleted/banned accounts) come back with username/nickname/avatarUrl all null. This is the canonical way to turn a numeric userId from a WS event (e.g. the co-host id in a linkMic frame's extras["5"]) into a readable profile.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
user_ids | string[] | Yes | Array of numeric user IDs (max 20) |
Request
const res = await fetch('https://api.tik.tools/webcast/resolve_user_ids?apiKey=YOUR_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_ids: ['107955', '6789012345'] })
});
const { data } = await res.json();
Object.values(data).forEach(u => console.log(`${u.userId} → @${u.username}`));Response
{ "status_code": 0, "data": { "7529001445733385224": { "userId": "7529001445733385224", "username": "cirelieazap", "nickname": "Eric_2.0", "avatarUrl": "https://...", "cached": false } } }/webcast/user_profilePRO+Reverse lookup: unique_id (the @username) → numeric TikTok user ID + full public profile metadata.
When you need this:
- You have a TikTok username and need the numeric user ID (e.g. for cross-referencing with chat/gift events that ship userId)
- You want bio, follower/following counts, video count, verification status, avatar URLs
- You need the secUid (TikTok's signed user identifier, required for some downstream APIs)
Caching: Profile results cached server-side for 24h. Repeated lookups for the same unique_id are instant and don't burn quota. Pass nocache=1 to bypass (useful for bio-verification flows where the user just updated their profile).
Returns:
- id - numeric TikTok user ID
- uniqueId - @username
- secUid - signed user identifier (used internally by TikTok)
- nickname - display name
- signature - bio text
- bioLink - the creator's native link-in-bio (URL); empty string if they haven't set one. bioLinkRisk is TikTok's link-risk score. Resolved server-side, so it works from a datacenter IP. (Some creators paste their link into the bio text instead - that's in signature.)
- verified - boolean blue-check flag
- avatarThumb / avatarMedium / avatarLarger - three avatar resolutions
- stats - followerCount, followingCount, heartCount, videoCount
- league - current Diamond Rush league standing (see below)
League field (Ultra+ only):
Every response includes a top-level league object with the user's current Diamond Rush class + rank. Ultra and Global Agency tiers receive the real class label (A1/A2/D5/etc.) + region + rank. Pro and lower tiers receive an upgrade prompt.
- Ultra+: { found: true, region: 'IT+', classType: 100, classLabel: 'D5', rank: 1 }
- Pro/Basic: { found: true, available_on: 'ultra+', message: 'Upgrade to Ultra or Global for Rankings', upgrade_url: 'https://tik.tools/pricing' }
- Not in any league: { found: false, region: null, classType: null, classLabel: null, rank: null }
League snapshots refresh every ~60s from the leaderboard poller; per-user lookup is cached server-side for 5 minutes.
Tier limits: Pro 1,000/day, Ultra 10,000/day, Admin unlimited. Sub-Pro tiers receive 403.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
unique_id | string | Yes | TikTok @username (no leading @) |
nocache | 0 | 1 | No | Set to 1 to bypass cache (useful after profile updates) |
Request
Response
{
"status_code": 0,
"data": {
"profile": {
"id": "7355610677036581896",
"uniqueId": "dalga.ahmedov",
"secUid": "MS4wLjABAAAA...",
"nickname": "🤍DENIZ🖤🌊",
"signature": "",
"bioLink": "battles.app",
"bioLinkRisk": 3,
"verified": false,
"avatarThumb": "https://...",
"avatarMedium": "https://...",
"avatarLarger": "https://...",
"stats": { "followerCount": 12345, "followingCount": 250, "heartCount": 980000, "videoCount": 142 }
},
"league": {
"found": true,
"region": "IT+",
"classType": 100,
"classLabel": "D5",
"rank": 1
},
"cached": false,
"usage": { "profile_lookups_used": 47, "profile_lookups_limit": 1000 }
}
}/authentication/jwtGenerate a JWT (JSON Web Token) for secure frontend WebSocket connections. JWTs allow your client-side code to connect without exposing your API key.
Security model: Generate JWTs server-side using your API key, then pass the jwtKey to your frontend. The JWT can be scoped to specific creators (allowed_creators) and limited to a set number of concurrent connections.
Expiry: Tokens expire after expire_after seconds (default: 3600 = 1 hour). After expiry, the client must request a new token from your backend.
Use cases: Web applications where the frontend connects directly to wss://api.tik.tools?uniqueId=USERNAME&jwtKey=TOKEN. This keeps your API key on the server while letting browsers connect to the WebSocket.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
expire_after | number | No | Seconds until expiry (default: 3600) |
allowed_creators | string[] | No | Restrict to specific creators |
max_websockets | number | No | Max concurrent WS connections (default: 1) |
Request
const res = await fetch('https://api.tik.tools/authentication/jwt?apiKey=YOUR_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ expire_after: 3600, allowed_creators: ['streamer1'] })
});
const { data } = await res.json();
// Use data.token for WebSocket: wss://api.tik.tools?jwtKey=TOKENResponse
{ "status_code": 0, "data": { "token": "eyJ..." } }/webcast/rankingsGet real-time leaderboard data for a live room - top gifters, online audience rankings, and engagement scores. Uses the "sign-and-return" pattern for authenticated data.
Data available: In-room top gifters (online_audience - ranked by diamonds sent this session), all-time gifter leaderboard (anchor_rank_list), and entrance configuration (ranking tabs, timer info, class/league badges).
Authentication: For full leaderboard data, include your TikTok session cookies via x-cookie-header. Without authentication, the endpoint returns limited ranking data.
Response pattern: Returns a signed_url that you fetch with session cookies to get TikTok's ranking data. Each rank entry includes user (nickname, avatar, follower count), score (diamonds), and rank position.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
room_id | string | No | TikTok room ID |
unique_id | string | No | TikTok username (alternative to room_id) |
Request
const res = await fetch('https://api.tik.tools/webcast/rankings?unique_id=username&apiKey=YOUR_KEY');
const { data } = await res.json();
console.log(data.rankings);Response
{ "status_code": 0, "data": { "room_id": "...", "rankings": [...] } }/api/leaderboards/leagues/:regionULTRA+List all available league classes (A1, A2, B5, C1, D1, etc.) discovered for a region.
What are leagues? TikTok groups LIVE creators into competitive classes based on earnings tier. Each class (A1 = top earners, D2 = entry level) has its own leaderboard. Agencies use this to identify which class a creator competes in without manually checking TikTok.
Access: Ultra developer tier or Agency subscription only. Non-qualifying users see league tab names but not creator data.
Data source: Updated every 5 minutes by the leaderboard poller via live anchor probing. Regions with active streams populate faster.
Response: Array of { classType, classLabel, count, updatedAt } - one entry per discovered class.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
region | string | Yes | Region code (BK=Balkans, BC=Baltics, US+=North America, UK+=United Kingdom, etc.) |
Request
const res = await fetch('https://tik.tools/api/leaderboards/leagues/BK', {
headers: { 'x-api-key': 'YOUR_ULTRA_API_KEY' }
});
const { leagues } = await res.json();
leagues.forEach(l => console.log(l.classLabel, l.count));Response
{
"region": "BK",
"available": true,
"leagues": [
{ "classType": 2000, "classLabel": "A1", "count": 87, "updatedAt": 1715000000000 },
{ "classType": 1900, "classLabel": "A2", "count": 92, "updatedAt": 1715000000000 },
{ "classType": 1000, "classLabel": "C1", "count": 78, "updatedAt": 1715000000000 }
]
}/api/leaderboards/league/:region/:classTypeULTRA+Get the ranked creator list for a specific league class within a region.
Full access (Ultra + Agency): All entries (up to 99) with nickname, avatar, score, rank, and live status.
Teaser (all other tiers): 5 randomly sampled entries only.
classType values: A1=2000, A2=1900, B5=1100, C1=1000, C2=900, D1=500, D2=400, D3=300. Fetch available classTypes from /api/leaderboards/leagues/:region first.
Live enrichment: Each entry includes isLive and roomId if the creator is currently streaming.
Agency use case: Identify which A1/A2 creators are active in a region for talent scouting without manually checking dozens of profiles.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
region | string | Yes | Region code (BK, BC, US+, etc.) |
classType | number | Yes | Class type integer (2000=A1, 1900=A2, 1000=C1, etc.) |
Request
const headers = { 'x-api-key': 'YOUR_ULTRA_API_KEY' };
// Step 1: list available leagues
const { leagues } = await fetch(
'https://tik.tools/api/leaderboards/leagues/BK',
{ headers }
).then(r => r.json());
// Step 2: fetch A1 entries
const a1 = leagues.find(l => l.classLabel === 'A1');
const { entries } = await fetch(
https://tik.tools/api/leaderboards/league/BK/${a1.classType}`,
{ headers }
).then(r => r.json());
console.log('A1 top creator:', entries[0].nickname);Response
{
"region": "BK",
"classType": 2000,
"classLabel": "A1",
"hasFullAccess": true,
"totalCount": 87,
"liveCount": 12,
"maskedCount": 0,
"entries": [
{
"rank": 1,
"userId": "6841523190294479361",
"uniqueId": "creator_username",
"nickname": "Creator Name",
"avatar": "https://...",
"score": 1250000,
"isLive": true,
"roomId": "7123456789012345678"
}
]
}
`userId` is TikTok's stable numeric backend ID (never changes). `uniqueId` is the @-handle (user-changeable). `nickname` is the display name (user-changeable)./webcast/room_idResolve a TikTok username to their current live room ID. The server resolves the unique_id to a room_id on your behalf - no client-side page parsing needed.
How it works: The server checks its cache first (30-minute TTL). Resolution falls back to a live lookup on cache miss. Reliability varies by tier - Pro/Ultra/Admin requests use a higher-priority resolution path.
Freshness guarantee (Pro+): If a cached room ID is found but the user is no longer live on that room, the server automatically invalidates all cache layers and re-resolves. This ensures you always get the current room ID, even if a streamer went offline and started a new stream (which creates a new room ID). When this happens, the response includes stale_fix: true.
Response data: Returns unique_id, room_id, alive (whether the user is currently live), cached (whether the result came from server cache), and optionally stale_fix (if a stale cached room was detected and re-resolved). Use the returned room_id with other endpoints like sign_websocket, rankings, gift_info, etc.
Use cases: When you only have a username and need the room_id for other API calls. Many endpoints accept unique_id directly, but some (like sign_websocket) require the room ID. This endpoint eliminates the need for client-side TikTok page parsing. Safe to poll every 5-10 minutes - cached results are instant and re-resolution only triggers when needed.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
unique_id | string | Yes | TikTok username (without @) |
Request
const res = await fetch('https://api.tik.tools/webcast/room_id?apiKey=YOUR_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_id: 'username' })
});
const { data } = await res.json();
console.log(`Room ID: ${data.room_id}, Live: ${data.alive}`);Response
{ "status_code": 0, "data": { "unique_id": "username", "room_id": "7123456789", "alive": true, "cached": false, "stale_fix": false } }/webcast/room_coverGet the high-quality cover image (thumbnail) URL of an active live stream. Returns the same image TikTok displays on the live feed before a user taps to watch.
Image quality: Returns the highest resolution available from TikTok's CDN. Cover images are typically 720x960 or higher, served as JPEG/WebP from TikTok's image CDN.
Use cases: Building live stream galleries, social media embeds, notification cards with stream previews, or monitoring dashboards that show stream thumbnails alongside viewer counts.
Note: Cover images are set by the streamer before going live. If no custom cover is set, TikTok uses a default or auto-generated thumbnail. Returns an error if the user is not currently live.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
unique_id | string | No | TikTok username |
room_id | string | No | TikTok room ID |
Request
const res = await fetch('https://api.tik.tools/webcast/room_cover?unique_id=username&apiKey=YOUR_KEY');
const { data } = await res.json();
console.log(data.cover_url);Response
{ "status_code": 0, "data": { "room_id": "...", "cover_url": "https://..." } }/webcast/hashtag_listGet currently trending live stream hashtags from TikTok. Returns hashtag names, live viewer counts, and associated metadata.
Response data: Each hashtag includes id, title (the hashtag text), user_count (total viewers across all streams using this hashtag), and related metadata. Results are sorted by popularity.
Use cases: Discover trending topics for content strategy, build hashtag explorers, analyze live streaming trends over time, or filter the feed by popular hashtags.
Pagination: Use the count parameter to control results per page (default: 20, max: 50). The endpoint uses TikTok's internal trending algorithm, so results update frequently as trending topics shift.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
count | number | No | Number of results (default: 20, max: 50) |
Request
const res = await fetch('https://api.tik.tools/webcast/hashtag_list?count=10&apiKey=YOUR_KEY');
const { data } = await res.json();
data.forEach(tag => console.log(tag.title));Response
{ "status_code": 0, "data": [{ "id": "...", "title": "#gaming", "user_count": 15000 }, ...] }/webcast/gift_infoGet the global TikTok gift catalog: every gift name, diamond cost, icon URL, ID. The catalog is room-independent - call the endpoint with no params, get the catalog back. Tier-branched response:
- Sandbox / Basic / Pro → returns a static 50-gift SAMPLE so you can build + test your UI against the real schema without paying. Shape: { status_code, source: "sample", is_sample: true, count: 50, data: { gifts: [...] }, your_tier, upgrade_url }. ~9.7KB, instant response.
- Ultra / Global Agency → returns the full live catalog (~500-1000 gifts), parsed and ready to use. Shape: { status_code, mode: "fetch", data: { room_id, gifts: [...] } }. Catalog is refreshed hourly upstream from TikTok. ~100KB, ~1-2s response (server-side fetch via our proxy pool).
Same response key path (data.gifts[]) on every tier so your client code works without changes when you upgrade.
Recommended workflow (matches what every enterprise client builds):
1. Cache the full catalog on Ultra at app startup (one call/day is plenty).
2. Live events (all paid plans) → WebSocket sends just gift_id + sender + count for zero-latency delivery.
3. Local match → look up the gift_id in your cached catalog to display the correct icon + calculate diamonds.
4. Auto-update → if a WebSocket gift_id is missing from your cache, refresh the catalog (new gift just shipped from TikTok).
Each gift entry has: id, name, icon_url (or icon on Ultra fetch shape), diamond_count, value_usd (1 diamond ≈ $0.005). Sandbox/Basic/Pro samples include is_sample: true and source: "sample" so your client can detect dev vs prod data.
Why Ultra? TikTok ships new gifts hourly. A one-off pull goes stale fast. Ultra subscribers get the hourly auto-refresh + worldwide creator coverage across every leaderboard + league.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
(none required) | - | No | Catalog is global - no params needed. Optional: `mode=sign` (Ultra+) to get a signed URL back instead of the parsed catalog if you want to fetch upstream yourself. |
Request
// Sample (sandbox/basic/pro) OR full live catalog (ultra/agency) - tier-branched server-side
const res = await fetch('https://api.tik.tools/webcast/gift_info', {
headers: { 'x-api-key': 'YOUR_KEY' }
});
const json = await res.json();
const gifts = json.data.gifts;
console.log(`got ${gifts.length} gifts (source=${json.source || 'live'})`);
gifts.forEach(g => console.log(`${g.name}: ${g.diamond_count} diamonds`));Response
// Sandbox/Basic/Pro:
{ "status_code": 0, "source": "sample", "is_sample": true, "count": 50,
"sample_generated_at": "2026-06-05T11:18:13Z",
"data": { "gifts": [{ "id": "9072", "name": "TikTok Universe", "icon_url": "https://cdn.tik.tools/gifts/...", "diamond_count": 44999, "value_usd": 449.99 }, ...] },
"your_tier": "sandbox", "upgrade_url": "https://tik.tools/pricing" }
// Ultra/Agency (default mode=fetch - no params needed):
{ "status_code": 0, "mode": "fetch",
"data": { "room_id": "...", "gifts": [{ "id": "9072", "name": "TikTok Universe", "diamond_count": 44999, "icon": "https://..." }, ...] } }
// Ultra/Agency mode=sign (returns the upstream signed URL for you to fetch yourself):
{ "status_code": 0, "signed_url": "https://webcast.tiktok.com/webcast/gift/list/?...", "headers": { "User-Agent": "...", "Referer": "..." } }/webcast/gifts_by_countryULTRA+Get the gift catalog for a specific country/region. TikTok scopes the gift panel to the region of the LIVE room it is served from, so this endpoint anchors to a currently-live creator in the requested region's leaderboard and returns that room's catalog - each gift with ID, icon URL, diamond value, USD value, and whether it is a streakable/combo gift.
Tier-branched response:
- Sandbox / Basic / Pro → static 50-gift sample so you can build against the schema. Shape: { status_code, source: "sample", is_sample: true, message, data: { country, region, gifts }, your_tier, upgrade_url }.
- Ultra / Global Agency → live per-country catalog for the region you request.
Params: country - an ISO country code (US, GB, BR, JP, DE, ...) or a tracked region code. One country per call.
Each gift entry: id, name, icon_url, diamond_count, value_usd (= diamond_count / 100), type, combo, streakable (streakable = combo gift, can be held/sent in a rapid streak).
Notes: A creator must be currently LIVE in the region to anchor the panel (else 503 - retry shortly or try another country). Catalogs change slowly, so cache results - this endpoint is rate-limited to 60 calls/hour per key.
Available on Ultra+. Upgrade at https://tik.tools/pricing.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
country | string | Yes | ISO country code (US, GB, BR, JP, ...) or a tracked region code. One country per call. |
Request
const res = await fetch('https://api.tik.tools/webcast/gifts_by_country?country=US', {
headers: { 'x-api-key': 'YOUR_KEY' }
});
const json = await res.json();
json.data.gifts.forEach(g =>
console.log(`${g.name}: ${g.diamond_count} diamonds ($${g.value_usd})${g.streakable ? ' [streakable]' : ''}`));Response
// Ultra/Agency:
{ "status_code": 0, "country": "US", "region": "US+", "region_label": "United States",
"anchor_room_id": "765...", "count": 587,
"data": { "gifts": [
{ "id": "59881", "name": "A Shard of Hope", "icon_url": "https://...png",
"diamond_count": 1, "value_usd": 0.01, "type": 1, "combo": true, "streakable": true }, ...
] } }
// Sandbox/Basic/Pro (static sample):
{ "status_code": 0, "source": "sample", "is_sample": true,
"message": "Live per-country gift catalog is an Ultra+ feature...",
"data": { "country": "US", "region": null, "gifts": [...] },
"your_tier": "sandbox", "upgrade_url": "https://tik.tools/pricing" }/webcast/gift_galleryGet the per-creator Gift Gallery summary - the curated gift wishlist a TikTok creator picks BEFORE going LIVE so viewers can help fulfill it. Returns whether the creator is qualified to use Gift Gallery, the total gallery size, how many gifts they've already lit this period, and their gallery league tier.
What is a Gift Gallery? TikTok creators on the LIVE Gift Gallery program select a curated set of gifts (the "wishlist") before going live. Viewers see a dedicated gallery icon during the stream and can send gifts from that wishlist to help the creator "light up" each slot. Filled slots unlock per-creator rewards + league promotion.
Tier-branched response:
- Sandbox / Basic / Pro -> returns a static SAMPLE so you can build + test your UI against the real schema without paying. Shape: { status_code: 0, source: "sample", is_sample: true, your_tier, upgrade_url, upgrade_message, data: {...} }. Same response shape as Ultra+, so client code works without changes on upgrade.
- Ultra / Agency Global / Admin -> returns the live per-creator data signed via webmssdk through our residential proxy pool. Shape: { status_code: 0, data: {...} }.
Detect sample vs live: check the top-level is_sample boolean. Sample responses also carry your_tier + upgrade_url so your client can branch logic safely.
Use cases (Ultra+ with live data): discover which creators are running active gift wishlists, surface progress (e.g. "8 of 10 lit") in third-party dashboards, target sponsored campaigns, score creators by Gallery league for partnership outreach.
Resolution: pass either unique_id (we resolve sec_uid + the creator's current room_id automatically) OR pass sec_uid + room_id directly if you already have them.
Creator must be currently LIVE (Ultra+ only). The upstream gift_gallery endpoint validates the room_id against the creator's active session - calls with no active room return error: "creator_not_live". Sandbox/Basic/Pro sample responses are returned regardless of LIVE state so you can keep building. Poll /webcast/is_live first OR subscribe via the WebSocket and call /webcast/gift_gallery from your roomStart handler.
Refresh cadence: TikTok updates lighted_gift_count in near-real-time during a LIVE; period rolls over monthly. Cache responses for ~60 seconds per (sec_uid) on your side to be polite.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
unique_id | string | No | TikTok @username (without the @). We resolve sec_uid + current room_id automatically. |
sec_uid | string | No | Direct sec_uid (skip the username resolve hop). Required if unique_id not provided. |
room_id | string | No | Current room_id. Optional - 0 is accepted for offline creators. Auto-filled when unique_id is provided. |
Request
const res = await fetch('https://api.tik.tools/webcast/gift_gallery?unique_id=s.luvi', {
headers: { 'x-api-key': 'YOUR_KEY' }
});
const json = await res.json();
const g = json.data;
console.log(`@${g.unique_id} qualified=${g.is_qualified} lit ${g.lighted_gift_count}/${g.total_gift_count} league=${g.anchor_league}`);Response
// Sandbox/Basic/Pro (SAMPLE - flagged via is_sample + your_tier + upgrade_url):
{
"status_code": 0,
"source": "sample",
"is_sample": true,
"your_tier": "sandbox",
"upgrade_url": "https://tik.tools/pricing",
"upgrade_message": "Real per-creator Gift Gallery data is available on Ultra and above...",
"data": {
"unique_id": "anyone",
"anchor_nickname": "Sample Creator",
"anchor_avatar_url": "https://...sample-avatar.png",
"period": 20260607, "period_starts_at": "2026-06-07T00:00:00.000Z", "period_ends_at": "2026-06-14T00:00:00.000Z",
"anchor_league": "B", "gallery_league": "B",
"total_gift_count": 10, "lit_gift_count": 2, "all_lit": false,
"gifts": [
{ "gift_id": "5586", "name": "Hearts", "coin_price": 199, "diamond_count": 199, "goal_count": 5, "current_sent_count": 2, "lit": false, "image_url": "...", "sponsored": false, "can_sponsor": true, "sponsorship_require_count": 5, "sponsor": null },
{ "gift_id": "5487", "name": "Finger Heart", "coin_price": 5, "diamond_count": 5, "goal_count": 15, "current_sent_count": 15, "lit": true, "image_url": "...", "sponsored": false, "can_sponsor": true, "sponsorship_require_count": 15, "sponsor": null },
{ "gift_id": "5655", "name": "Rose", "coin_price": 1, "diamond_count": 1, "goal_count": 20, "current_sent_count": 20, "lit": true, "image_url": "...", "sponsored": true, "can_sponsor": true, "sponsorship_require_count": 20, "sponsor": { "sponsor_id": "...", "nickname": "...", "avatar": "...", "sent_count": 20 } }
// ... 7 more
],
"ops_gifts": [], "incoming_gifts": []
}
}
// Ultra/Agency Global/Admin (LIVE per-creator data, sample from live creator 'SARA'):
{
"status_code": 0,
"data": {
"unique_id": "user",
"anchor_id": "7545242946763015186",
"anchor_nickname": "\ud835\udc12\ud835\udc00\ud835\udc11\ud835\udc00\u2728",
"anchor_avatar_url": "https://p16-common-sign.tiktokcdn-eu.com/...png",
"period": 20260601, "period_starts_at": "2026-05-31T...", "period_ends_at": "2026-06-07T...",
"anchor_league": "C5", // creator's gallery rank (S+ > S > A > B > C > D > D5)
"gallery_league": "C",
"gallery_ranking_icon_url": "https://...png",
"total_gift_count": 12, // total wishlist slots the creator configured
"lit_gift_count": 0, // how many slots fans have lit this period
"all_lit": false,
"gifts": [
{ "gift_id": "5586", "name": "Hearts", "coin_price": 199, "diamond_count": 199, "goal_count": 5, "current_sent_count": 0, "lit": false, ... },
{ "gift_id": "6820", "name": "Whale Diving", "coin_price": 2150, ... },
{ "gift_id": "5487", "name": "Finger Heart", "coin_price": 5, ... },
{ "gift_id": "5655", "name": "Rose", "coin_price": 1, "sponsored": true, "sponsor": {...} }
// ... 8 more
],
"ops_gifts": [], // promotional gifts (usually empty)
"incoming_gifts": [], // next-period preview
"class_type": 600
}
}/chat-sendBASIC+Send a chat message to a live TikTok room as the logged-in user. One call - the server signs, joins the room, sends, and confirms the message actually appeared in the live chat.
Authentication: include TikTok session cookies via the x-cookie-header header. Minimum required is sessionid + tt-target-idc (the data-center cookie, e.g. eu-ttp2). Get them from browser DevTools → Application → Cookies → tiktok.com.
Use a real, established account. Brand-new accounts (auto usernames like user35441..., no avatar/posts, or under ~24h old) are auto-shadowbanned from live chat by TikTok and return delivered: false no matter what.
delivered is the truth. TikTok returns status_code: 0 even on messages it silently shadow-drops. This endpoint confirms your message appeared in the room's live event stream and reports delivered: true / false. Do not rely on status_code alone.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
channel | string | Yes | The live creator's @username (the server resolves the current room) |
text | string | Yes | Chat message text (max ~150 chars) |
Request
const res = await fetch('https://api.tik.tools/chat-send?apiKey=YOUR_KEY', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-cookie-header': 'sessionid=YOUR_SESSIONID; tt-target-idc=eu-ttp2'
},
body: JSON.stringify({ channel: 'creatorname', text: 'Hello!' })
});
console.log(await res.json()); // { delivered: true, msg_id: '...', confirmed_via: 'live-event' }Response
{ "delivered": true, "status_code": 0, "msg_id": "7650032344963615510", "channel": "creatorname", "confirmed_via": "live-event" }/webcast/user_earningsRetrieve TikTok LIVE earnings data for a creator including diamonds received, coin equivalent, and period breakdowns. This is an authenticated endpoint that relays TikTok's creator earnings API.
Authentication required: You must include TikTok session cookies via x-cookie-header. Only works when the session belongs to the creator whose earnings are being requested (you can't view other people's earnings).
Response data: Returns diamonds (total received), coins (equivalent coin value), and period breakdown. Diamond-to-USD conversion: 1 diamond ≈ $0.005 USD.
Use cases: Creator dashboards showing real-time earnings, financial analytics for agency talent management, or automated earnings reports for multi-stream operations.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
unique_id | string | Yes | TikTok username |
period | string | No | Period filter: "daily" (default) |
Request
const res = await fetch('https://api.tik.tools/webcast/user_earnings?unique_id=creator&apiKey=YOUR_KEY', {
headers: { 'x-cookie-header': 'sessionid=YOUR_TIKTOK_COOKIES' }
});
const { data } = await res.json();
console.log(`Diamonds: ${data.diamonds}`);Response
{ "status_code": 0, "data": { "diamonds": 1500, "coins": 75000, "period": "daily" } }/webcast/feedPRO+Discover currently live TikTok LIVE streams. Uses a two-step "sign-and-return" pattern - the API returns a signed URL with headers and cookies that you fetch from YOUR own IP to get the actual TikTok feed data.
Authentication: Include your TikTok session_id cookie for personalized, populated results. Without it, results are anonymous and limited (~5 rooms).
Channels: 87 = Recommended - the main "For You" infinite scroll feed, 86 = Suggested - sidebar host recommendations (shown while watching), 89 or 1111006 = Gaming, 42 = Following (requires session).
Geo-targeting: Feed results are determined by the IP address making the final TikTok fetch (Step 2), NOT by the region parameter. Since you fetch the signed URL from your own client, you'll see content relevant to your geographic location. The region param is passed to TikTok but has minimal effect on results.
Pagination: The API automatically switches to TikTok's "load more" mode when you pass a max_time cursor. Fetch the signed URL → parse the TikTok JSON response → read data.extra.max_time → pass it as max_time in your next call. Each page returns ~6-15 rooms; keep paginating for continuous discovery. The response also includes a load_more_url template - just replace {MAX_TIME} with the cursor.
Rate limits: Pro: 100 calls/day. Ultra: 2,000 calls/day. Response includes feed_remaining and feed_limit fields.
Response data: Each room includes owner.display_id (username), owner.nickname (display name), title, user_count (viewers), owner.avatar_thumb (profile photo), id_str (room ID), and more.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
session_id | string | No | Your TikTok sessionid cookie - strongly recommended. Get from browser DevTools → Application → Cookies → tiktok.com. Without it, results are sparse. |
channel_id | string | No | Feed channel: 87 = Recommended "For You" (default), 86 = Suggested sidebar, 89 or 1111006 = Gaming, 42 = Following |
count | number | No | Rooms per page (default: 20, max: 50) |
max_time | string | No | Pagination cursor from previous TikTok response (data.extra.max_time). Omit or "0" for first page. |
region | string | No | Hint passed to TikTok (default: US). Note: actual results are geo-targeted by the IP making the final fetch, not this param. |
ttwid | string | No | TikTok ttwid visitor cookie. Server provides one if omitted. |
ms_token | string | No | TikTok msToken cookie (optional, a placeholder is used if omitted). |
Request
// Step 1: Get signed URL with session for populated results
const res = await fetch(
'https://api.tik.tools/webcast/feed?' + new URLSearchParams({
apiKey: 'YOUR_KEY',
session_id: 'YOUR_TIKTOK_SESSION_ID', // from browser cookies
region: 'US',
channel_id: '87', // 87=recommended, 86=suggested, 1111006=gaming
count: '20',
})
);
const { signed_url, headers, cookies, feed_remaining, feed_limit } = await res.json();
console.log(`Quota: ${feed_remaining}/${feed_limit} calls remaining`);
// Step 2: Fetch TikTok data from YOUR IP
const tikRes = await fetch(signed_url, {
headers: { ...headers, Cookie: cookies }
});
const feed = await tikRes.json();
// Step 3: Parse live rooms
for (const entry of feed.data) {
const room = entry.data;
console.log(`🔴 @${room.owner.display_id} (${room.owner.nickname})`);
console.log(` "${room.title}" - ${room.user_count} viewers`);
}
// Step 4: Load more (pagination)
const nextCursor = feed.extra?.max_time;
if (nextCursor) {
const page2 = await fetch(
'https://api.tik.tools/webcast/feed?' + new URLSearchParams({
apiKey: 'YOUR_KEY', session_id: 'YOUR_SESSION_ID',
region: 'US', max_time: nextCursor, count: '20',
})
);
// ... repeat Step 2-3
}Response
{
"status_code": 0,
"signed_url": "https://webcast.tiktok.com/webcast/feed/?...",
"method": "GET",
"headers": { "User-Agent": "...", "Referer": "https://www.tiktok.com/", ... },
"cookies": "ttwid=...; sessionid=...; sessionid_ss=...",
"region": "US",
"channel_id": "87",
"feed_remaining": 99,
"feed_limit": 100,
"note": "Fetch signed_url from your client/IP with these headers and cookies."
}
// After fetching signed_url, TikTok returns:
{
"status_code": 0,
"data": [{
"data": {
"id_str": "7123456789",
"title": "🔴 Live Now!",
"user_count": 1250,
"owner": {
"display_id": "streamer_name",
"nickname": "Display Name",
"avatar_thumb": { "url_list": ["https://..."] }
}
}
}, ...],
"extra": { "max_time": "1773128824428" }
}/webcast/live_analytics/video_listList historical live stream recordings for a TikTok account. Returns past streams with metadata including title, duration, viewer count, and stream date. Requires authenticated TikTok session cookies.
Authentication required: Include session cookies via x-cookie-header. Only accessible for the account that owns the session - you cannot view another creator's video history.
Response data: Each entry includes video_id (stream identifier), title, duration (seconds), viewer_count (peak viewers), create_time (start timestamp), and engagement metrics.
Use cases: Building creator analytics dashboards, tracking streaming performance over time, generating stream history reports, or identifying a specific video_id for use with the video_detail endpoint.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
unique_id | string | Yes | TikTok username |
count | number | No | Number of results (default: 20) |
Request
const res = await fetch('https://api.tik.tools/webcast/live_analytics/video_list?unique_id=creator&apiKey=YOUR_KEY', {
headers: { 'x-cookie-header': 'sessionid=YOUR_TIKTOK_COOKIES' }
});
const { data } = await res.json();
data.forEach(v => console.log(`${v.title}: ${v.viewer_count} viewers`));Response
{ "status_code": 0, "data": [{ "video_id": "...", "title": "...", "duration": 3600, "viewer_count": 1200 }, ...] }/webcast/live_analytics/video_detailGet granular analytics for a specific past live stream session. Returns detailed performance metrics including total views, peak concurrent viewers, gifts received, new followers gained, and engagement rates. Requires authenticated TikTok session cookies.
Authentication required: Include session cookies via x-cookie-header. Access is restricted to the account's own streams.
Response data: Includes views (total), duration (seconds), peak_viewers, gifts (total diamond value), new_followers, likes, comments, and period-specific breakdowns. The video_id comes from the video_list endpoint.
Use cases: Post-stream analytics reports, A/B testing different stream formats, tracking creator growth metrics, or building agency reporting dashboards that aggregate performance across multiple creators.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
video_id | string | Yes | Video/stream ID |
Request
const res = await fetch('https://api.tik.tools/webcast/live_analytics/video_detail?video_id=VID123&apiKey=YOUR_KEY', {
headers: { 'x-cookie-header': 'sessionid=YOUR_TIKTOK_COOKIES' }
});
const { data } = await res.json();
console.log(`Duration: ${data.duration}s, Peak: ${data.peak_viewers}`);Response
{ "status_code": 0, "data": { "video_id": "...", "duration": 3600, "views": 12000, "gifts": 350, "peak_viewers": 800 } }/webcast/live_analytics/user_interactionsGet the real-time online audience roster for a live room, ranked by gift score (diamonds sent during this session). Returns each viewer's rank, score, and full profile. Requires authenticated TikTok session cookies.
How it works: Uses the "sign-and-return" pattern. The API returns a signed_url that you fetch with your session cookies to get TikTok's audience data. The anchor_id (host's user ID) is auto-resolved from the room_id.
Response data: Each entry includes rank (1-based position), score (diamonds sent this session), and user profile with nickname, display_id, id_str, avatar_thumb, and follow_info (follower/following counts).
Use cases: Identifying top supporters in real-time, building live leaderboard overlays, tracking viewer engagement and spending patterns, or providing viewer analytics for agency clients.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
room_id | string | Yes | Room ID of the stream |
user_id | string | No | Filter to specific user ID |
count | number | No | Number of results (default: 50) |
Request
const res = await fetch('https://api.tik.tools/webcast/live_analytics/user_interactions?room_id=7123456789&apiKey=YOUR_KEY', {
headers: { 'x-cookie-header': 'sessionid=YOUR_TIKTOK_COOKIES' }
});
const { signed_url, headers, anchor_id } = await res.json();
// Fetch from your IP with your session
const tikRes = await fetch(signed_url, {
headers: { ...headers, Cookie: `sessionid=YOUR_SESSIONID; ${headers.Cookie || ''}` }
});
const { data } = await tikRes.json();
data.ranks.forEach(r => console.log(`#${r.rank} ${r.user.nickname}: ${r.score} pts`));Response
{ "status_code": 0, "signed_url": "https://...", "anchor_id": "...", "note": "Fetch signed_url with your session cookies" }
// After fetching signed_url with session cookies, TikTok returns:
{ "data": { "ranks": [{ "rank": 1, "score": 192, "user": { "nickname": "...", "display_id": "...", "id_str": "..." } }], "total": 49 } }/api/live/connectBundled endpoint - fetches both a scoped JWT token and stream video URLs in a single request. Ideal for frontend apps that need to open a WebSocket and play video simultaneously. Rate limited to 10 req/min.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
uniqueId | string | Yes | TikTok username (without @) |
Request
const res = await fetch('https://tik.tools/api/live/connect?uniqueId=username');
const data = await res.json();
// data.token - short-lived JWT for WebSocket auth
// data.wsUrl - WebSocket server URL
// data.stream - video URLs (HLS/FLV)
const ws = new WebSocket(`${data.wsUrl}?uniqueId=username&jwtKey=${data.token}`);Response
{ "token": "eyJ...", "wsUrl": "wss://api.tik.tools", "uniqueId": "streamer", "stream": { "room_id": "...", "alive": true, "stream_urls": { "origin": { "hls": "...", "flv": "..." } }, "default_quality": "origin" } }/webcast/ranklist/regionalPRO+Get regional LIVE leaderboard rankings - daily, hourly, popular, or league. Returns a signed URL that the client fetches with their own TikTok session cookie. Requires Pro or Ultra tier.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
unique_id | string | No | TikTok username (auto-resolves room_id and anchor_id) |
room_id | string | No | Room ID (alternative to unique_id) |
anchor_id | string | No | Anchor/owner user ID. Auto-resolved if unique_id is used. |
rank_type | string | No | Ranking period: "1" = Hourly, "8" = Daily (default), "15" = Popular LIVE, "16" = League |
type | string | No | "list" (default) for rankings, "entrance" for available tabs |
gap_interval | string | No | Gap interval filter (default: "0") |
Request
// Step 1: Get signed URL from the API
const res = await fetch('https://api.tik.tools/webcast/ranklist/regional?apiKey=YOUR_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
room_id: '7607695933891218198',
anchor_id: '7444599004337652758',
rank_type: '8' // Daily
})
});
const signData = await res.json();
// Step 2: Fetch from YOUR IP with YOUR session cookie
const tikResp = await fetch(signData.signed_url, {
method: signData.method,
headers: { ...signData.headers, Cookie: `sessionid=YOUR_SESSIONID; ${signData.cookies}` },
body: signData.body
});
const { data } = await tikResp.json();
data.rank_view.ranks.forEach((r, i) =>
console.log(`${i+1}. ${r.user.nickname} - ${r.score} pts`)
);Response
{
"status_code": 0,
"action": "fetch_signed_url",
"signed_url": "https://webcast.tiktok.com/webcast/ranklist/list/v2/?...",
"method": "POST",
"headers": { "User-Agent": "...", "Content-Type": "application/x-www-form-urlencoded" },
"body": "room_id=123&rank_type=8&anchor_id=456&gap_interval=0",
"cookies": "ttwid=...",
"note": "POST this URL with your sessionid cookie from your browser/IP."
}/webcast/ranklist/gamingGLOBALTikTok LIVE Gaming Ranks per region: the overall gaming leaderboard plus that region's 3 top game boards (per-game weekly, ~99 creators each). Served from our continuously-maintained cache - no TikTok session needed. Global Agency tier gets every name unmasked; all other tiers receive the exact same response shape with the top 3 names as a teaser and the rest dot-masked, plus an upgrade prompt, so you can build against the structure before upgrading.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
region | string | No | Region code, e.g. US+ (URL-encode the plus as %2B), BR, JP, ID, TR, BK. Defaults to US+. |
Request
const res = await fetch(
'https://api.tik.tools/webcast/ranklist/gaming?region=US%2B&apiKey=YOUR_KEY'
);
const data = await res.json();
console.log(`${data.region}: ${data.count} gaming creators, ${data.games.length} games`);
for (const g of data.games) {
console.log(`${g.game} (${g.official ? 'weekly' : 'live'}): ${g.count} creators`);
g.entries.slice(0, 5).forEach(e => console.log(` #${e.rank} ${e.unique_id} - ${e.score}`));
}Response
{
"status_code": 0,
"region": "US+",
"gated": false, // true for non-global tiers (masked)
"tier": "agency_global",
"updated_at": 1781500000000,
"count": 99,
"entries": [ // overall gaming leaderboard
{ "rank": 1, "unique_id": "creator", "nickname": "Creator",
"avatar": "https://...", "score": 607740, "is_live": true, "room_id": "7..." }
],
"games": [ // the region's 3 game boards
{ "rank_type": 23, "game": "Call of Duty: Mobile", "game_id": 0,
"official": true, // true = TikTok's weekly board; false = grouped fallback
"weekly": true, "reset_at": 1782050000000, "count": 99,
"entries": [ { "rank": 1, "unique_id": "...", "score": 926566, "is_live": true, "room_id": "7..." } ] }
]
// non-global tiers also include: "upgrade": "...Upgrade to the Global tier..."
}/webcast/ranklist/gaming_moversGLOBALThe "99+" view: every creator who entered the Gaming Top 99 at ANY point during the current rank-day - including the reset-time churn where a creator holds a spot for seconds - per game. TikTok only ever exposes the live Top 99; this surfaces who moved through and below the cutoff, sourced from our rank-history (no extra consumption). Each mover carries their best (peak) rank of the day, peak diamonds, how many times they appeared, whether they are live now, and the diamonds needed to re-enter the current Top 99. Global Agency tier gets every name unmasked; all other tiers get the same shape with dot-masked identifiers (real ranks + scores shown) plus an upgrade prompt.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
region | string | No | Region code, e.g. US+ (URL-encode the plus as %2B), BR, JP, ID, TR, BK. Defaults to US+. |
Request
const res = await fetch(
'https://api.tik.tools/webcast/ranklist/gaming_movers?region=US%2B&apiKey=YOUR_KEY'
);
const data = await res.json();
for (const g of data.games) {
const below = g.movers.filter(m => !m.in_top99_now);
console.log(`${g.game}: ${below.length} below the #99 cutoff (${g.cutoff_score})`);
below.slice(0, 5).forEach(m => console.log(` peak #${m.peak_rank} ${m.unique_id} - ${m.peak_score}, reenter ${m.diamonds_to_reenter}`));
}Response
{
"status_code": 0,
"region": "US+",
"region_label": "United States",
"gated": false, // true for non-global tiers (masked)
"tier": "agency_global",
"games": [
{ "class_type": 0, "game": "Overall (Daily)", "cutoff_score": 54771, "total": 393,
"movers": [
{ "peak_rank": 2, "unique_id": "creator", "nickname": "Creator", "avatar": "https://...",
"peak_score": 202840, "appearances": 155, "live_now": false,
"in_top99_now": false, "diamonds_to_reenter": 0 }
] }
]
// non-global tiers also include: "upgrade": "...Upgrade to the Global tier..."
}/webcast/ranklist/region_moversGLOBALThe "entered today, below cutoff now" view for the standard regional/country leaderboards (single board per region, vs gaming_movers per game). Every creator who held a board spot at ANY point during the current rank-day - including the reset-time churn - but sits below the live cutoff now, sourced from our rank-history (no extra consumption). Each mover carries their best (peak) rank of the day, peak diamonds, appearances, whether they are live now, and the diamonds needed to re-enter. Global Agency tier gets every name unmasked; all other tiers get the same shape with dot-masked identifiers (real ranks + scores shown) plus an upgrade prompt.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
region | string | No | Region code, e.g. US+ (URL-encode the plus as %2B), BR, JP, ID, TR, BK, VN. Defaults to US+. |
Request
const res = await fetch(
'https://api.tik.tools/webcast/ranklist/region_movers?region=US%2B&apiKey=YOUR_KEY'
);
const data = await res.json();
console.log(`${data.total} entered today, now below the ${data.cutoff_score} cutoff`);
data.movers.slice(0, 5).forEach(m => console.log(` peak #${m.peak_rank} ${m.unique_id} - ${m.peak_score}, reenter ${m.diamonds_to_reenter}`));Response
{
"status_code": 0,
"region": "US+",
"region_label": "United States",
"gated": false, // true for non-global tiers (masked)
"tier": "agency_global",
"cutoff_score": 284791,
"total": 247,
"movers": [
{ "peak_rank": 4, "unique_id": "creator", "nickname": "Creator", "avatar": "https://...",
"peak_score": 146343, "appearances": 102, "live_now": false,
"in_board_now": false, "diamonds_to_reenter": 138448 }
]
// non-global tiers also include: "upgrade": "...Upgrade to the Global tier..."
}/webcast/ranklist/shoppingGLOBALNEW: the weekly TikTok Shop Live Shopping leaderboard per region - the creators driving the most GMV on LIVE this week, recorded continuously on the same rotation as the rest of the rank intel (no TikTok session needed). Each rank carries the creator handle, display name, the week's GMV score, and whether they are live now. The response also carries the rank-window countdown so you know when the weekly board resets. Global Agency tier gets every name unmasked; all other tiers receive the exact same response shape with the top 3 names as a teaser and the rest dot-masked, plus an upgrade prompt, so you can build against the structure before upgrading. Also live on the website at /tiktok-live-gaming-ranks?board=shopping.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
region | string | No | Region code, e.g. US+ (URL-encode the plus as %2B), BR, JP, ID, TR, BK. Defaults to US+. |
Request
const res = await fetch(
'https://api.tik.tools/webcast/ranklist/shopping?region=US%2B&apiKey=YOUR_KEY'
);
const data = await res.json();
console.log(`${data.region}: ${data.count} sellers, board resets in ${Math.round(data.countdown / 3600000)}h`);
data.ranks.slice(0, 5).forEach(r => console.log(` #${r.rank} ${r.uniqueId} - GMV ${r.score}${r.isLive ? ' (live)' : ''}`));Response
{
"status_code": 0,
"region": "US+",
"region_label": "United States",
"gated": false, // true for non-global tiers (masked)
"tier": "agency_global",
"weekly": true,
"updated_at": 1781500000000,
"next_reset": 1782050000000, // ms epoch when the weekly board rolls over
"countdown": 549999000, // ms remaining until next_reset
"count": 99,
"ranks": [
{ "rank": 1, "uniqueId": "shopcreator", "displayName": "Shop Creator",
"avatar": "https://...", "score": 184320, "isLive": true, "roomId": "7..." }
]
// non-global tiers also include: "upgrade": "...Upgrade to the Global tier..."
// and dot-mask uniqueId/displayName for ranks 4+ (real rank + score kept)
}/webcast/leaderboardReturns the pre-aggregated TikTok daily creator leaderboard for a region in <50ms (no TikTok roundtrip). The leaderboard-poller continuously snapshots 28 regions every 30-60s using anchor-verified probes and a top-10 cross-region validator that rejects wrong-region scrapes before they corrupt the snapshot. Each entry includes rank, uniqueId, userId, nickname, avatar, diamond score, isLive flag, and roomId for the live ones.
Tier behaviour: SANDBOX / FREE / BASIC / PRO callers receive the real uniqueId + nickname + avatar + score + isLive for ranks 1-3 of every region (so the response shape is fully demonstrable). userId and roomId are nulled even on those top-3 ranks. Ranks 4 to 99 are replaced with random per-request placeholder rows (\creator-<8 hex>\, blank nickname, generic avatar). \masked: true\ is set on every placeholder row. ULTRA / AGENCY GLOBAL / ADMIN receive every rank fully unmasked including userId + roomId.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
region | string | Yes | One of: BR, LATAM, US+, CA, AU+, JP, KR, TW, ID, MY, PH, CCA, TH, VN, DE+, ES+, HU+, PT+, FR+, BK, PL, UK+, IT+, SE+, NL, RO, MENA, TR |
Request
const r = await fetch('https://api.tik.tools/webcast/leaderboard?region=KR', {
headers: { 'x-api-key': process.env.TIKTOOL_API_KEY }
})
const data = await r.json()
console.log(`${data.region}: ${data.live_count} creators live, top: ${data.entries[0].uniqueId}`)Response
{
"status_code": 0,
"region": "KR",
"rank_type": "daily",
"fetched_at": 1780775023239,
"live_count": 611,
"entries": [
{ "rank": 1, "uniqueId": "park1h", "userId": "...", "nickname": "Wonho", "avatar": "...", "score": 41003344, "isLive": true, "roomId": "..." },
{ "rank": 2, "uniqueId": "...", "score": 30221100, "isLive": false }
]
}/webcast/ranklist/gamingGLOBALReturns the TikTok LIVE Gaming Ranking for a region - the live in-app gaming leaderboard, recorded continuously by anchor-verified probes on the same rotation as leagues. Each entry includes rank, uniqueId, nickname, avatar, diamond score, estimated USD, and isLive.
Access: Global Agency only. SANDBOX / FREE / BASIC / PRO / ULTRA callers receive a masked 3-name preview (ranks 1-3 real with real scores as the hook; ranks 4+ replaced with length-matched placeholder rows, masked: true, no avatar). AGENCY GLOBAL / ADMIN receive every rank fully unmasked.
Also live on the website at /tiktok-live-gaming-ranks.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
region | string | No | Region code (one of: BR, LATAM, US+, AU+, JP, KR, TW, ID, MY, PH, CCA, TH, VN, DE+, ES+, HU+, PT+, FR+, BC, BK, PL, UK+, IT+, SE+, NL, RO, MENA, TR). Omit or pass "top" for the most-populated region. |
Request
const r = await fetch('https://api.tik.tools/webcast/ranklist/gaming?region=US%2B', {
headers: { 'x-api-key': process.env.TIKTOOL_API_KEY }
})
const data = await r.json()
console.log(`${data.region}: top gaming creator ${data.entries[0].uniqueId} (${data.entries[0].score})`)Response
{
"status_code": 0,
"region": "US+",
"fetched_at": 1780775023239,
"total": 99,
"entries": [
{ "rank": 1, "uniqueId": "yazitax_", "nickname": "Kiki", "avatar": "...", "score": 530587, "usd": 2653, "isLive": true },
{ "rank": 2, "uniqueId": "...", "score": 412900, "isLive": false }
]
}/webcast/leaderboard/leaguesReturns ALL 18 league brackets for a region in one response. Each bracket is a class_type from 100 (lowest) up to 2000 (Diamond) plus 1800/1900 intermediate brackets, with the top-99 creators currently in that bracket. Useful for full-region league analytics, dashboards, or replicating a region's league standings inside another product.
Region parameter: accepts a region code (IT+, BK, UK+, ...) OR a country code (IT, GR, GB, DE, FR, ...) - country codes resolve to their TikTok Business Market region automatically. The response region field echoes the resolved canonical code.
Tier behaviour: every tier can hit this endpoint. SANDBOX through PRO see the top-3 of each bracket fully unmasked (uniqueId + nickname + avatar + score + isLive), with userId and roomId nulled; ranks 4 to 99 are replaced with random placeholder rows. ULTRA / AGENCY GLOBAL / ADMIN see every rank with all identifiers.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
region | string | Yes | Region code (one of 28: BR, LATAM, US+, CA, AU+, JP, KR, TW, ID, MY, PH, CCA, TH, VN, DE+, ES+, HU+, PT+, FR+, BK, PL, UK+, IT+, SE+, NL, RO, MENA, TR) OR a country code (IT, DE, FR, GB, GR, AE, etc) that resolves to one of those regions. |
Request
const r = await fetch('https://api.tik.tools/webcast/leaderboard/leagues?region=KR', {
headers: { 'x-api-key': process.env.TIKTOOL_API_KEY }
})
const data = await r.json()
console.log('Diamond top:', data.classes['2000'].entries[0])Response
{
"status_code": 0,
"region": "KR",
"classes": {
"100": { "fetched_at": 1780414384749, "entries": [...] },
"200": { "fetched_at": ..., "entries": [...] },
"...": "...",
"2000": { "fetched_at": 1780774261464, "entries": [{ "rank": 1, "uniqueId": "park1h", "score": 23119099, "isLive": true, ... }] }
}
}/webcast/leaderboard/leagueReturns a single league bracket (one class_type) inside a region. Cheaper than /leaderboard/leagues when you only care about the top-tier bracket (Diamond = 2000) or want to poll a specific class on a tight schedule.
Tier behaviour: same masking rules as the other /webcast/leaderboard endpoints - SANDBOX through PRO see the top-3 unmasked (with userId/roomId nulled) and ranks 4 to 99 as placeholder rows; ULTRA / AGENCY GLOBAL / ADMIN see every rank fully.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
region | string | Yes | Region code (e.g. "KR"). |
class | string | Yes | League class type. One of: 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1800, 1900, 2000. |
Request
const r = await fetch('https://api.tik.tools/webcast/leaderboard/league?region=KR&class=2000', {
headers: { 'x-api-key': process.env.TIKTOOL_API_KEY }
})
const data = await r.json()Response
{
"status_code": 0,
"region": "KR",
"class_type": "2000",
"fetched_at": 1780774261464,
"entries": [
{ "rank": 1, "uniqueId": "park1h", "userId": "...", "nickname": "Wonho", "score": 23119099, "isLive": true, "roomId": "..." }
]
}/webcast/live-countsReturns the per-region count of unique live creators (across the daily leaderboard + every league bracket) plus the global total and a second "verified" total that intersects the leaderboard with an active relay/gift WS signal (lower bound). Powers the tik.tools/ranking page header. Refreshed every 30s.
Tier behaviour: open to every tier. No per-creator identifiers are returned at all - the response is pure aggregate counts - so there is nothing to mask.
Request
const r = await fetch('https://api.tik.tools/webcast/live-counts', {
headers: { 'x-api-key': process.env.TIKTOOL_API_KEY }
})
const data = await r.json()
console.log(`Global live: ${data.global_live}`)Response
{
"status_code": 0,
"global_live": 12046,
"global_live_verified": 1837,
"regions": {
"BR": 401, "LATAM": 314, "US+": 321, "CA": 271, "AU+": 139,
"JP": 604, "KR": 611, "TW": 486, "ID": 654, "MY": 474,
"PH": 420, "CCA": 528, "TH": 485, "VN": 723,
"DE+": 491, "ES+": 359, "HU+": 310, "PT+": 260, "FR+": 412,
"BK": 317, "PL": 286, "UK+": 343, "IT+": 403, "SE+": 192,
"NL": 232, "RO": 326, "MENA": 304, "TR": 345
}
}/api/leaderboards/country/:slugPRO+Real-time creator leaderboard for any country or TikTok Business Market region. Returns ranked entries with diamonds, live status, room IDs (for the alive ones), and 7-90 day rank history for paid tiers. Region slug examples: balkans, united-states-region, japan, latam, united-kingdom. Country slugs follow standard kebab-case names. Identifiers are masked to someone-<hash> for Sandbox viewers; PRO+ unmasks names, handles, and profile URLs.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | Country slug (e.g. "greece") or region slug (e.g. "balkans"). 28 regions + every TikTok-served country supported. |
Request
Response
{
"country": { "code": "GR", "name": "Greece", "slug": "greece" },
"region": { "code": "BK", "countries": ["GR","BG","RS","RO","..."] },
"is_active": true,
"tier": "pro",
"masked": false,
"history_days": 7,
"next_reset": "2026-05-26T00:00:00.000Z",
"current": {
"fetched_at": "2026-05-25T10:34:00.000Z",
"live_count": 142,
"total_score": 89234500,
"channels": [
{ "rank": 1, "uniqueId": "...", "displayName": "...", "score": 4502300, "alive": true, "roomId": "...", "profilePic": "..." }
]
},
"history": [ { "date": "2026-05-24", "top3": [...], "total_score": 76234500, "entry_count": 200 } ]
}/api/leaderboards/leagues/:regionULTRA+Returns the full league overview for a region: every active class (2000 down to 100, with up to 18 brackets corresponding to TikTok's A1-D5 ladder) with rank windows, member counts, anchor pointers, and the next reset boundary. Identifiers are masked for Sandbox and Pro tiers; ULTRA+ unmasks names + handles + profile URLs. Combine with /api/leaderboards/league/:region/:classType for the per-class entry rows.
Valid region codes (28): BR, LATAM, US+, CA, AU+, JP, KR, TW, ID, MY, PH, CCA, TH, VN, DE+, ES+, HU+, PT+, FR+, BK, PL, UK+, IT+, SE+, NL, RO, MENA, TR. The "+" suffix is REQUIRED for multi-country buckets (eg Italy is "IT+", United Kingdom is "UK+"). Country codes are accepted as aliases - hitting /leagues/IT resolves to IT+, /leagues/GB resolves to UK+, /leagues/GR resolves to BK, etc.
available flag: available: false means our cache has not yet captured any league brackets for the resolved region in the current cycle. It does NOT mean TikTok has no league for that market - we re-probe per missing class every cycle and most regions hold 14-18 of 20 possible classes at any moment. If you see available: false on a region you expect to be active, check that the resolved region field in the response matches what you intended (eg passing IT returns region: "IT+" because IT+ is the canonical code).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
region | string | Yes | Region code (e.g. "IT+", "BK", "US+", "JP") OR country code (e.g. "IT", "GR", "DE", "GB"). Country codes resolve to their region automatically. See full list of 28 region codes in the description. |
Request
Response
{
"region": "IT+",
"requested": "IT",
"leagues": [
{ "classType": 2000, "classLabel": "A1", "count": 99, "updatedAt": 1780779587561, "anchorUniqueId": "abc", "anchorRoomId": "...", "stale": false, "staleMinutes": 1 },
{ "classType": 1900, "classLabel": "A2", "count": 99, "updatedAt": 1780779590451, "anchorUniqueId": "xyz", "anchorRoomId": "...", "stale": false, "staleMinutes": 0 }
],
"available": true,
"hasFullAccess": true,
"valid_regions": ["BR","LATAM","US+","CA","AU+","JP","KR","TW","ID","MY","PH","CCA","TH","VN","DE+","ES+","HU+","PT+","FR+","BK","PL","UK+","IT+","SE+","NL","RO","MENA","TR"]
}/api/leaderboards/league/:region/:classTypeULTRA+All creator entries in a specific league class within a region. Useful for tracking who is winning the current cycle in Diamond/Platinum/Gold/Silver/Bronze. Same mask rule: ULTRA+ unmasks identifiers. ClassType values: 2000 (Diamond), 1000 (Platinum), 600 (Gold), 300 (Silver), 100 (Bronze).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
region | string | Yes | Region code (e.g. "BK") |
classType | number | Yes | League class: 2000 | 1000 | 600 | 300 | 100 |
Request
Response
{
"region": "BK",
"classType": 2000,
"className": "Diamond",
"entries": [
{ "rank": 1, "uniqueId": "...", "nickname": "...", "score": 12450, "roomId": "..." }
],
"masked": false,
"fetched_at": "2026-05-25T10:34:00.000Z"
}/api/gifters/leaderboardCross-creator gifter leaderboard - the actual whales spending diamonds across TikTok LIVE in a given region and time window. Global Agency exclusive for unmasked rows; Pro / Ultra / Sandbox all see stable someone-<hash> placeholders. Aggregate numbers (diamonds, ranks, creators gifted) remain visible for everyone so you can still build analytics without paying.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
region | string | No | Region filter (e.g. "BK", "US+"). Default "GLOBAL". |
period | string | No | "daily" (default) | "weekly" | "monthly" | "lifetime" |
limit | number | No | Rows to return (max 200, default 100) |
Request
Response
{
"region": "GLOBAL",
"period": "daily",
"gifters": [
{ "rank": 1, "platformId": "...", "username": "...", "displayName": "...",
"totalDiamonds": 1240000, "creatorsGifted": 6, "giftCount": 1840,
"profilePic": "...", "profileUrl": "...", "masked": false }
],
"fetched_at": "2026-05-25T10:34:00.000Z"
}/api/gifters/topTop gifters for a single creator with per-period breakdowns + each gifter's share of the creator's total income. Useful for VIP outreach and roster prioritisation. Global Agency exclusive for unmasked rows; Pro / Ultra / Sandbox see the same masked placeholders. Pass the AES-GCM masked-handle token (?p=<token>) to resolve a masked URL without exposing the real handle.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
creator | string | Yes | Creator username (without @) |
p | string | No | AES-GCM mask token (used by masked-handle URLs to resolve back to the real creator without leaking the username in the URL) |
limit | number | No | Rows to return (max 50, default 10) |
Request
Response
{
"creator": "...",
"totalDiamonds": 8420000,
"topGifters": [
{ "rank": 1, "platformId": "...", "username": "...", "displayName": "...",
"diamonds": 1240000, "share": 0.147, "lastGiftAt": "2026-05-25T10:30:00.000Z",
"masked": false }
]
}/api/gifters/profileDeep gifter profile - lifetime diamonds, list of creators they support, wallet-fatigue cohort, loyalty score, latest big gifts, and historical velocity. Global Agency exclusive for unmasked identifiers + profile URL; Pro / Ultra / Sandbox see placeholders with aggregate numbers intact.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
username | string | Yes | Gifter username (without @) |
p | string | No | AES-GCM mask token for masked-URL resolution |
Request
Response
{
"gifter": {
"platformId": "...",
"username": "...",
"displayName": "...",
"profilePic": "...",
"profileUrl": "...",
"lifetimeDiamonds": 18420000,
"loyaltyScore": 0.86,
"walletFatigue": "elevated",
"creatorsSupported": [
{ "creator": "...", "totalDiamonds": 8420000, "share": 0.46 }
],
"recentBigGifts": [...],
"history": { "daily": [...], "weekly": [...] }
}
}/ws/sweepBASIC+Self-service ghost-WebSocket cleanup. Closes every WebSocket the server currently tracks for your own API key - and nothing else. No admin auth required, just your normal apiKey.
When you need this:
- Your SDK crashed without a clean .close() (e.g. SIGKILL, hard reboot, container OOM), leaving TCP sockets open at the OS level. The WebSocket library auto-replies to control PING frames at the OS/library layer even when your app code is gone, so the server side can't tell the connection is logically dead. Result: your concurrent-WS count grows until you hit the tier cap and can't open new ones.
- You're deploying a new build and want a clean slate before the new container starts subscribing.
- You implemented your own their_count vs our_count drift detector (recommended) and want a button to call when the gap exceeds your tolerance.
What it does NOT do: - Does not touch any OTHER account's connections. - Does not change your tier or limits. - Does not affect future connections - your SDK can immediately reconnect.
Rate limit: 10 calls per hour per key. Polling this is pointless: every NEW WebSocket connect already triggers FIFO eviction of the oldest tracked socket for the same key, so steady-state ghost accumulation is already prevented server-side. Manual sweep is for after-crash hygiene, not a fix-the-drift loop.
Tier: Basic+ (Demo + Sandbox keys have wsLimit=1 so the endpoint is meaningless for them - 403).
Close code your SDK will see: 4502 (SWEEP_INITIATED) with reason text "ws-sweep: caller initiated ghost-WS cleanup". This is distinct from 4401 (INVALID_AUTH). If your SDK auto-treats 4401 as "fatal, stop retrying", a sweep will NOT trigger that path - 4502 is a clean, retryable close.
Request
const r = await fetch('https://api.tik.tools/ws/sweep', {
method: 'POST',
headers: { 'x-api-key': process.env.TIKTOOL_API_KEY },
});
const { closed } = await r.json();
console.log('Swept ' + closed + ' stale WebSocket(s).');Response
{
"status_code": 0,
"message": "Closed 94 WebSocket connection(s) tracked for this key.",
"closed": 94,
"close_code": 4502,
"close_reason_prefix": "ws-sweep:",
"note": "Sockets closed with code 4502 (SWEEP_INITIATED). Treat 4502 as a clean, retryable close - NOT a 4401 auth failure. Reason text starts with \"ws-sweep:\". Your SDK can immediately reconnect."
}/webcast/eligible_creatorsGLOBALFind Eligible Creators to Recruit - the API behind the agency dashboard's recruiting page. Returns a paginated, score-ranked list of every creator currently tracked across the live league boards, so a scouting script can filter out low earners before reaching out.
Payload per creator: username, current_league (e.g. A1, B2), and current_snapshot_score - the real-time score TikTok is reporting for them on their league board at the moment of the query. Each row also carries its region code.
Live snapshot only. This route returns the current snapshot score. Historical data, 30-day timelines, and heavy aggregations stay in the web dashboard by design.
Pagination: results are ranked by current_snapshot_score descending. Page through with page (1-based) and limit (up to 100). The response includes total, total_pages, and returned.
Rate limit: 60 requests/hour per key on this route (it queries the heavy aggregation tables). At limit=100 that is up to ~6,000 profiles/hour. x-ratelimit-limit / x-ratelimit-remaining / x-ratelimit-reset headers and a rate_limit object are returned on every call; over the cap returns HTTP 429.
Access: Agency tier only.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
page | integer | No | 1-based page number (default 1) |
limit | integer | No | Results per page, 1-100 (default 50) |
region | string | No | Optional region code filter (e.g. US+, MENA, IT+) |
min_score | integer | No | Optional - only return creators at or above this current_snapshot_score |
Request
// Page through every eligible creator, filtering low earners client-side.
let page = 1;
while (true) {
const res = await fetch(https://api.tik.tools/webcast/eligible_creators?apiKey=YOUR_KEY&page=${page}&limit=100`);
const data = await res.json();
for (const c of data.results) {
if (c.current_snapshot_score < 50000) continue; // skip low earners
console.log(c.username, c.current_league, c.current_snapshot_score);
}
if (page >= data.total_pages) break;
page++;
}Response
{ "status_code": 0, "page": 1, "page_size": 50, "total": 32920, "total_pages": 659, "returned": 50,
"results": [ { "username": "azd0092", "current_league": "A1", "current_snapshot_score": 10923989, "region": "MENA" } ],
"rate_limit": { "limit": 60, "remaining": 59, "reset": 1781550485 } }/api/webhooksBASIC+Register a webhook that pushes events to your URL when one of your watched creators goes live. This is go-live push, not a forward of an already-connected session - we monitor your creators server-side, so nothing needs to be running on your end. Authenticate with your x-api-key (server-to-server) or while logged in to the dashboard.
Watched creators are set per webhook via the creators array (TikTok usernames, no @). Events are tier-gated:
- live.start / live.end - all paid plans (Basic and up)
- live.peak_viewers, live.gift.high_value - Ultra and Global
- creator.follower_milestone, creator.eligible - Global
Per-plan limits (webhook endpoints / deliveries per day / creators per webhook): - Basic: 1 endpoint, 500 deliveries/day, up to 1000 creators - Pro: 3 endpoints, 5,000 deliveries/day, up to 1000 creators - Ultra: 5 endpoints, 50,000 deliveries/day, priority delivery - Global: unlimited endpoints, 500,000 deliveries/day, IP allowlist + priority delivery - Sandbox: test-only (1 disabled config, up to 5 creators, no live delivery)
Delivery is at-least-once with automatic retries; every attempt is recorded and can be replayed. Payloads are signed (X-Tiktool-Signature over the raw body, with X-Tiktool-Timestamp) so you can verify authenticity.
---
Endpoint requirements
- Your URL must be publicly reachable. Both http and https are accepted (https recommended - but the HMAC signature protects every delivery either way, so a self-hosted http://IP:port receiver is fine). Private, internal, loopback and link-local hosts are rejected.
- Respond 2xx fast. Return 200 the moment you receive the POST, then do your real work (recording, DB writes, downstream calls) asynchronously. If your handler blocks before responding, your listener stops accepting new connections and deliveries start failing. Connect timeout is 10s, read timeout 15s.
- Keep the listener always-on. A refused connection (server busy or restarting) or a timeout both count as a failed delivery. Run multiple workers and raise your accept backlog so you never drop connections under load.
Detection & timing
- We monitor your creators server-side and fire live.start within seconds of them going live - nothing needs to run on your end.
- Detection is near-real-time but poll-based: a live that both starts and ends within a few seconds may not be observed. Lives of normal length are caught reliably.
Delivery & retries
- At-least-once. Each event is attempted immediately, then retried on failure at 0s, 30s, 2m, 10m, 1h, 6h (6 attempts over ~6 hours). After the final attempt it is dead-lettered.
- Dedupe on event_id - the same event can arrive more than once.
- Every attempt is logged; dead-letters can be replayed from the dashboard or API once your endpoint is healthy.
Verifying signatures
- Each delivery carries X-Tiktool-Signature (HMAC-SHA256 of the raw request body) and X-Tiktool-Timestamp. Recompute the HMAC with your webhook secret and compare. Also sent on every delivery: X-Tiktool-Event, X-Tiktool-Event-Id, X-Tiktool-Delivery-Id, X-Tiktool-Attempt, X-Tiktool-Webhook-Id.
Troubleshooting - receiving fewer events than expected
- Test deliveries work but live events do not: detection is not the problem - look at your endpoint. A "pending" status or missing events almost always means deliveries are failing at your server.
- Failures that return in ~200ms are a refused connection: your server was not accepting connections at that moment (busy or restarting). Keep the listener responsive - acknowledge with 200 first, process after.
- Slow failures are timeouts: your handler took longer than the read timeout. Acknowledge first, work after.
- GET /api/webhooks shows recent delivery health (last status, fail streak) per webhook. Stabilise your endpoint, then replay any dead-lettered deliveries.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
target_url | string | Yes | Public http or https URL that receives the POST. https recommended; http is allowed (the HMAC signature protects each delivery). Private/internal/loopback hosts are rejected. Can be changed later via PATCH. |
event_types | string[] | Yes | Events to subscribe to, e.g. ["live.start","live.end"]. Must be allowed by your plan. |
creators | string[] | No | TikTok usernames to watch (no @). Up to 1000 on paid plans, 5 on sandbox. Omit to receive events for all creators allowed by your plan scope. |
name | string | No | Label for this webhook in the dashboard. |
description | string | No | Free-text note. |
bearer_token | string | No | Optional bearer token we send as Authorization on each delivery. |
extra_headers | object | No | Optional static headers added to each delivery. |
Request
const res = await fetch('https://tik.tools/api/webhooks', {
method: 'POST',
headers: { 'x-api-key': 'YOUR_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({
target_url: 'https://your-app.com/hooks/tiktool',
event_types: ['live.start', 'live.end'],
creators: ['charlidamelio', 'khaby.lame'],
}),
})
const webhook = await res.json()
console.log('store this secret:', webhook.secret)Response
{
"id": "wh_01J...",
"target_url": "https://your-app.com/hooks/tiktool",
"event_types": ["live.start", "live.end"],
"creators": ["charlidamelio", "khaby.lame"],
"secret": "whsec_...", // shown once - store it to verify signatures
"active": true,
"created_at": "2026-06-18T13:00:00.000Z"
}/api/webhooksBASIC+List your registered webhooks with their event types, watched creators, active state, and recent delivery health. Same auth as register (x-api-key or dashboard session).
Request
const res = await fetch('https://tik.tools/api/webhooks', {
headers: { 'x-api-key': 'YOUR_KEY' },
})
console.log(await res.json())Response
{
"webhooks": [
{
"id": "wh_01J...",
"target_url": "https://your-app.com/hooks/tiktool",
"event_types": ["live.start", "live.end"],
"creators": ["charlidamelio"],
"active": true,
"deliveries_today": 42
}
]
}/api/webhooks/{id}/testBASIC+Fire a synthetic live.start at your target URL (signed with the real secret, flagged is_test:true) so you can validate your integration end-to-end before a real creator goes live. The delivery is logged like any other.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Webhook id (path parameter). |
Request
await fetch('https://tik.tools/api/webhooks/wh_01J.../test', {
method: 'POST', headers: { 'x-api-key': 'YOUR_KEY' },
})Response
{ "delivered": true, "status_code": 200, "delivery_id": "del_..." }Live-State Workflow
End-to-end pattern for monitoring a creator watchlist at scale. Combines cheap preflight (/webcast/bulk_live_check) with managed WebSocket open on confirmed-live, plus close-code classification for healthy backoff.
Why this shape: - Cap-aware preflight keeps you from wasting WS slots on offline creators - Tiered backoff per close code avoids hammering streams that just ended - Stateful retry registry prevents reconnect storms after a TikTok throttle - Single connection per creator (no fanout) so end-users read from your store, not from our WS
Production guidance: preflight every 60-120s for high-priority creators, every 10-30min for low-priority rotation. Skip preflight when active WS >= 90% of your tier's cap. On code 4404 (NOT_LIVE), back off 30s/90s/300s then 5-30min. On 4005/4006/4555 (stream-end family), wait until the next preflight tick rather than reconnecting immediately.
// Live-state workflow: preflight + WS open + close-code backoff
// Requires: npm i ws
import WebSocket from 'ws';
const API_KEY = process.env.TIKTOOL_API_KEY;
const BASE = 'https://api.tik.tools';
const WS_BASE = 'wss://api.tik.tools';
const WATCHLIST = ['creator1', 'creator2', 'creator3'];
const WS_BUDGET = 100;
const STREAM_END = new Set([4005, 4006, 4555]);
const live = new Map(); // uniqueId -> WebSocket
const cooldown = new Map(); // uniqueId -> retryAt (ms epoch)
const fails = new Map(); // uniqueId -> consecutive 4404 count
// Returns { uniqueId: is_live } map built from the array response.
async function preflight(ids) {
if (!ids.length) return {};
const r = await fetch(`${BASE}/webcast/bulk_live_check?apiKey=${API_KEY}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unique_ids: ids }),
});
const { data = [] } = await r.json();
return Object.fromEntries(data.map(row => [row.unique_id, !!row.is_live]));
}
function backoffFor(code, n) {
if (code === 4404) return [, , ][Math.min(n, 2)] ?? (5 + Math.min(n - 3, 5) * 5) * ;
if (STREAM_END.has(code)) return ;
if (code === 4429) return ;
return ;
}
function openWs(uniqueId) {
if (live.has(uniqueId)) return;
const ws = new WebSocket(`${WS_BASE}?apiKey=${API_KEY}&uniqueId=${encodeURIComponent(uniqueId)}`);
live.set(uniqueId, ws);
ws.on('message', raw => {
const ev = JSON.parse(raw.toString());
if (ev.event === 'roomInfo') return;
console.log(uniqueId, ev.event);
});
ws.on('close', (code) => {
live.delete(uniqueId);
const n = code === 4404 ? (fails.get(uniqueId) || 0) + 1 : 0;
fails.set(uniqueId, n);
cooldown.set(uniqueId, Date.now() + backoffFor(code, n));
});
ws.on('error', () => {});
}
async function tick() {
if (live.size >= WS_BUDGET * 0.9) return;
const now = Date.now();
const due = WATCHLIST.filter(u => !live.has(u) && (cooldown.get(u) || 0) <= now);
const status = await preflight(due);
for (const [u, isLive] of Object.entries(status)) {
if (isLive) { fails.delete(u); openWs(u); }
else cooldown.set(u, now + );
}
}
setInterval(tick, ); tick();WebSocket Events
All event types sent over the WebSocket connection.
Fields
| Name | Type | Description |
|---|---|---|
event_id | string | Unique id for this event (idempotency key - dedupe on it). |
event | string | "live.start". |
occurred_at | string | ISO timestamp when the event was generated. |
is_test | boolean | true for deliveries fired via the test endpoint, false for real events. |
creator | object | { unique_id, nickname, avatar_url, user_id, follower_count } for the creator who went live. |
live | object | { room_id, started_at, live_url } for the live session. |
_links | object | Convenience links, e.g. { creator: "https://tik.tools/@username" }. |
Example Payload
// POST https://your-app.com/hooks/tiktool
// X-Tiktool-Event: live.start
// X-Tiktool-Signature: <hmac> X-Tiktool-Timestamp: 1750000000
{
"event_id": "evt_01J...",
"event": "live.start",
"occurred_at": "2026-06-18T13:00:00.000Z",
"is_test": false,
"creator": {
"unique_id": "charlidamelio",
"nickname": "charli d'amelio",
"avatar_url": "https://p16-sign.tiktokcdn.com/...",
"user_id": "6569595380449902593",
"follower_count": 155000000
},
"live": {
"room_id": "7000000000000000000",
"started_at": "2026-06-18T13:00:00.000Z",
"live_url": "https://www.tiktok.com/@charlidamelio/live"
},
"_links": { "creator": "https://tik.tools/@charlidamelio" }
}Fields
| Name | Type | Description |
|---|---|---|
event_id | string | Unique id for this event (dedupe key). |
event | string | "live.end". |
occurred_at | string | ISO timestamp when the event was generated. |
creator | object | Same creator shape as live.start. |
live | object | { room_id, ended_at } for the session that ended. |
Example Payload
{
"event_id": "evt_01J...",
"event": "live.end",
"occurred_at": "2026-06-18T14:30:00.000Z",
"is_test": false,
"creator": { "unique_id": "charlidamelio", "nickname": "charli d'amelio" },
"live": { "room_id": "7000000000000000000", "ended_at": "2026-06-18T14:30:00.000Z" }
}Fields
| Name | Type | Description |
|---|---|---|
user | WebcastUser | The user who sent the message. Includes a `payGrade` (SDK) / `level` (raw WS) field carrying the sender's TikTok gifter level (1-50, higher = more coins spent platform-wide). Present on every user-bearing event. |
comment | string | The chat message text |
starred | object | undefined | Present only for starred (paid highlighted) messages. Contains { claps: number, score: number } |
language | string | undefined | **v3 (2026-06-07)**: ISO 639-1 language code TikTok auto-detected for this comment ("en", "tr", "un" = unknown). Use for per-message routing or translation. |
messageUuid | string | undefined | **v3 (2026-06-07)**: stable per-message UUID. Used by the `imDelete` moderation event so consumers can correlate "this chat got deleted" back to the original line. |
replyToUser | WebcastUser | undefined | **v3 (2026-06-07)**: set when this comment is a reply to another viewer (~8% of chats). Contains the target viewer's `uniqueId`, `nickname`, and avatar URLs. |
protoVersion | number | undefined | **v3 (2026-06-07)**: 3 when this chat carries v3-only fields, otherwise 2. Lets v1/v2 consumers branch on schema age without sniffing field presence. |
Example Payload
{
"event": "chat",
"data": {
"type": "chat",
"user": {
"id": "123",
"nickname": "John",
"uniqueId": "john123",
"level": 17 // raw WS field name (Gifter level 1-50)
},
"comment": "Hello!",
"starred": { "claps": 3, "score": 1200 }
}
}
// SDK consumers (Node / Python TikTokLive client) receive the same field
// renamed to user.payGrade for consistency with the official tikfinity /
// tiktok-live-connector typings.
// Same applies to every other event with a "user" field (gift, like,
// follow, member, share, social, roomPin, etc).Fields
| Name | Type | Description |
|---|---|---|
user | WebcastUser | The user who liked |
likeCount | number | Number of likes in this event |
totalLikes | number | Total likes on the stream |
Example Payload
{ "event": "like", "data": { "type": "like", "user": { "id": "123", "uniqueId": "fan99" }, "likeCount": 15, "totalLikes": 4200 } }Fields
| Name | Type | Description |
|---|---|---|
user | WebcastUser | The gifter |
giftId | number | Gift identifier |
giftName | string | Display name of the gift |
repeatCount | number | Number of gifts in combo |
diamondCount | number | Diamond value per gift |
repeatEnd | boolean | Whether the gift combo has ended |
transactionId | string | undefined | **v3 (2026-06-07)**: stable per-gift transaction UUID (hex). Use as dedup key across upstream retries - the same combo will always carry the same transactionId even if `repeatCount` updates twice. |
senderUserId | string | undefined | **v3 (2026-06-07)**: explicit sender id parsed from the new `{"sender_id":"..."}` envelope. Mirrors `user.id` but is delivered separately on the wire so it remains available even on truncated gift frames. |
relationship | { joinDayNumber?, fromUser?, toUser? } | undefined | **v3 (2026-06-07)**: relationship metadata when TikTok attaches it. `joinDayNumber` = how many days the gifter has been following the creator. Use for "Day 131 fan sent X" overlays. |
protoVersion | number | undefined | **v3 (2026-06-07)**: 3 when this gift carries v3-only fields, otherwise 2. |
Example Payload
{ "event": "gift", "data": { "type": "gift", "user": { "uniqueId": "generous" }, "giftName": "Rose", "repeatCount": 5, "diamondCount": 1, "repeatEnd": true, "transactionId": "202606070219033A38D7B466DD66036089", "senderUserId": "7595581591918347286", "relationship": { "joinDayNumber": 131 }, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
user | WebcastUser | The user who joined |
action | string | Join action type |
actionCode | number | undefined | **v3 (2026-06-07)**: granular numeric subcode (38, 44, ...). Finer-grained than `action`. |
entrySource | string | undefined | **v3 (2026-06-07)**: where the viewer came from. Examples: "homepage_hot-live_cell" (Discover hot row), "follow-tab", "share-link". Critical creator-analytics signal - shows what surface drives a creator's viewer base. |
entryAction | string | undefined | **v3 (2026-06-07)**: how the viewer entered. "draw" = TikTok algorithmically pulled the viewer in. "click" = explicit click (search / follow). "other" = uncategorised. |
entryType | string | undefined | **v3 (2026-06-07)**: "rec" when TikTok recommended this stream. Absent on direct joins. |
protoVersion | number | undefined | **v3 (2026-06-07)**: 3 when this member event carries v3-only fields, otherwise 2. |
Example Payload
{ "event": "member", "data": { "type": "member", "user": { "uniqueId": "viewer1" }, "action": "join", "actionCode": 38, "entrySource": "homepage_hot-live_cell", "entryAction": "draw", "entryType": "rec", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
totalViewers | number | Total unique viewers |
viewerCount | number | Current concurrent viewers |
Example Payload
{ "event": "roomUserSeq", "data": { "type": "roomUserSeq", "totalViewers": 15000, "viewerCount": 342 } }Fields
| Name | Type | Description |
|---|---|---|
user | WebcastUser | The subscriber |
subMonth | number | Subscription month count |
Example Payload
{ "event": "subscribe", "data": { "type": "subscribe", "user": { "uniqueId": "subscriber1" }, "subMonth": 3 } }Fields
| Name | Type | Description |
|---|---|---|
user | WebcastUser | The user |
emoteId | string | Emote identifier |
emoteUrl | string | URL of the emote image |
Example Payload
{ "event": "emoteChat", "data": { "type": "emoteChat", "user": { "uniqueId": "emojifan" }, "emoteId": "123", "emoteUrl": "https://..." } }Fields
| Name | Type | Description |
|---|---|---|
battleId | string | Battle identifier |
status | number | 1 = ACTIVE, 2 = STARTING, 3 = ENDED, 4 = PREPARING |
battleDuration | number | Total battle duration in seconds (typically 300) |
teams | BattleTeam[] | Array of competing teams |
extraHostUserIds | string[] | undefined | **v3 (2026-06-07)**: additional host user IDs surfaced on multi-guest battles (host pairs beyond the primary two). |
layoutSubtype | string | undefined | **v3 (2026-06-07)**: TikTok layout subtype ("cohost_normal_expand_2", ...) - overlays use this to pick the right PK frame template. |
protoVersion | number | undefined | **v3 (2026-06-07)**: 3 when v3-only fields are present, otherwise 2. |
Example Payload
{ "event": "battle", "data": { "type": "battle", "battleId": "456", "status": 1, "battleDuration": 300, "teams": [{ "hostUserId": "1", "score": 100 }], "extraHostUserIds": ["6832673084131902469"], "layoutSubtype": "cohost_normal_expand_2", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
battleId | string | Battle session tag. Often "0" on the opening and early frames - use matchId as the stable identity instead. |
matchId | string | Stable match ID across a multi-round PK. Use this as the battle key (battleId can be "0"). |
sessionId | string | Per-round session ID |
status | number | Battle phase. **1 and 4 are both ACTIVE score frames** - treat them identically; status 4 is the most common live frame and carries the countdown tail, so do not filter it out. **2 = settle/end** (fires once when the round finishes). Status 3 and 5 do NOT appear in the current protocol - do not key completion on them. The final countdown frame (secsRemaining = 0) almost always arrives as a status 4 or status 2 frame, never status 1. |
serverTsMs | number | TikTok server clock (ms epoch) at frame emit |
durationSec | number | Total battle duration in seconds |
secsRemaining | number | Seconds remaining, derived from serverTsMs (drift-free). **secsRemaining === 0 is the primary completion signal** and is the most reliable way to finalize a battle. |
hosts | BattleHost[] | One entry per PK-side host. Multi-guest PKs include up to 4 per side. Each carries hostUserId, teamTotalScore, teamIdx and a sorted contributors[] array (MVP first). |
transactionId | string | undefined | **v3 (2026-06-07)**: stable per-frame transaction UUID (hex). Use as a dedup key to avoid double-counting score updates when upstream retries a frame. |
protoVersion | number | undefined | **v3 (2026-06-07)**: 3 when transactionId is present, otherwise 2. |
Example Payload
{
"event": "battleArmies",
"data": {
"type": "battleArmies",
"battleId": "7298765432109876543",
"status": 4,
"matchId": "...",
"sessionId": "...",
"serverTsMs": 1747654321000,
"durationSec": 300,
"secsRemaining": 187,
"hosts": [
{
"hostUserId": "6892636847263982593",
"teamTotalScore": 12450,
"teamIdx": 0,
"contributors": [
{ "userId": "111", "score": 8000, "nickname": "MVP_Gifter" },
{ "userId": "222", "score": 3000, "nickname": "OtherFan" },
{ "userId": "333", "score": 1450, "nickname": "ThirdFan" }
]
},
{
"hostUserId": "7012345678901234567",
"teamTotalScore": 9800,
"teamIdx": 1,
"contributors": []
}
]
}
}
// hosts[0].contributors[0] is always the MVP on that side.
// ── Detecting battle completion (read this if you persist battle results) ──
// status 1 and 4 are BOTH active score frames - process them the same way.
// The terminal/settle frame is status 2. status 3 and 5 do not exist in the
// current protocol; do not wait for them. The secsRemaining = 0 frame almost
// always arrives on a status 4 or status 2 frame, so a consumer that only
// reads status 1 frames will never observe secsRemaining = 0 and will leave
// battles unfinalized.
//
// Recommended finalize logic (priority order):
// 1. secsRemaining === 0 on any frame -> round finished (primary signal)
// 2. status === 2 -> settle frame (confirms result)
// 3. WALL-CLOCK fallback (required):
// finalizeAt = startedAt + durationSec * 1000 + 30000
// Battles that end early (host cancels the PK, opponent leaves, or the
// host ends the stream) emit NO terminal frame - the event stream simply
// stops. Finalize with the last known active score once finalizeAt passes.
// 4. IDLE-GAP fallback: if an active battle receives no battleArmies frame
// for 30s, finalize it with the last score (frames normally fire every
// 2-6s). Catches host-cancel / disconnect.
//
// matchId is the stable battle key. Keep the last active score per matchId and
// commit it when any of the conditions above fire.Fields
| Name | Type | Description |
|---|---|---|
battleId | string | Battle session tag |
cardType | number | 2 = gloves / crit, 3 = mist, 4 = match guide, 11 = booster x2/x3, ... |
effect | string | 'gloves' | 'mist' | 'booster_x2' | 'booster_x3' | 'match_guide' | 'thunder' | 'extra_time' | raw key |
multiplier | number | 2 or 3 for booster_x2 / booster_x3, otherwise 0 |
senderUserId | string | User ID of the gifter who triggered the card |
senderNickname | string | Display name of the trigger |
senderUniqueId | string | Username of the trigger |
senderAvatarUrl | string | Avatar URL of the trigger |
activatedAtSec | number | When the card activated (epoch seconds) |
durationSec | number | How long the effect lasts |
endsAtSec | number | When the effect ends (epoch seconds) |
iconUrl | string | Full TikTok CDN URL for the card art |
iconKey | string | Short identifier like 'card_mist_v3' or 'card_crit_v3' |
accentColor | string | Hex accent color (#BCD9E0 mist, #E0D4BC gloves, ...) |
Example Payload
{
"event": "battleItemCard",
"data": {
"type": "battleItemCard",
"battleId": "7298765432109876543",
"cardType": 11,
"effect": "booster_x3",
"multiplier": 3,
"senderUserId": "111",
"senderNickname": "Big_Spender",
"senderUniqueId": "big_spender",
"senderAvatarUrl": "https://p16-sign.tiktokcdn-us.com/...",
"activatedAtSec": 1747654321,
"durationSec": 30,
"endsAtSec": 1747654351,
"iconUrl": "https://...",
"iconKey": "card_booster_x3",
"accentColor": "#FFCB47"
}
}Fields
| Name | Type | Description |
|---|---|---|
user | WebcastUser | The user who asked |
questionText | string | The question text |
Example Payload
{ "event": "question", "data": { "type": "question", "user": { "uniqueId": "curious" }, "questionText": "What game is this?" } }Fields
| Name | Type | Description |
|---|---|---|
action | number | Control action code (3 = stream ended) |
Example Payload
{ "event": "control", "data": { "type": "control", "action": 3 } }Fields
| Name | Type | Description |
|---|---|---|
envelopeId | string | Envelope identifier |
diamondCount | number | Diamond value |
Example Payload
{ "event": "envelope", "data": { "type": "envelope", "envelopeId": "789", "diamondCount": 50 } }Fields
| Name | Type | Description |
|---|---|---|
user | WebcastUser | The user who wrote the pinned message |
comment | string | The pinned comment text |
action | number | Pin action: 1 = pin, 2 = unpin |
durationSeconds | number | How long the pin lasts (e.g. 60) |
pinnedAt | number | Timestamp when pinned (ms) |
originalMsgType | string | Original message type (e.g. "WebcastChatMessage") |
originalMsgId | string | ID of the original chat message |
operatorUserId | string | User ID of who pinned the message |
Example Payload
{ "event": "roomPin", "data": { "type": "roomPin", "user": { "uniqueId": "viewer1", "nickname": "Viewer" }, "comment": "Great stream!", "action": 1, "durationSeconds": 60, "pinnedAt": 1773564373063, "originalMsgType": "WebcastChatMessage", "originalMsgId": "7617400904630995734", "operatorUserId": "7444599004337652758" } }Fields
| Name | Type | Description |
|---|---|---|
user | WebcastUser | The user who became a Super Fan |
level | number | Super Fan level achieved (e.g. 10) |
displayType | string | Raw TikTok display type string |
Example Payload
{ "event": "superFan", "data": { "type": "superFan", "user": { "uniqueId": "loyalfan", "nickname": "LoyalFan" }, "level": 10, "displayType": "ttlive_superFan_commentNotif_someoneBecameSuperFan" } }Fields
| Name | Type | Description |
|---|---|---|
user | WebcastUser | The Super Fan who joined |
displayType | string | Raw TikTok display type string |
Example Payload
{ "event": "superFanJoin", "data": { "type": "superFanJoin", "user": { "uniqueId": "superfan1", "nickname": "SuperFan" }, "displayType": "ttlive_superFan_commentNotif_superFanJoined" } }Fields
| Name | Type | Description |
|---|---|---|
user | WebcastUser | The user who sent the Super Fan Box |
envelopeId | string | Envelope identifier |
diamondCount | number | Diamond value of the Super Fan Box |
Example Payload
{ "event": "superFanBox", "data": { "type": "superFanBox", "user": { "uniqueId": "generous", "nickname": "Generous" }, "envelopeId": "123456", "diamondCount": 100 } }Fields
| Name | Type | Description |
|---|---|---|
giftId | number | TikTok gift catalog id. Cross-reference with /webcast/gift_info for name + diamond cost. |
giftName | string | Resolved gift name when known, otherwise empty. |
repeatCount | number | Sequence counter for the gift within the combo. |
comboCount | number | Combo counter (mirrors repeatCount). |
diamondValue | number | Collab session diamond total carried on the gift. |
diamondTotal | number | Collab session score total carried on the gift. |
sender | { uniqueId, nickname, userId, avatar } | The viewer who sent the gift. userId is authoritative; nickname/avatar are not shipped by TikTok for the sender and are masked below Pro. |
coHosts | { userId, nickname, uniqueId, avatar }[] | Every co-streaming creator credited by this gift. nickname + avatar unmasked on Pro+, masked below. uniqueId is empty (not shipped by TikTok). |
recipients | { userId, nickname, uniqueId, avatar }[] | Alias of coHosts for SDK ergonomics. |
coHostCount | number | Number of co-hosts credited on this gift instance. |
roomId | string | null | Room the gift fired in, when resolvable. |
_masked | boolean | undefined | Present and true when identities were masked (sub-Pro). Absent on Pro+. |
_upgrade_required | string | undefined | Tier needed to unmask identities ("pro"). Present only when masked. |
Example Payload
{ "event": "collabGift", "data": { "type": "collabGift", "giftId": 1233, "giftName": "Collab Gift", "repeatCount": 1, "comboCount": 1, "diamondValue": 62819, "diamondTotal": 62816, "coHostCount": 3, "sender": { "uniqueId": "", "nickname": "", "userId": "7387482728746353696" }, "coHosts": [ { "userId": "7327728695157376032", "nickname": "Chloe", "uniqueId": "", "avatar": "https://..." }, { "userId": "6761121990676218885", "nickname": "Danielle", "uniqueId": "", "avatar": "https://..." } ], "recipients": [ ], "roomId": "7655296008007584534" } }
// Sub-Pro callers receive the same shape with nickname masked (e.g. "•••••"), avatar null, plus "_masked": true, "_upgrade_required": "pro".Fields
| Name | Type | Description |
|---|---|---|
user | WebcastUser | The associated user (if any) |
label | string | Barrage label text |
displayType | string | Raw TikTok display type string for classification |
content | string | Barrage content text |
Example Payload
{ "event": "barrage", "data": { "type": "barrage", "user": { "uniqueId": "viewer1" }, "label": "BecomingSuperFanLv10", "displayType": "ttlive_superFan_commentNotif_someoneBecameSuperFan", "content": "{0:user} just became a Super Fan" } }Fields
| Name | Type | Description |
|---|---|---|
text | string | Caption text (utf-8). Partial windows arrive while the speaker talks; the final window for the same segment has isFinal=true. |
language | string | undefined | **v3 (2026-06-07)**: ISO 639-1 language code of the spoken caption ("en", "ko", "es"). Detect mid-stream language switches without a separate translation request. |
isFinal | boolean | true on the segment-final caption, false on partial / streaming updates. |
startedAtMs | number | Caption window start (ms epoch on TikTok server clock). |
endsAtMs | number | Caption window end (ms epoch). |
protoVersion | number | Always 3 - this event was added in the v3 schema. |
Example Payload
{ "event": "caption", "data": { "type": "caption", "text": "Hello everyone welcome to the stream", "language": "en", "isFinal": true, "startedAtMs": 1780798747292, "endsAtMs": 1780798748292, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
senderUserId | string | TikTok user id of the opponent-side gifter. |
opponentRoomId | string | Room id of the opposing host. |
giftId | number | Gift identifier (same id-space as the main /webcast/gift_info catalog). |
giftPictureUrl | string | Full TikTok CDN URL for the gift image. |
startedAtMs | number | Gift animation start (ms epoch). |
endsAtMs | number | Gift animation end (ms epoch). |
transactionId | string | Stable per-gift UUID (hex) - use as dedup key. |
protoVersion | number | Always 3 - this event was added in the v3 schema. |
Example Payload
{ "event": "linkMicOpponentGift", "data": { "type": "linkMicOpponentGift", "senderUserId": "7366557450243605522", "opponentRoomId": "7569885742970635284", "giftId": 5655, "giftPictureUrl": "https://...", "startedAtMs": 1780798748470, "endsAtMs": 1780798748589, "transactionId": "2026060710190765D03BD889F88B8F065D", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
deletedMsgId | string | TikTok message id of the chat that was deleted. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "imDelete", "data": { "type": "imDelete", "deletedMsgId": "7648470148944628496", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
goalKey | string | TikTok goal key, e.g. "live_goal_indicator_stream_goal". |
creatorUserId | string | User id of the creator running the goal. |
contributionLevel | number | Current contribution level (1..N). |
metadataJson | string | Raw JSON: { is_first_contribute, challenge_type, update_source, ... }. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "goalUpdate", "data": { "type": "goalUpdate", "goalKey": "live_goal_indicator_stream_goal", "creatorUserId": "7280230115615114245", "contributionLevel": 4, "metadataJson": "{\"is_first_contribute\":false,\"challenge_type\":0}", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
privilegeKey | string | Privilege key, e.g. "gallery_all_lit_up_d". |
action | string | Action descriptor, e.g. "gift_broadcast_msg". |
bgUrls | string[] | TikTok CDN background frame URLs. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "privilegeAdvance", "data": { "type": "privilegeAdvance", "privilegeKey": "gallery_all_lit_up_d", "action": "gift_broadcast_msg", "bgUrls": ["https://p16-webcast.tiktokcdn.com/..."], "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
trayCount | number | Number of items in the tray. |
updatedAtMs | number | Update timestamp (ms epoch). |
relatedMsgId | string | Snowflake id of the related message. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "commentTray", "data": { "type": "commentTray", "trayCount": 3978, "updatedAtMs": 1780798747613, "relatedMsgId": "7648470148944628496", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
action | number | Top-level action code (11 = invite, ...). |
subAction | number | Sub-action code. |
sourceType | string | undefined | Where the link originated. Examples: "inviteCancel_new_arc", "SOURCE_TYPE_FRIEND_LIST". |
targetUserId | string | undefined | User id of the link target. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "linkLayer", "data": { "type": "linkLayer", "action": 11, "subAction": 4, "sourceType": "SOURCE_TYPE_FRIEND_LIST", "targetUserId": "7648408274391419681", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
action | number | Action code. |
subAction | number | Sub-action code. |
relatedUser | WebcastUser | undefined | The other user on the link, present only when TikTok ships it in the envelope. For co-host join/leave it is usually absent and you receive an empty user (id "0", isAnonymous true). This is upstream TikTok behaviour, NOT a tier limitation - relatedUser is never masked by plan. For the co-host's uniqueId / nickname / avatar, read the linkMic event below. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "link", "data": { "type": "link", "action": 20, "subAction": 4, "relatedUser": { "uniqueId": "cristina_ailerua" }, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
action | string | Roster action (e.g. join / leave / update). |
users | WebcastUser[] | Full host profiles when TikTok includes them - frequently EMPTY on layout/state frames. When empty, read the co-host userId from `extras["5"]` and resolve it (see snippet). |
extras | Record<string,any> | Raw unmapped protobuf fields. For link-mic, field "5" is the co-host userId; fields "6"/"7" are link-mic type/score metadata. |
protoVersion | number | Always 3. |
Example Payload
// Real link-mic frame: users[] empty, co-host userId lives in extras["5"].
// { "event": "linkMic", "data": { "users": [], "extras": { "5": "7529001445733385224", "6": 5, "7": 7986 } } }
// Resolve the id to a profile with /webcast/resolve_user_ids (works on every tier).
const API = 'https://api.tik.tools';
client.on('linkMic', async (e) => {
const userId = e.users?.[0]?.userId || e.extras?.['5'];
if (!userId) return;
const { data } = await fetch(`${API}/webcast/resolve_user_ids?apiKey=${API_KEY}`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_ids: [String(userId)] })
}).then(r => r.json());
const u = data[String(userId)]; // { userId, username, nickname, avatarUrl }
console.log('co-host:', u.username, u.nickname, u.avatarUrl);
});
// Use the link event for join/leave TIMING (its relatedUser is usually empty too).
client.on('link', (e) => console.log('link state change:', e.action, e.subAction));Fields
| Name | Type | Description |
|---|---|---|
panelId | string | Panel id (or room id) the update applies to. |
updatedAtSec | number | Update timestamp (epoch seconds, NOT ms). |
protoVersion | number | Always 3. |
Example Payload
{ "event": "giftPanelUpdate", "data": { "type": "giftPanelUpdate", "panelId": "7648436315382123281", "updatedAtSec": 1780798748, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
rawTag | string | Raw packed game tag + title. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "gameServerFeature", "data": { "type": "gameServerFeature", "rawTag": "106Roblox", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
toolPayload | string | Tool descriptor + ids. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "anchorToolModification", "data": { "type": "anchorToolModification", "toolPayload": "video_anchor 7392706231896379905", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
pickType | number | Pick category (2 = standard). |
payload | string | Packed gift highlight list. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "viewerPicksUpdate", "data": { "type": "viewerPicksUpdate", "pickType": 2, "payload": "Kiss 5655, Bullet 7934", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
rawPayload | string | Packed fan-ticket descriptor. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "fanTicket", "data": { "type": "fanTicket", "rawPayload": "mg_default ...", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
rawPayload | string | Packed restriction descriptor. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "giftDynamicRestriction", "data": { "type": "giftDynamicRestriction", "rawPayload": "...", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
battleId | string | Battle id this punishment belonged to. |
punishedUserId | string | User id of the loser-side host. |
punishType | number | Punishment subtype code. |
sessionId | string | Battle session id. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "battlePunishFinish", "data": { "type": "battlePunishFinish", "battleId": "7648485765504125714", "punishedUserId": "7204121790628185094", "punishType": 1, "sessionId": "7648485804360108818", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
noticeCode | number | Notice category code. |
noticeKey | string | Localized notice key, e.g. "ttlive_liveMatch_inviteFail_versionUpdateTo". |
noticeText | string | Decoded notice text in TikTok's active locale. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "battleNotice", "data": { "type": "battleNotice", "noticeCode": 6, "noticeKey": "ttlive_liveMatch_inviteFail_versionUpdateTo", "noticeText": "Update your TikTok...", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
rawPayload | string | Board snapshot payload. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "hostBoard", "data": { "type": "hostBoard", "rawPayload": "...", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
pollId | string | Snowflake id of the poll. |
action | number | Action code (2 = update). |
status | number | Poll status. |
questionPayload | string | Question text / packed body. |
optionsPayload | string | Options text / packed body. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "poll", "data": { "type": "poll", "pollId": "7648486279037946655", "action": 2, "status": 2, "questionPayload": "LETS FILL UP THE GALLERY", "optionsPayload": "HeartMe | Like | Share", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
competitionType | number | Competition category code. |
layoutSubtype | string | undefined | Layout subtype overlays use to render the competition frame ("cohost_take_stage_v2_2", ...). |
protoVersion | number | Always 3. |
Example Payload
{ "event": "competition", "data": { "type": "competition", "competitionType": 8, "layoutSubtype": "cohost_take_stage_v2_2", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
rawPayload | string | Packed status payload. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "streamStatus", "data": { "type": "streamStatus", "rawPayload": "...", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
gameplayId | string | Gameplay session id. |
gameplayType | number | Game category code. |
subType | number | Game subtype. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "battleGameplay", "data": { "type": "battleGameplay", "gameplayId": "7648486290535369480", "gameplayType": 2, "subType": 1, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
summary | string | AI summary text in TikTok's active locale. |
scenarioKey | string | Scenario key, e.g. "web_enter_room_asr_summary". |
iconUrl | string | TikTok CDN icon URL. |
labelKey | string | Label key, e.g. "ttlive_AIsummary_viewer_label". |
displayDurationMs | number | How long to display (ms). |
protoVersion | number | Always 3. |
Example Payload
{ "event": "aiSummary", "data": { "type": "aiSummary", "summary": "Casual dance session with spontaneous singing", "scenarioKey": "web_enter_room_asr_summary", "iconUrl": "https://...", "labelKey": "ttlive_AIsummary_viewer_label", "displayDurationMs": 3000, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
transactionId | string | Stable per-snapshot UUID (hex). |
protoVersion | number | Always 3. |
Example Payload
{ "event": "giftGallery", "data": { "type": "giftGallery", "transactionId": "202606070317035805FCD15954308C96D1", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
layoutSubtype | string | Cohost layout subtype ("cohost_normal_expand_4", ...). |
protoVersion | number | Always 3. |
Example Payload
{ "event": "cohostLayoutUpdate", "data": { "type": "cohostLayoutUpdate", "layoutSubtype": "cohost_normal_expand_4", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
fanType | number | Fan category code. |
eventKey | string | Fan-club event key, e.g. "ttlive_fanClub_communityHeartMe_tierUp_club". |
protoVersion | number | Always 3. |
Example Payload
{ "event": "fansEvent", "data": { "type": "fansEvent", "fanType": 7, "eventKey": "ttlive_fanClub_communityHeartMe_tierUp_club", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
changeType | number | Change category. |
sessionInfo | string | Packed session ids ("hostId_opponentId_subType"). |
protoVersion | number | Always 3. |
Example Payload
{ "event": "linkScreenChange", "data": { "type": "linkScreenChange", "changeType": 2, "sessionInfo": "7648433431395453718_16994212205653601499_0", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
stickerPayload | string | Sticker label + color + descriptor. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "roomSticker", "data": { "type": "roomSticker", "stickerPayload": "My label - BAYAN NG KONOHA - #000000", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
noticeKey | string | Notice localization key (e.g. "pm_mt_health_warning"). |
anchorUserId | string | Anchor (creator) user id. |
durationSec | number | How long the notice stays. |
riskKey | string | Risk descriptor. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "bottomMessage", "data": { "type": "bottomMessage", "noticeKey": "pm_mt_health_warning", "anchorUserId": "6925591924343669765", "durationSec": 599, "riskKey": "risk_notice7648487247360672544", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
status | number | Status code (1 = show, 3 = update, ...). |
productInfo | string | Product title + price. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "oecLiveShopping", "data": { "type": "oecLiveShopping", "status": 1, "productInfo": "Make Me Melt Makeup Removing Balm $23.80", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
templateKey | string | Template key, e.g. "pm_mt_topviewer_comment/{0:user} just became...". |
userId | string | User id this announcement is about. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "rankText", "data": { "type": "rankText", "templateKey": "pm_mt_topviewer_comment/{0:user} just became the top viewer", "userId": "7089122432367674373", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
status | number | Recall status. |
reason | string | Reason code, e.g. "CONTENT_CLASSIFICATION". |
durationSec | number | How long the suspension lasts. |
suspendKey | string | Localized suspend key. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "accessRecall", "data": { "type": "accessRecall", "status": 1, "reason": "CONTENT_CLASSIFICATION", "durationSec": 600, "suspendKey": "pm_mt_giftAccess_10MinSuspend_toast", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
showdownType | number | Showdown subtype. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "guestShowdown", "data": { "type": "guestShowdown", "showdownType": 4, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
hotKey | string | Surface descriptor, e.g. "revenue_high_traffic". |
protoVersion | number | Always 3. |
Example Payload
{ "event": "hotRoom", "data": { "type": "hotRoom", "hotKey": "revenue_high_traffic", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
sessionInfo | string | Packed portal session info. |
status | number | Portal status. |
kind | number | Portal kind. |
level | number | Portal level. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "envelopePortal", "data": { "type": "envelopePortal", "sessionInfo": "...", "status": 2, "kind": 2, "level": 2, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
userId | string | TikTok user id. |
nickname | string | Display name. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "groupLiveMemberNotify", "data": { "type": "groupLiveMemberNotify", "userId": "7467181997212075026", "nickname": "Kris", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
variant | string | Variant tag, e.g. "CustomPoll". |
action | string | Action descriptor, e.g. "shortTouchCustomPoll". |
refId | string | Reference id (poll / lucky-bag id). |
protoVersion | number | Always 3. |
Example Payload
{ "event": "shortTouch", "data": { "type": "shortTouch", "variant": "CustomPoll", "action": "shortTouchCustomPoll", "refId": "7648483782558993174", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
guideCode | number | Guide category code. |
rawPayload | string | Packed guide payload. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "linkMicAnchorGuide", "data": { "type": "linkMicAnchorGuide", "guideCode": 7, "rawPayload": "...", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
momentType | number | Moment category code. |
startedAtMs | number | Moment window start (ms epoch). |
endsAtMs | number | Moment window end (ms epoch). |
momentMsgId | string | Moment message id. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "gameMoment", "data": { "type": "gameMoment", "momentType": 3, "startedAtMs": 1780803292414, "endsAtMs": 1780803297414, "momentMsgId": "7648491877775774737", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
competitionType | number | Competition category code. |
contributorUserId | string | Contributor user id. |
receiverUserId | string | Receiving creator user id. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "competitionContributor", "data": { "type": "competitionContributor", "competitionType": 3, "contributorUserId": "7648483205057776397", "receiverUserId": "7648489576457587469", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
status | number | Round status. |
pictionaryId | string | Pictionary session id. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "pictionaryUpdate", "data": { "type": "pictionaryUpdate", "status": 2, "pictionaryId": "7648487101176056583", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
pictionaryId | string | Pictionary session id. |
answer | string | Revealed answer. |
status | number | End status code. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "pictionaryEnd", "data": { "type": "pictionaryEnd", "pictionaryId": "7648487101176056583", "answer": "dog", "status": 2, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
pictionaryId | string | Pictionary session id. |
exitReason | number | Exit reason code. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "pictionaryExit", "data": { "type": "pictionaryExit", "pictionaryId": "7648483515134167815", "exitReason": 1, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
status | number | Manager assignment status. |
managerNickname | string | Manager display name. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "oecLiveManager", "data": { "type": "oecLiveManager", "status": 2, "managerNickname": "Luna Volare", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
status | number | Billboard status. |
slotCount | number | Number of product slots. |
updatedAtMs | number | Last update timestamp (ms epoch). |
productPayload | string | Packed product info. |
flagsPayload | string[] | Display flags ("is_new0", "is_auto_display0", ...). |
protoVersion | number | Always 3. |
Example Payload
{ "event": "oecLiveBillboard", "data": { "type": "oecLiveBillboard", "status": 2, "slotCount": 5, "updatedAtMs": 1780802349750, "productPayload": "...", "flagsPayload": ["is_new0"], "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
productId | string | TikTok Shop product id. |
title | string | Product title as shown on the card. |
price | string | Displayed price string (already currency-formatted by TikTok). |
imageUrl | string | CDN URL for the product thumbnail. |
status | number | Card status (1 = shown / pinned, 0 = hidden). |
protoVersion | number | Always 3. |
Example Payload
{ "event": "shopProduct", "data": { "type": "shopProduct", "productId": "1729...", "title": "Make Me Melt Removing Balm", "price": "$23.80", "imageUrl": "https://...", "status": 1, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
products | object[] | Ordered list, each { productId, title, price, imageUrl, soldCount? }. |
total | number | Number of products in the tray. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "shopProductList", "data": { "type": "shopProductList", "total": 2, "products": [ { "productId": "1729...", "title": "Removing Balm", "price": "$23.80", "imageUrl": "https://..." }, { "productId": "1731...", "title": "Cleansing Oil", "price": "$19.00", "imageUrl": "https://..." } ], "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
productId | string | TikTok Shop product id this update applies to. |
stock | number | Remaining inventory for the product. |
soldCount | number | Units sold so far in this live session. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "shopInventory", "data": { "type": "shopInventory", "productId": "1729...", "stock": 142, "soldCount": 58, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
slotCount | number | Number of product slots on the wall. |
products | object[] | Slot contents, each { slot, productId, title, price, imageUrl }. |
updatedAtMs | number | Last update timestamp (ms epoch). |
protoVersion | number | Always 3. |
Example Payload
{ "event": "shopBillboard", "data": { "type": "shopBillboard", "slotCount": 3, "updatedAtMs": 1780802349750, "products": [ { "slot": 1, "productId": "1729...", "title": "Removing Balm", "price": "$23.80", "imageUrl": "https://..." } ], "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
perceptionCode | number | Perception category code. |
action | string | Action descriptor, e.g. "muting_cancel<msgId>". |
protoVersion | number | Always 3. |
Example Payload
{ "event": "perception", "data": { "type": "perception", "perceptionCode": 8, "action": "muting_cancel7648486963015877389", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
questionText | string | Question text. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "questionSelected", "data": { "type": "questionSelected", "questionText": "Quel est ton acteur ou ton actrice preferee?", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
questionId | string | Question id. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "questionSlideDown", "data": { "type": "questionSlideDown", "questionId": "7648490528341835796", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
iconUrl | string | CDN icon URL for the unlocked gift. |
tooltipKey | string | Localized tooltip key. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "giftUnlock", "data": { "type": "giftUnlock", "iconUrl": "https://p16-webcast.tiktokcdn.com/...", "tooltipKey": "ttlive_commGift_postReveal_tooltip", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
refreshToken | string | Refresh token / item identifier. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "ecShortItemRefresh", "data": { "type": "ecShortItemRefresh", "refreshToken": "shortTouchEcommerceLuckyBag7648485714367643661", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
imageUrl | string | CDN icon URL. |
titleKey | string | Localized title key. |
btnKey | string | Localized button key. |
deepLink | string | Deep link target. |
reminderKey | string | Reminder category key. |
displayDurationSec | number | How long to display the capsule. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "capsule", "data": { "type": "capsule", "imageUrl": "https://...", "titleKey": "ttlive_servicePlus_live_modPinCapsule", "btnKey": "ttlive_servicePlus_live_modPinCapsule_btnPin", "deepLink": "sslocal://webcast_subscribe/allinone?sec_anchor_id=...", "reminderKey": "high_intention_comment_pincard_reminder", "displayDurationSec": 90, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
verifyCode | number | Verify category code. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "roomVerify", "data": { "type": "roomVerify", "verifyCode": 3, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
boardPayload | string | Packed board payload. |
status | number | Board status. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "smbBoard", "data": { "type": "smbBoard", "boardPayload": "...", "status": 1, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
pictionaryId | string | Round identifier. |
status | number | Lifecycle status code. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "pictionaryStart", "data": { "type": "pictionaryStart", "pictionaryId": "7648...", "status": 1, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
settingsPayload | string | Packed settings payload. |
status | number | Update status. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "cohostSettingsUpdate", "data": { "type": "cohostSettingsUpdate", "settingsPayload": "...", "status": 1, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
collectionId | string | Gift collection identifier. |
status | number | Update status. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "giftCollectionUpdate", "data": { "type": "giftCollectionUpdate", "collectionId": "76...", "status": 1, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
noticeText | string | Notice text shown to viewers. |
gameId | string | Originating game identifier. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "gameAutoPostNotice", "data": { "type": "gameAutoPostNotice", "noticeText": "New high score!", "gameId": "106", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
pinnedText | string | The pinned comment text. |
user | WebcastUser | Subscriber who pinned the comment. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "subPin", "data": { "type": "subPin", "pinnedText": "Good luck!", "user": { "uniqueId": "vipfan" }, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
toastText | string | Toast body text. |
durationMs | number | How long to display, in milliseconds. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "toast", "data": { "type": "toast", "toastText": "Connected to LIVE", "durationMs": 3000, "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
guideKey | string | Identifier for the guide template TikTok wants rendered. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "gapHighlightPushGuide", "data": { "type": "gapHighlightPushGuide", "guideKey": "ttlive_push_guide_v1", "protoVersion": 3 } }Fields
| Name | Type | Description |
|---|---|---|
trackId | string | Identifier of the requested track. |
trackName | string | Display name of the requested track. |
status | number | Queue / request status. |
protoVersion | number | Always 3. |
Example Payload
{ "event": "karaokeReq", "data": { "type": "karaokeReq", "trackId": "12345", "trackName": "Bohemian Rhapsody", "status": 1, "protoVersion": 3 } }Live Gifter Firehose
Single WebSocket that streams gift events from every live TikTok creator we monitor, globally, on one connection. Not a per-creator subscription. Tracking 1, 100, or 10,000 concurrent live creators consumes the same slot count: one.
Endpoint
wss://api.tik.tools/firehose/gifters?apiKey=YOUR_KEYHow it works
Server-side, every gift event from every monitored LIVE is published to a shared pub/sub channel. Your WebSocket subscribes to that channel and the server fans every matching event to you in real time. No per-creator WebSocket. No per-creator REST call. No watchlist registration. The firehose ingests two upstream channels and dedupes cross-channel duplicates within a 60-second window so you never receive the same gift twice.
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
apiKey | string | required | Your API key. Tier must be agency_global. |
mode | enum | global | global | region | league |
region | string | - | Region code (e.g. KR+, US+, DE+, BK). Required when mode=region or mode=league. |
league | string | - | League class (e.g. A1, B2, P1). Required when mode=league. Filter applies to whale-tier events (≥30k diamonds) where league metadata is available. |
min_diamonds | int | 1 | Skip events below this diamond count. Lower the floor for full-coverage tracking, raise for whale-only alerting. |
Event shape
{
"type": "gifter_alert",
"ts": "2026-06-07T18:33:07.829Z",
"gifter": {
"username": "lajan.eli",
"displayName": "lajan.eli",
"isAnonymous": false
},
"creator": {
"uniqueId": "globalboy_ry"
},
"gift": {
"name": "Meteor Shower",
"totalDiamonds": 3000
},
"region": "US+",
"classType": null
}Filter updates mid-stream
Send a JSON frame to switch filter without reconnecting:
{
"type": "update_filter",
"mode": "region",
"region": "KR+",
"min_diamonds": 1000
}Capacity model
The Firehose is a fan-out stream. Your connection slot count is fixed at one regardless of how many creators are live. This sits outside the per-creator concurrent-WS budget shown in Rate Limits — the Firehose slot does not subtract from your 500 concurrent-WS Global Agency allowance.
Example client
// firehose-client.mjs
// Node 22+. One WS = every live creator globally.
import WebSocket from 'ws'
const KEY = process.env.TIKTOOL_KEY
const url = 'wss://api.tik.tools/firehose/gifters'
+ '?apiKey=' + KEY
+ '&mode=region®ion=KR+'
+ '&min_diamonds=1000'
const ws = new WebSocket(url)
ws.on('open', () => console.log('Firehose subscribed'))
ws.on('message', (raw) => {
const e = JSON.parse(raw.toString())
if (e.type !== 'gifter_alert') return
console.log(`[${e.region}] ${e.gifter.username} → @${e.creator.uniqueId} ` +
`${e.gift.name} (${e.gift.totalDiamonds}\u{1F48E})`)
})
ws.on('close', (code) => console.log('Closed', code))Rate Limits
Rate limits are applied per API key. The per-minute throttle is a sliding window; daily counters reset at 00:00 UTC. The canonical, always-current feature matrix lives on the pricing page.
| Tier | API Req/min | API Req/day | Concurrent WS | WS Max Duration | Bulk Check |
|---|---|---|---|---|---|
| Sandbox | 5 (300/h) | 2,500 | 1 | 2h | - |
| Basic | 60 | 10,000 | 20 | 8h | 10/req |
| Pro | 300 | 75,000 | 50 | 12h | 50/req |
| Ultra | 1,000 | 300,000 | 250 | 24h | 100/req |
| Global Agency | 5,000 | 1,000,000 | 500 | 24h | 200/req |
Rate limit headers are included in every response:
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 28
X-RateLimit-Reset: 1234567890All daily counters - WebSocket connection attempts and feed discovery calls - reset at midnight UTC (00:00 UTC) every day. The dashboard reflects the same UTC boundary in real time.
Managed Infrastructure - No Setup Required
In Relayed mode, all upstream sessions are handled by the TikTools managed edge. Your application connects to a single, stable endpoint: api.tik.tools.
Your API key limit (50 for Pro, 500 for Ultra) is the only cap that applies. No infrastructure setup needed on your end.
WebSocket Close Codes
Custom close codes used when the WebSocket connection is terminated.
| Code | Name | Description |
|---|---|---|
1000 | NORMAL | Normal closure |
4005 | STREAM_END | The live stream ended |
4006 | NO_MESSAGES_TIMEOUT | No messages received for 60 seconds |
4400 | INVALID_OPTIONS | Missing or invalid connection parameters |
4401 | INVALID_AUTH | Invalid API key or JWT |
4403 | NO_PERMISSION | JWT does not allow access to this creator |
4404 | NOT_LIVE | The user is not currently live |
4429 | TOO_MANY_CONNECTIONS | Exceeded WebSocket connection limit |
4500 | TIKTOK_CLOSED | Upstream TikTok connection closed |
4555 | MAX_LIFETIME | Connection exceeded 8-hour max lifetime |
4556 | WEBCAST_FETCH_ERROR | Failed to fetch webcast data |
Live Captions
Real-time speech-to-text transcription for TikTok LIVE streams. AI-powered with speaker diarization, multi-language support, and sub-second latency.
NEWNo-code OBS overlay - get a single URL to paste into Browser Source for real-time captions on your stream->Connection
wss://api.tik.tools/captions?uniqueId=USERNAME&apiKey=YOUR_KEY&language=en&translate=en&diarization=true&max_duration_minutes=60Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
uniqueId | string | Yes | TikTok username to caption |
apiKey | string | Yes | Your API key |
language | string | No | Source language hint (empty = auto-detect) |
translate | string | No | Target translation language code. Only ONE language per session (multi-translate rejected) |
diarization | boolean | No | Enable speaker identification (default: true) |
max_duration_minutes | number | No | Auto-disconnect after N minutes (default: 60, max: 300). Forces reconnect to prevent runaway sessions. |
Events
| Type | Fields | Description |
|---|---|---|
caption | text, speaker, isFinal, language | Real-time caption text (partial and final) |
translation | text, speaker, targetLanguage | Translated caption text |
status | status, message | Session status updates (connecting, transcribing, ended) |
credits | total, used, remaining | Credit balance update (sent every 30s) |
credits_low | remaining, percentage | Low credit warning (≤20% remaining) |
error | code, message | Error event |
Example Payload
{ "type": "caption", "text": "Hello everyone, welcome to my stream!", "speaker": "Speaker 1", "isFinal": true, "language": "en" }Code Examples
import WebSocket from 'ws';
const ws = new WebSocket(
'wss://api.tik.tools/captions?uniqueId=streamer&apiKey=YOUR_KEY&translate=en&max_duration_minutes=120'
);
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
switch (msg.type) {
case 'caption':
const prefix = msg.speaker ? `[${msg.speaker}] ` : '';
console.log(`${prefix}${msg.text}${msg.isFinal ? ' ✓' : '...'}`);
break;
case 'translation':
console.log(` → ${msg.text}`);
break;
case 'credits':
console.log(`Credits: ${msg.remaining}/${msg.total} min remaining`);
break;
case 'status':
console.log(`Status: ${msg.status}`);
break;
}
});SDK Usage
import { TikTokCaptions } from '@tiktool/live';
const captions = new TikTokCaptions({
apiKey: process.env.TIKTOOL_API_KEY,
uniqueId: 'streamer_name',
translate: 'en',
diarization: true,
});
captions.on('caption', (event) => {
console.log(`[${event.speaker}] ${event.text}`);
});
captions.on('translation', (event) => {
console.log(` → ${event.text}`);
});
captions.on('credits', (event) => {
console.log(`${event.remaining}/${event.total} min remaining`);
});
captions.on('credits_low', (event) => {
console.warn(`Low credits! ${event.remaining} min left`);
});
await captions.start();
// captions.stop() to endPricing - Subscription Plans
Captions are sold as recurring subscription plans by hours of stream time. 1 hour = 60 minutes of transcription or translation in one target language. No API key required.
| Plan | Weekly | Monthly | Per-min (monthly) |
|---|---|---|---|
| Casual | 12h / $7 | 60h / $29 | $0.0081 |
| Pro | 30h / $15 | 140h / $59 | $0.0070 |
| Extreme | 60h / $29 | 260h / $99 | $0.0064 |
Basic ships 12h/wk - 60h/mo. Pro 30h/wk - 140h/mo. Ultra 60h/wk - 260h/mo. Global Agency 120h/wk - 500h/mo. Hours reset every renewal and do not roll over.
Plans auto-renew weekly or monthly. If you run out of hours mid-period, the next renewal fires early so captions never stop. Cancel any time on /billing.
See captions in action on a real TikTok LIVE stream at tik.tools/captions
Unreal Engine Plugin
Native Unreal Engine plugin with full Blueprint and C++ support. Access TikTok LIVE data directly inside your UE project - no HTTP calls, no external processes.
Installation
YourProject/
├── Plugins/
│ └── TikToolLive/
│ ├── TikToolLive.uplugin
│ └── Source/
│ └── TikToolLive/
│ ├── Public/
│ │ ├── TikToolLiveSubsystem.h
│ │ ├── TikToolSettings.h
│ │ └── TikToolTypes.h
│ └── Private/
│ └── TikToolLiveSubsystem.cpp- Copy the
TikToolLivefolder into your project'sPlugins/directory - Regenerate project files (right-click
.uproject→ Generate Visual Studio project files) - Open the editor → Edit → Plugins → verify TikToolLive is enabled
Project Settings
Configure via Edit → Project Settings → Plugins → TikTool Live:
| Setting | Type | Default | Description |
|---|---|---|---|
ApiKey | FString | "" | Your TikTool API key |
DefaultUniqueId | FString | "" | TikTok username (without @) |
bAutoConnect | bool | false | Connect automatically on game start |
bAutoReconnect | bool | true | Reconnect on disconnection |
MaxReconnectAttempts | int32 | 5 | Max reconnect retries |
HeartbeatInterval | float | 30.0 | Seconds between heartbeats |
bDebugLogging | bool | false | Enable verbose log output |
Blueprint Quick Start
Zero C++ required. Three steps to receive live events in any Blueprint:
- Search for "Get TikTool Live Subsystem" from any Blueprint node graph
- Call Connect (or enable Auto Connect in Project Settings)
- Bind to events: drag from the subsystem → Assign OnChatEvent, Assign OnGiftEvent, etc.
Connection API
| Function | Returns | Description |
|---|---|---|
Connect(UniqueId) | void | Connect to a TikTok LIVE stream. Uses DefaultUniqueId if empty. |
Disconnect() | void | Close the WebSocket connection |
IsConnected() | bool | Check if currently connected |
GetRoomId() | FString | Get the current room ID |
GetViewerCount() | int32 | Current concurrent viewer count |
GetEventCount() | int32 | Total events received this session |
Events
All events are BlueprintAssignable multicast delegates. Bind in Blueprint (Assign node) or C++ (AddDynamic):
| Delegate | Payload | Description |
|---|---|---|
OnChatEvent | FTikToolChatEvent | Chat message received |
OnGiftEvent | FTikToolGiftEvent | Gift sent by viewer |
OnLikeEvent | FTikToolLikeEvent | Likes received |
OnMemberEvent | FTikToolMemberEvent | Viewer joined stream |
OnFollowEvent | FTikToolFollowEvent | New follower |
OnShareEvent | FTikToolShareEvent | Stream shared |
OnSubscribeEvent | FTikToolSubscribeEvent | Subscription |
OnViewerCountUpdate | FTikToolViewerEvent | Viewer count changed |
OnBattleUpdate | FTikToolBattleEvent | Battle status update |
OnEmoteChatEvent | FTikToolEmoteChatEvent | Emote in chat |
OnQuestionEvent | FTikToolQuestionEvent | Q&A question |
OnEnvelopeEvent | FTikToolEnvelopeEvent | Treasure box / envelope |
OnStreamEnd | - | Stream ended |
OnConnected | - | Successfully connected |
OnDisconnected | - | Connection lost |
C++ Example
#include "TikToolLiveSubsystem.h"
void AMyChatActor::BeginPlay()
{
Super::BeginPlay();
auto* TikTool = GetGameInstance()->GetSubsystem<UTikToolLiveSubsystem>();
if (!TikTool) return;
// Bind to chat events
TikTool->OnChatEvent.AddDynamic(this, &AMyChatActor::HandleChat);
TikTool->OnGiftEvent.AddDynamic(this, &AMyChatActor::HandleGift);
// Connect (uses DefaultUniqueId from Project Settings)
TikTool->Connect();
}
void AMyChatActor::HandleChat(const FTikToolChatEvent& Event)
{
UE_LOG(LogTemp, Log, TEXT("%s: %s"),
*Event.User.Nickname, *Event.Comment);
}
void AMyChatActor::HandleGift(const FTikToolGiftEvent& Event)
{
UE_LOG(LogTemp, Log, TEXT("%s sent %s x%d"),
*Event.User.Nickname, *Event.GiftName, Event.RepeatCount);
}