Public TikTok LIVE data — who's ranking right now, who's gifting the most, what's happening inside a room this second — is awkward to fetch and easy to get wrong. Anonymous scraping breaks, the shapes are undocumented, and nothing tells you how fresh or how complete an answer actually is.
TokTik is a typed, metered API over observed public TikTok LIVE activity, with official SDKs for JavaScript/TypeScript and Python. Every REST response carries a provenance envelope so you always know what you're looking at, and there's a realtime event stream for chat / gift / like frames over WebSocket.
Here's the whole "hello world."
Independent project — not affiliated with, or endorsed by, TikTok. The data is observed from public LIVE surfaces and labelled as such; it is not an official TikTok feed, and it covers a stated, expanding Coverage Set rather than all of TikTok.
REST in 5 lines
JavaScript / TypeScript
npm i @toktikhq/sdk-js
import { TokTikClient } from "@toktikhq/sdk-js";
const client = new TokTikClient({ apiKey: process.env.TOKTIK_API_KEY! });
const board = await client.rankings.official({ board: "hourly", region: "VN" });
console.log(board.provenance.freshness, board.data.board.entries);
Python
pip install toktik
from toktik import TokTikClient
client = TokTikClient(api_key="ttk_live_...")
board = client.rankings.official(board="hourly", region="VN")
print(board["provenance"], board["data"]["board"]["entries"])
Same surface in both languages: official observed rankings (hourly / daily / regional boards), movers, gifter and whale leaderboards, creator lookups and changes, videos and comments. Arguments are the language-native casing (camelCase in JS, snake_case in Python); the SDK maps them to the wire names, and a wrong key is a hard 400 rather than a silently ignored filter.
Provenance is part of the answer
Much of this data is observed, not officially published — so pretending it's ground truth is a bug. Every data method returns { data, provenance }, and the SDK never strips the envelope:
-
freshness—near_realtime/stale/historical -
source— where the observation came from -
coverageStatus— whether this slice is inside the Coverage Set
Branch on it before you trust a number:
const board = await client.rankings.official({ board: "daily", region: "US" });
if (board.provenance.freshness === "historical") {
// last snapshot is old — label it in your UI instead of showing it as "now"
}
for (const entry of board.data.board.entries) {
console.log(entry.rank, entry.creator.handle);
}
That envelope is the difference between "a ranking" and "a ranking you can honestly put in front of a customer."
Realtime — the differentiator
Rankings are a snapshot; a LIVE room is a firehose. client.live.stream(...) gives you the firehose in three lines and owns the parts that are genuinely annoying:
const stream = await client.live.stream(["@some.creator"], {
onEvent: (f) => console.log(f.event, f.data), // chat | gift | like | ...
onStatus: (f) => console.log(f.status, f.reason), // queued | active | offline | unavailable
onError: (e) => console.error(e),
onReconnect: () => console.log("resumed"),
});
What the SDK handles for you:
- Handshake token lifetime — it mints a short-lived token per connection, so an expiry mid-stream costs a reconnect, not a dead socket.
- Secure token transport — the token rides in the WebSocket subprotocol, never in the URL (URLs get logged; tokens shouldn't).
- Reconnect with resume — backoff-with-jitter, then resubscribe on resume, so a blip doesn't drop your subscriptions.
The statuses are deliberately honest: queued means "we've accepted the creator but aren't attached yet," active means frames are flowing, offline / unavailable mean exactly what they say — no faking a live feed that isn't there.
Python gets the same stream as an async iterator:
pip install "toktik[realtime]" # adds the websockets dependency
import asyncio
from toktik import TokTikClient, EventFrame
async def main():
client = TokTikClient(api_key="ttk_live_...")
async for frame in await client.live.stream(["@some.creator"]):
if isinstance(frame, EventFrame):
print(frame.event, frame.data) # chat / gift / like / ...
asyncio.run(main())
Errors that mean something
A non-2xx raises TokTikApiError, which keeps the status distinguishable instead of collapsing everything into one exception — because the right reaction is different each time:
import { TokTikClient, TokTikApiError } from "@toktikhq/sdk-js";
try {
await client.creators.get("someone");
} catch (error) {
if (error instanceof TokTikApiError) {
error.isPaymentRequired; // 402 — out of credits; retrying won't help, top up
error.isForbidden; // 403 — your key lacks the scope this resource is sold under
error.isUnauthorized; // 401 — bad or expired key
error.retryable; // true only for 429 + 5xx — safe to back off and retry
}
}
Python mirrors it: is_payment_required (402), is_forbidden (403), is_rate_limited (429), and retryable, each carrying status, code, and request_id. 402 means buy credits, 403 means the key is missing a scope, 429 means back off — three very different fixes, three distinguishable errors.
Start
There's a free tier to kick the tyres, then metered credits — prepaid, no surprise invoices. Point your key at the quickstart above and you have observed rankings, gifter leaderboards, and a realtime LIVE stream behind one typed client.
- Docs: https://toktikhq.com/docs
-
JS SDK:
@toktikhq/sdk-js· Python SDK:toktik(both MIT) - OpenAPI: https://api.toktikhq.com/openapi.json
TokTik is an independent product. It is not affiliated with or endorsed by TikTok; its data is observed from public surfaces, labelled with provenance, and bounded to a stated Coverage Set.
Top comments (0)