Connect to any TikTok Live stream and receive real-time events in Lua — chat, gifts, likes, joins, viewer counts, and 65 decoded event types. Poll-based API for game engine integration. No signing server, no API keys, no authentication required.
local PirateTok = require "piratetok"
-- Create client with builder pattern — cdn("eu") picks the EU endpoint
local client = PirateTok.builder("username_here")
:cdn("eu")
:build()
-- Register event callbacks — poll-based, ideal for game engines (Love2D, Defold)
client:on("chat", function(msg)
local name = msg.user and msg.user.nickname or "?"
print("[chat] " .. name .. ": " .. (msg.comment or ""))
end)
client:on("gift", function(msg)
local name = msg.user and msg.user.nickname or "?"
local gift = msg.gift_details and msg.gift_details.name or "gift"
local diamonds = PirateTok.events.diamond_total(msg)
print("[gift] " .. name .. " sent " .. gift .. " (" .. diamonds .. " diamonds)")
end)
client:on("like", function(msg)
local name = msg.user and msg.user.nickname or "?"
print("[like] " .. name .. " (" .. (msg.total or 0) .. " total)")
end)
-- Blocks until disconnected — heartbeat and reconnection handled internally
client:run()luarocks install piratetok-live-luaOr install dependencies manually and copy the source:
luarocks install luasocket
luarocks install luasec
luarocks install lua-protobuf
luarocks install lua-zlib
luarocks install lua-cjsonThen copy the piratetok/ directory into your project.
| Language | Install | Repo |
|---|---|---|
| Rust | cargo add piratetok-live-rs |
live-rs |
| Go | go get github.com/PirateTok/live-go |
live-go |
| Python | pip install piratetok-live-py |
live-py |
| JavaScript | npm install piratetok-live-js |
live-js |
| C# | dotnet add package PirateTok.Live |
live-cs |
| Java | com.piratetok:live |
live-java |
| Elixir | {:piratetok_live, "~> 0.1"} |
live-ex |
| Dart | dart pub add piratetok_live |
live-dart |
| C | #include "piratetok.h" |
live-c |
| PowerShell | Install-Module PirateTok.Live |
live-ps1 |
| Shell | bpkg install PirateTok/live-sh |
live-sh |
Poll-based API — call client:poll() in your game loop instead of client:run():
local PirateTok = require "piratetok"
local messages = {}
local client = PirateTok.builder("username_here"):build()
client:on("chat", function(msg)
local name = msg.user and msg.user.nickname or "?"
table.insert(messages, name .. ": " .. (msg.comment or ""))
end)
client:connect()
function love.update(dt)
client:poll() -- non-blocking, reads available frames
end
function love.draw()
for i, line in ipairs(messages) do
love.graphics.print(line, 10, i * 20)
end
end- Zero signing dependency — no API keys, no signing server, no external auth
- 65 decoded event types — chat, gifts, likes, joins, follows, shares, battles, polls, envelopes, and more
- Poll-based API —
client:poll()for game loops,client:run()for standalone scripts - Auto-reconnection — stale detection, exponential backoff, self-healing auth
- Enriched User data — badges, gifter level, moderator status, follow info, fan club
- Sub-routed convenience events —
follow,share,join,live_endedfire alongside raw events - No protoc — protobuf schemas defined inline via lua-protobuf, no codegen, no build tools
local client = PirateTok.builder("username_here")
:cdn("eu") -- "eu" / "us" / "global" (default)
:timeout(15) -- HTTP timeout in seconds (default 10)
:heartbeat_interval(10) -- seconds between heartbeats (default 10)
:stale_timeout(90) -- reconnect after N seconds of silence (default 60)
:max_retries(10) -- consecutive failed reconnects before giving up (default 5)
:proxy("http://user:pass@host:port") -- HTTP CONNECT proxy for HTTP + WSS (Basic auth optional; SOCKS not supported)
:compress(false) -- disable gzip compression for WSS payloads (default true)
:user_agent("Mozilla/...") -- override random UA rotation with a fixed user-agent
:cookies("sessionid=xxx; sid_tt=xxx") -- session cookies for 18+ room info
:language("en") -- override detected system language (two-letter code)
:region("US") -- override detected system region (two-letter code)
:build()| Event | Callback data |
|---|---|
chat |
.user, .comment |
gift |
.user, .gift_details, .repeat_count, .repeat_end, .combo_count |
like |
.user, .count, .total |
join |
.user (sub-routed from MemberMessage) |
follow |
.user (sub-routed from SocialMessage) |
share |
.user (sub-routed from SocialMessage) |
room_user_seq |
.viewer_count, .total_user, .anonymous, .pop_str, .ranks_list (use PirateTok.top_viewers(data)) |
live_ended |
.reason (sub-routed from ControlMessage) |
connected |
.room_id |
reconnecting |
.attempt, .max_retries, .delay_secs, .reason |
disconnected |
.reason |
error |
error table (use PirateTok.errors.format(err)) |
unknown |
.method, .payload (raw bytes for unhandled types) |
Plus 58 more decoded types: emote_chat, poll, envelope, rank_update, link_mic_battle, etc.
local PirateTok = require "piratetok"
local result, err = PirateTok.check_online("username_here")
if not result then
print(PirateTok.errors.format(err)) -- "not found" / "not online" / "blocked"
else
print("LIVE — room_id: " .. result.room_id .. " anchor_id: " .. tostring(result.anchor_id))
endclient:on("room_user_seq", function(seq)
for _, c in ipairs(PirateTok.top_viewers(seq)) do
print(c.rank, c.user.nickname, c.score)
end
end)The full viewer list behind the web viewer panel. Session cookies are required for this call only — without them you get SessionRequired.
local live = PirateTok.check_online("username_here")
local aud, err = PirateTok.fetch_room_audience(
live.room_id, live.anchor_id, "sessionid=abc; sid_tt=abc")
if aud then
print(aud.total, aud.anonymous)
for _, v in ipairs(aud.viewers) do print(v.rank, v.username, v.score) end
endanchor_id may be nil — it is then resolved via room info (one extra request).
-- Normal rooms — no cookies needed
local info, err = PirateTok.fetch_room_info("ROOM_ID")
-- 18+ rooms — pass session cookies from browser DevTools
local info, err = PirateTok.fetch_room_info("ROOM_ID", 10, "sessionid=abc; sid_tt=abc")client:on("gift", function(msg)
local events = PirateTok.events
if events.is_combo_gift(msg) then
if events.is_streak_over(msg) then
print("x" .. msg.repeat_count .. " = " .. events.diamond_total(msg) .. " diamonds")
end
else
print(events.diamond_total(msg) .. " diamonds")
end
end)- Resolves username to room ID via TikTok JSON API
- Authenticates and opens a direct WSS connection
- Sends protobuf heartbeats every 10s to keep alive
- Decodes protobuf event stream into Lua tables
- Auto-reconnects on stale/dropped connections, reusing the ttwid + UA; rotates them only on
DEVICE_BLOCKEDor a session that died within 30 s. A session that stayed up 30 s resets the retry counter
All protobuf schemas are defined inline via lua-protobuf — no .proto files, no codegen, no build-time dependencies.
Every HTTPS / WSS connection verifies the server certificate chain and the hostname (subjectAltName). The CA bundle comes from the system trust store: $SSL_CERT_FILE / $SSL_CERT_DIR if set, otherwise the standard locations on Linux, the BSDs and macOS (/etc/ssl/certs/ca-certificates.crt, /etc/pki/tls/certs/ca-bundle.crt, /etc/ssl/cert.pem, …). Windows has no file-based store luasec can read — set SSL_CERT_FILE to a PEM bundle (e.g. the one shipped with curl or Git for Windows); without it connections fail with "no CA trust store found". To pin a custom bundle in code: require("piratetok.tls").ca_file = "/path/ca.pem".
Requires Lua 5.1+ (including LuaJIT) with unrestricted system access (raw TCP sockets). Works in:
- Love2D — poll-based API designed for game loops
- Defold — with native extension packaging for C deps
- Standalone LuaJIT / Lua — scripts, bots, monitoring tools
Does not work in sandboxed Lua environments (Roblox, Neovim, FiveM, OpenResty) — these restrict socket access and/or use incompatible Lua variants.
luajit examples/basic_chat.lua <username> # connect + print chat events
luajit examples/online_check.lua <username> # check if user is live
luajit examples/stream_info.lua <username> # fetch room metadata + stream URLs
luajit examples/gift_streak.lua <username> # gift streak tracking with diamond totals
luajit examples/profile_lookup.lua <username> # fetch profile metadata + avatars (cached)
luajit examples/audience.lua <username> <cookies> # full viewer roster (session cookies required)See examples/love2d/ for Love2D game engine integration.
Deterministic cross-lib validation against binary WSS captures. Requires testdata from a separate repo:
git clone https://github.com/PirateTok/live-testdata ../live-testdata
make testMissing testdata is a test failure, not a skip. Lookup order: $PIRATETOK_TESTDATA, testdata/, ../live-testdata/ (manifests in manifests/ or captures/manifests/). The _raw (uncompressed) captures are not in live-testdata — supply them via testdata/ or PIRATETOK_TESTDATA.
make test also runs tests/unit_test.lua — offline tests for ttwid retry, reconnect policy, acks, ranks_list/top_viewers, audience / room info parsing and check_online error mapping — and tests/wire_test.lua, which drives the real client through a local Basic-auth CONNECT proxy + TLS fake (requires openssl to make the test cert).
0BSD
