Tok
Downloads
Donate
Every TikTok Live library on the internet funnels your traffic through a proprietary signing server — ours connect directly — no middleman — no API keys — no fees
12 native implementations — same protocol — same events — same freedom
Every other TikTok Live library depends on a proprietary signing server — a single point of failure no one controls
Every library ships the same feature set — same protocol, same helpers, same control
Gzip is on by default — 3–5× bandwidth savings. Set .compress(false) and payloads arrive as raw protobuf with zero CPU overhead per frame. The decode pipeline checks for gzip magic bytes automatically, so you can toggle between reconnects without breaking anything.
Single stream? Leave gzip on. Monitoring hundreds of rooms on the same box? Raw protobuf frees the CPU bottleneck. The tradeoff is yours to make.
let mut stream = TikTokLive::builder("username")
.compress(false) // <-- raw protobuf, no gzip
.connect()
.await?;
while let Some(event) = stream.next_event().await {
match event {
TikTokLiveEvent::Chat(msg) => {
let nick = msg.user.as_ref().map_or("?", |u| u.nickname.as_str());
println!("{nick}: {}", msg.comment);
}
TikTokLiveEvent::Disconnected => break,
_ => {}
}
}client = TikTokLiveClient("username")
client.compress = False # <-- raw protobuf, no gzip
@client.on("chat")
async def on_chat(event):
print(f"{event.user.nickname}: {event.comment}")
@client.on("disconnect")
async def on_disconnect(event):
print("stream ended")
await client.connect() # blocks until disconnectconst client = new TikTokLiveClient("username")
.compress(false) // <-- raw protobuf, no gzip
client.on("chat", (msg) => {
console.log(`${msg.user.nickname}: ${msg.comment}`)
})
client.on("disconnect", () => {
console.log("stream ended")
})
await client.connect() // blocks until disconnectclient := golive.NewClient("username").
Compress(false) // <-- raw protobuf, no gzip
client.OnChat(func(msg *golive.ChatMessage) {
fmt.Printf("%s: %s\n", msg.User.Nickname, msg.Comment)
})
client.OnDisconnect(func() {
fmt.Println("stream ended")
})
err := client.Connect() // blocks until disconnectvar client = new TikTokLiveClient("username")
.Compress(false); // <-- raw protobuf, no gzip
client.OnChat += (sender, msg) => {
Console.WriteLine($"{msg.User.Nickname}: {msg.Comment}");
};
client.OnDisconnect += (sender, e) => {
Console.WriteLine("stream ended");
};
await client.Connect(); // blocks until disconnectvar client = new PirateTokClient("username")
.compress(false); // <-- raw protobuf, no gzip
client.onChat(msg -> {
System.out.printf("%s: %s%n", msg.getUser().getNickname(), msg.getComment());
});
client.onDisconnect(() -> {
System.out.println("stream ended");
});
client.connect(); // blocks until disconnectlocal client = PirateTok.builder("username")
:compress(false) -- <-- raw protobuf, no gzip
:build()
client:on("chat", function(msg)
print(msg.user.nickname .. ": " .. msg.comment)
end)
client:on("disconnect", function()
print("stream ended")
end)
client:connect() -- blocks until disconnect{:ok, pid} = PirateTok.Live.connect("username",
compress: false # <-- raw protobuf, no gzip
)
receive do
{:chat, msg} ->
IO.puts("#{msg.user.nickname}: #{msg.comment}")
:disconnected ->
IO.puts("stream ended")
endfinal client = TikTokLiveClient("username")
.compress(false); // <-- raw protobuf, no gzip
client.on<ChatEvent>((msg) {
print("${msg.user.nickname}: ${msg.comment}");
});
client.on<DisconnectEvent>((_) {
print("stream ended");
});
await client.connect(); // blocks until disconnectIf no data arrives within stale_timeout (default 60s), the connection is torn down and rebuilt from scratch — fresh ttwid, fresh WSS handshake, fresh heartbeat. Exponential backoff between retries. Set max_retries(0) to disable the limit entirely.
Your event loop sees nothing. The stream self-heals without a single line of retry logic in your code.
let mut stream = TikTokLive::builder("username")
.max_retries(10) // <-- 0 = unlimited
.stale_timeout(Duration::from_secs(90)) // <-- tear down after 90s silence
.connect()
.await?;
while let Some(event) = stream.next_event().await {
match event {
TikTokLiveEvent::Chat(msg) => {
let nick = msg.user.as_ref().map_or("?", |u| u.nickname.as_str());
println!("{nick}: {}", msg.comment);
}
TikTokLiveEvent::Disconnected => break,
_ => {}
}
}client = TikTokLiveClient("username")
client.max_retries = 10 # <-- 0 = unlimited
client.stale_timeout = 90 # <-- tear down after 90s silence
@client.on("chat")
async def on_chat(event):
print(f"{event.user.nickname}: {event.comment}")
@client.on("disconnect")
async def on_disconnect(event):
print("stream ended")
await client.connect()const client = new TikTokLiveClient("username")
.maxRetries(10) // <-- 0 = unlimited
.staleTimeout(90_000) // <-- tear down after 90s silence
client.on("chat", (msg) => {
console.log(`${msg.user.nickname}: ${msg.comment}`)
})
client.on("disconnect", () => {
console.log("stream ended")
})
await client.connect()client := golive.NewClient("username").
MaxRetries(10). // <-- 0 = unlimited
StaleTimeout(90 * time.Second) // <-- tear down after 90s silence
client.OnChat(func(msg *golive.ChatMessage) {
fmt.Printf("%s: %s\n", msg.User.Nickname, msg.Comment)
})
client.OnDisconnect(func() {
fmt.Println("stream ended")
})
err := client.Connect()var client = new TikTokLiveClient("username")
.MaxRetries(10) // <-- 0 = unlimited
.StaleTimeout(TimeSpan.FromSeconds(90)); // <-- 90s silence
client.OnChat += (sender, msg) => {
Console.WriteLine($"{msg.User.Nickname}: {msg.Comment}");
};
client.OnDisconnect += (sender, e) => {
Console.WriteLine("stream ended");
};
await client.Connect();var client = new PirateTokClient("username")
.maxRetries(10) // <-- 0 = unlimited
.staleTimeout(Duration.ofSeconds(90)); // <-- 90s silence
client.onChat(msg -> {
System.out.printf("%s: %s%n", msg.getUser().getNickname(), msg.getComment());
});
client.onDisconnect(() -> {
System.out.println("stream ended");
});
client.connect();local client = PirateTok.builder("username")
:max_retries(10) -- <-- 0 = unlimited
:stale_timeout(90) -- <-- tear down after 90s silence
:build()
client:on("chat", function(msg)
print(msg.user.nickname .. ": " .. msg.comment)
end)
client:on("disconnect", function()
print("stream ended")
end)
client:connect(){:ok, pid} = PirateTok.Live.connect("username",
max_retries: 10, # <-- 0 = unlimited
stale_timeout: 90_000 # <-- tear down after 90s silence
)
receive do
{:chat, msg} ->
IO.puts("#{msg.user.nickname}: #{msg.comment}")
:disconnected ->
IO.puts("stream ended")
endfinal client = TikTokLiveClient("username")
.maxRetries(10) // <-- 0 = unlimited
.staleTimeout(Duration(seconds: 90)); // <-- 90s silence
client.on<ChatEvent>((msg) {
print("${msg.user.nickname}: ${msg.comment}");
});
client.on<DisconnectEvent>((_) {
print("stream ended");
});
await client.connect();Scrapes TikTok’s SIGI HTML for profile data: HD avatars (100/720/1080px), follower counts, verified status, bio links, live room ID. Results are cached with configurable TTL. Private and banned profiles are negative-cached — no redundant HTTP requests.
Thread-safe. Zero new dependencies. Manages its own ttwid internally.
let cache = ProfileCache::new(
Duration::from_secs(300), // TTL per entry
None, None, None, // proxy, cookies, user-agent
);
let profile = cache.fetch("tiktok").await?;
println!("{} — {} followers", profile.nickname, profile.follower_count);
println!("avatar HD: {}", profile.avatar_large);
println!("live room: {:?}", profile.room_id);
// second call hits cache — no HTTP
let cached = cache.fetch("tiktok").await?;cache = ProfileCache(ttl=300) # TTL per entry
profile = await cache.fetch("tiktok")
print(f"{profile.nickname} — {profile.follower_count} followers")
print(f"avatar HD: {profile.avatar_large}")
print(f"live room: {profile.room_id}")
# second call hits cache — no HTTP
cached = await cache.fetch("tiktok")const cache = new ProfileCache({
ttlMs: 300_000, // TTL per entry
});
const profile = await cache.fetch("tiktok");
console.log(`${profile.nickname} — ${profile.followerCount} followers`);
console.log(`avatar HD: ${profile.avatarLarge}`);
console.log(`live room: ${profile.roomId}`);
// second call hits cache — no HTTP
const cached = await cache.fetch("tiktok");cache := helpers.NewProfileCache(
5*time.Minute, // TTL per entry
"", "", "", // proxy, cookies, user-agent
)
profile, err := cache.Fetch("tiktok")
fmt.Printf("%s — %d followers\n", profile.Nickname, profile.FollowerCount)
fmt.Printf("avatar HD: %s\n", profile.AvatarLarge)
fmt.Printf("live room: %s\n", profile.RoomID)
// second call hits cache — no HTTP
cached, _ := cache.Fetch("tiktok")var cache = new ProfileCache(
ttl: TimeSpan.FromMinutes(5) // TTL per entry
);
var profile = await cache.Fetch("tiktok");
Console.WriteLine($"{profile.Nickname} — {profile.FollowerCount} followers");
Console.WriteLine($"avatar HD: {profile.AvatarLarge}");
Console.WriteLine($"live room: {profile.RoomId}");
// second call hits cache — no HTTP
var cached = await cache.Fetch("tiktok");var cache = new ProfileCache(
Duration.ofMinutes(5) // TTL per entry
);
var profile = cache.fetch("tiktok");
System.out.printf("%s — %d followers%n", profile.nickname, profile.followerCount);
System.out.printf("avatar HD: %s%n", profile.avatarLarge);
System.out.printf("live room: %s%n", profile.roomId);
// second call hits cache — no HTTP
var cached = cache.fetch("tiktok");local cache = ProfileCache.new({
ttl = 300, -- TTL per entry
})
local profile = cache:fetch("tiktok")
print(profile.nickname .. " — " .. profile.follower_count .. " followers")
print("avatar HD: " .. profile.avatar_large)
print("live room: " .. tostring(profile.room_id))
-- second call hits cache — no HTTP
local cached = cache:fetch("tiktok"){:ok, cache} = PirateTok.Helpers.ProfileCache.start_link(
ttl: 300_000 # TTL per entry
)
{:ok, profile} = PirateTok.Helpers.ProfileCache.fetch(cache, "tiktok")
IO.puts("#{profile.nickname} — #{profile.follower_count} followers")
IO.puts("avatar HD: #{profile.avatar_large}")
IO.puts("live room: #{profile.room_id}")
# second call hits cache — no HTTP
{:ok, cached} = PirateTok.Helpers.ProfileCache.fetch(cache, "tiktok")final cache = ProfileCache(
ttl: Duration(minutes: 5), // TTL per entry
);
final profile = await cache.fetch("tiktok");
print("${profile.nickname} — ${profile.followerCount} followers");
print("avatar HD: ${profile.avatarLarge}");
print("live room: ${profile.roomId}");
// second call hits cache — no HTTP
final cached = await cache.fetch("tiktok");One builder call routes all traffic through your proxy: ttwid authentication, room resolution, WSS connection, profile scrapes. HTTP, HTTPS, and SOCKS5 are all supported.
Run through Tor, through a datacenter proxy, or through your own tunnel. The library doesn’t care — it just needs a URL.
let mut stream = TikTokLive::builder("username")
.proxy("socks5://127.0.0.1:9050") // <-- routes ALL traffic
.connect()
.await?;
while let Some(event) = stream.next_event().await {
match event {
TikTokLiveEvent::Chat(msg) => {
let nick = msg.user.as_ref().map_or("?", |u| u.nickname.as_str());
println!("{nick}: {}", msg.comment);
}
TikTokLiveEvent::Disconnected => break,
_ => {}
}
}client = TikTokLiveClient("username")
client.proxy = "socks5://127.0.0.1:9050" # <-- routes ALL traffic
@client.on("chat")
async def on_chat(event):
print(f"{event.user.nickname}: {event.comment}")
@client.on("disconnect")
async def on_disconnect(event):
print("stream ended")
await client.connect()const client = new TikTokLiveClient("username")
.proxy("socks5://127.0.0.1:9050") // <-- routes ALL traffic
client.on("chat", (msg) => {
console.log(`${msg.user.nickname}: ${msg.comment}`)
})
client.on("disconnect", () => {
console.log("stream ended")
})
await client.connect()client := golive.NewClient("username").
Proxy("socks5://127.0.0.1:9050") // <-- routes ALL traffic
client.OnChat(func(msg *golive.ChatMessage) {
fmt.Printf("%s: %s\n", msg.User.Nickname, msg.Comment)
})
client.OnDisconnect(func() {
fmt.Println("stream ended")
})
err := client.Connect()var client = new TikTokLiveClient("username")
.Proxy("socks5://127.0.0.1:9050"); // <-- routes ALL traffic
client.OnChat += (sender, msg) => {
Console.WriteLine($"{msg.User.Nickname}: {msg.Comment}");
};
client.OnDisconnect += (sender, e) => {
Console.WriteLine("stream ended");
};
await client.Connect();var client = new PirateTokClient("username")
.proxy("socks5://127.0.0.1:9050"); // <-- routes ALL traffic
client.onChat(msg -> {
System.out.printf("%s: %s%n", msg.getUser().getNickname(), msg.getComment());
});
client.onDisconnect(() -> {
System.out.println("stream ended");
});
client.connect();local client = PirateTok.builder("username")
:proxy("socks5://127.0.0.1:9050") -- <-- routes ALL traffic
:build()
client:on("chat", function(msg)
print(msg.user.nickname .. ": " .. msg.comment)
end)
client:on("disconnect", function()
print("stream ended")
end)
client:connect(){:ok, pid} = PirateTok.Live.connect("username",
proxy: "socks5://127.0.0.1:9050" # <-- routes ALL traffic
)
receive do
{:chat, msg} ->
IO.puts("#{msg.user.nickname}: #{msg.comment}")
:disconnected ->
IO.puts("stream ended")
endfinal client = TikTokLiveClient("username")
.proxy("socks5://127.0.0.1:9050"); // <-- routes ALL traffic
client.on<ChatEvent>((msg) {
print("${msg.user.nickname}: ${msg.comment}");
});
client.on<DisconnectEvent>((_) {
print("stream ended");
});
await client.connect();TikTok’s total_like_count arrives from different server shards with inconsistent values — the counter jumps backwards non-deterministically. Confirmed: 4 backwards jumps in 5 minutes on a single stream.
LikeAccumulator monotonizes the total (never decreases) and accumulates from the reliable per-event count delta. You get a went_backwards diagnostic flag when TikTok sends stale data.
let mut acc = LikeAccumulator::new();
let mut stream = TikTokLive::builder("username")
.connect().await?;
while let Some(event) = stream.next_event().await {
match event {
TikTokLiveEvent::Like(like) => {
let stats = acc.process(&like); // <-- monotonized
println!("total: {} accumulated: {} backwards: {}",
stats.total_like_count,
stats.accumulated_count,
stats.went_backwards);
}
TikTokLiveEvent::Disconnected => break,
_ => {}
}
}acc = LikeAccumulator()
client = TikTokLiveClient("username")
@client.on("like")
async def on_like(event):
stats = acc.process(event) # <-- monotonized
print(f"total: {stats.total_like_count} "
f"accumulated: {stats.accumulated_count} "
f"backwards: {stats.went_backwards}")
@client.on("disconnect")
async def on_disconnect(event):
print("stream ended")
await client.connect()const acc = new LikeAccumulator();
const client = new TikTokLiveClient("username")
client.on("like", (data) => {
const stats = acc.process(data) // <-- monotonized
console.log(`total: ${stats.totalLikeCount} `
+ `accumulated: ${stats.accumulatedCount} `
+ `backwards: ${stats.wentBackwards}`)
})
client.on("disconnect", () => {
console.log("stream ended")
})
await client.connect()acc := helpers.NewLikeAccumulator()
client := golive.NewClient("username")
client.OnLike(func(like *golive.LikeMessage) {
stats := acc.Process(like) // <-- monotonized
fmt.Printf("total: %d accumulated: %d backwards: %v\n",
stats.TotalLikeCount,
stats.AccumulatedCount,
stats.WentBackwards)
})
client.OnDisconnect(func() {
fmt.Println("stream ended")
})
err := client.Connect()var acc = new LikeAccumulator();
var client = new TikTokLiveClient("username");
client.OnLike += (sender, like) => {
var stats = acc.Process(like); // <-- monotonized
Console.WriteLine($"total: {stats.TotalLikeCount} "
+ $"accumulated: {stats.AccumulatedCount} "
+ $"backwards: {stats.WentBackwards}");
};
client.OnDisconnect += (sender, e) => {
Console.WriteLine("stream ended");
};
await client.Connect();var acc = new LikeAccumulator();
var client = new PirateTokClient("username");
client.onLike(like -> {
var stats = acc.process(like); // <-- monotonized
System.out.printf("total: %d accumulated: %d backwards: %b%n",
stats.totalLikeCount,
stats.accumulatedCount,
stats.wentBackwards);
});
client.onDisconnect(() -> {
System.out.println("stream ended");
});
client.connect();local acc = LikeAccumulator.new()
local client = PirateTok.builder("username"):build()
client:on("like", function(like)
local stats = acc:process(like) -- <-- monotonized
print("total: " .. stats.total_like_count
.. " accumulated: " .. stats.accumulated_count
.. " backwards: " .. tostring(stats.went_backwards))
end)
client:on("disconnect", function()
print("stream ended")
end)
client:connect()acc = PirateTok.Helpers.LikeAccumulator.new()
{:ok, pid} = PirateTok.Live.connect("username")
receive do
{:like, like} ->
{stats, acc} = PirateTok.Helpers.LikeAccumulator.process(acc, like)
IO.puts("total: #{stats.total_like_count} "
<> "accumulated: #{stats.accumulated_count} "
<> "backwards: #{stats.went_backwards}")
:disconnected ->
IO.puts("stream ended")
endfinal acc = LikeAccumulator();
final client = TikTokLiveClient("username");
client.on<LikeEvent>((like) {
final stats = acc.process(like); // <-- monotonized
print("total: ${stats.totalLikeCount} "
"accumulated: ${stats.accumulatedCount} "
"backwards: ${stats.wentBackwards}");
});
client.on<DisconnectEvent>((_) {
print("stream ended");
});
await client.connect();Binary WSS captures frozen as .bin files with a JSON manifest generated by the Rust golden standard and verified with protoc --decode_raw. Every library replays the same frames and asserts the same output — deterministic, offline, no live connection needed.
Tests skip gracefully if testdata is not cloned. Set PIRATETOK_TESTDATA to use a custom location.
# clone the shared binary WSS captures
git clone https://github.com/PirateTok/live-testdata testdata
# run replay tests — deterministic, offline
cargo test
# or point to a custom location
PIRATETOK_TESTDATA=/path/to/captures cargo test
# tests skip gracefully if testdata/ is missing
# all assertions match the golden JSON manifest# clone the shared binary WSS captures
git clone https://github.com/PirateTok/live-testdata testdata
# run replay tests — deterministic, offline
pytest tests/test_replay.py -v
# or point to a custom location
PIRATETOK_TESTDATA=/path/to/captures pytest
# tests skip gracefully if testdata/ is missing
# all assertions match the golden JSON manifest// clone the shared binary WSS captures
git clone https://github.com/PirateTok/live-testdata testdata
// run replay tests — deterministic, offline
npm test
// or point to a custom location
PIRATETOK_TESTDATA=/path/to/captures npm test
// tests skip gracefully if testdata/ is missing
// all assertions match the golden JSON manifest# clone the shared binary WSS captures
git clone https://github.com/PirateTok/live-testdata testdata
# run replay tests — deterministic, offline
go test ./...
# or point to a custom location
PIRATETOK_TESTDATA=/path/to/captures go test ./...
# tests skip gracefully if testdata/ is missing
# all assertions match the golden JSON manifest// clone the shared binary WSS captures
git clone https://github.com/PirateTok/live-testdata testdata
// run replay tests — deterministic, offline
dotnet test
// or point to a custom location
PIRATETOK_TESTDATA=/path/to/captures dotnet test
// tests skip gracefully if testdata/ is missing
// all assertions match the golden JSON manifest// clone the shared binary WSS captures
git clone https://github.com/PirateTok/live-testdata testdata
// run replay tests — deterministic, offline
mvn test
// or point to a custom location
PIRATETOK_TESTDATA=/path/to/captures mvn test
// tests skip gracefully if testdata/ is missing
// all assertions match the golden JSON manifest-- clone the shared binary WSS captures
git clone https://github.com/PirateTok/live-testdata testdata
-- run replay tests — deterministic, offline
lua tests/test_replay.lua
-- or point to a custom location
PIRATETOK_TESTDATA=/path/to/captures lua tests/test_replay.lua
-- tests skip gracefully if testdata/ is missing
-- all assertions match the golden JSON manifest# clone the shared binary WSS captures
git clone https://github.com/PirateTok/live-testdata testdata
# run replay tests — deterministic, offline
mix test
# or point to a custom location
PIRATETOK_TESTDATA=/path/to/captures mix test
# tests skip gracefully if testdata/ is missing
# all assertions match the golden JSON manifest// clone the shared binary WSS captures
git clone https://github.com/PirateTok/live-testdata testdata
// run replay tests — deterministic, offline
dart test
// or point to a custom location
PIRATETOK_TESTDATA=/path/to/captures dart test
// tests skip gracefully if testdata/ is missing
// all assertions match the golden JSON manifest18+ rooms stream events over WSS without any special authentication. The event pipeline works identically to any other room — chat, gifts, likes, all of it.
Session cookies are only needed if you want fetch_room_info() metadata (title, viewer count, FLV URLs). Pass them from browser DevTools via .cookies().
// 18+ events stream without cookies — just connect
let mut stream = TikTokLive::builder("username")
.connect().await?;
// cookies only needed for room metadata on 18+ rooms
let info = fetch_room_info("ROOM_ID", FetchParams {
cookies: Some("sessionid=abc; sid_tt=abc"),
..Default::default()
}).await?;
println!("title: {}", info.title);
println!("viewers: {}", info.user_count);
println!("flv: {}", info.stream_url);# 18+ events stream without cookies — just connect
client = TikTokLiveClient("username")
# cookies only needed for room metadata on 18+ rooms
info = await fetch_room_info(
room_id,
cookies="sessionid=abc; sid_tt=abc"
)
print(f"title: {info.title}")
print(f"viewers: {info.user_count}")
print(f"flv: {info.stream_url}")// 18+ events stream without cookies — just connect
const client = new TikTokLiveClient("username")
// cookies only needed for room metadata on 18+ rooms
const info = await fetchRoomInfo(
roomId,
10_000,
"sessionid=abc; sid_tt=abc"
)
console.log(`title: ${info.title}`)
console.log(`viewers: ${info.userCount}`)
console.log(`flv: ${info.streamUrl}`)// 18+ events stream without cookies — just connect
client := golive.NewClient("username")
// cookies only needed for room metadata on 18+ rooms
info, err := golive.FetchRoomInfo(
roomID,
10*time.Second,
"sessionid=abc; sid_tt=abc",
)
fmt.Printf("title: %s\n", info.Title)
fmt.Printf("viewers: %d\n", info.UserCount)
fmt.Printf("flv: %s\n", info.StreamURL)// 18+ events stream without cookies — just connect
var client = new TikTokLiveClient("username");
// cookies only needed for room metadata on 18+ rooms
var info = await TikTokApi.FetchRoomInfo(
roomId,
cookies: "sessionid=abc; sid_tt=abc"
);
Console.WriteLine($"title: {info.Title}");
Console.WriteLine($"viewers: {info.UserCount}");
Console.WriteLine($"flv: {info.StreamUrl}");// 18+ events stream without cookies — just connect
var client = new PirateTokClient("username");
// cookies only needed for room metadata on 18+ rooms
var info = PirateTokApi.fetchRoomInfo(
roomId,
10_000,
"sessionid=abc; sid_tt=abc"
);
System.out.printf("title: %s%n", info.title);
System.out.printf("viewers: %d%n", info.userCount);
System.out.printf("flv: %s%n", info.streamUrl);-- 18+ events stream without cookies — just connect
local client = PirateTok.builder("username"):build()
-- cookies only needed for room metadata on 18+ rooms
local info = PirateTok.fetch_room_info(
room_id,
{ cookies = "sessionid=abc; sid_tt=abc" }
)
print("title: " .. info.title)
print("viewers: " .. info.user_count)
print("flv: " .. info.stream_url)# 18+ events stream without cookies — just connect
{:ok, pid} = PirateTok.Live.connect("username")
# cookies only needed for room metadata on 18+ rooms
{:ok, info} = PirateTok.Http.Api.fetch_room_info(
room_id,
cookies: "sessionid=abc; sid_tt=abc"
)
IO.puts("title: #{info.title}")
IO.puts("viewers: #{info.user_count}")
IO.puts("flv: #{info.stream_url}")// 18+ events stream without cookies — just connect
final client = TikTokLiveClient("username");
// cookies only needed for room metadata on 18+ rooms
final info = await fetchRoomInfo(
roomId,
cookies: "sessionid=abc; sid_tt=abc",
);
print("title: ${info.title}");
print("viewers: ${info.userCount}");
print("flv: ${info.streamUrl}");Enriched user data on every event — badges, gifter level, moderator status, fan club, follow info
| Event | Ours | Go | C# | JS | Python | Java |
|---|---|---|---|---|---|---|
| ChatMessage | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| GiftMessage | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| LikeMessage | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| MemberMessage | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| SocialMessage | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| RoomUserSeqMessage | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| ControlMessage | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| LiveIntroMessage | ✓ | × | ✓ | ✓ | ✓ | × |
| RoomMessage | ✓ | × | ✓ | ✓ | ✓ | × |
| CaptionMessage | ✓ | × | × | × | ✓ | × |
| GoalUpdateMessage | ✓ | × | × | × | ✓ | × |
| ImDeleteMessage | ✓ | × | ✓ | ✓ | ✓ | × |
| RankUpdateMessage | ✓ | × | ✓ | × | ✓ | × |
| PollMessage | ✓ | × | ✓ | × | ✓ | × |
| EnvelopeMessage | ✓ | × | × | × | ✓ | × |
| RoomPinMessage | ✓ | × | × | × | ✓ | × |
| LinkMicBattle | ✓ | × | ✓ | ✓ | ✓ | ✓ |
| LinkMicArmies | ✓ | × | ✓ | ✓ | ✓ | ✓ |
| EmoteChatMessage | ✓ | × | ✓ | ✓ | ✓ | ✓ |
| QuestionNewMessage | ✓ | ✓ | × | ✓ | ✓ | ✓ |
| SubNotifyMessage | ✓ | × | ✓ | × | ✓ | ✓ |
| BarrageMessage | ✓ | × | ✓ | ✓ | ✓ | ✓ |
| HourlyRankMessage | ✓ | × | ✓ | ✓ | ✓ | × |
| MsgDetectMessage | ✓ | × | ✓ | ✓ | ✓ | × |
| LinkMicFanTicketMethod | ✓ | × | × | ✓ | ✓ | × |
| RoomVerifyMessage | ✓ | × | ✓ | ✓ | ✓ | × |
| OecLiveShoppingMessage | ✓ | × | ✓ | ✓ | ✓ | × |
| GiftBroadcastMessage | ✓ | × | ✓ | ✓ | ✓ | × |
| RankTextMessage | ✓ | × | ✓ | ✓ | ✓ | × |
| UnauthorizedMemberMessage | ✓ | × | × | × | ✓ | × |
| GiftPanelUpdateMessage | ✓ | × | × | × | × | × |
| InRoomBannerMessage | ✓ | × | × | × | × | × |
| GuideMessage | ✓ | × | × | × | × | × |
| GiftDynamicRestrictionMessage | ✓ | × | × | × | × | × |
| ViewerPicksUpdateMessage | ✓ | × | × | × | × | × |
| + 25 Tier B events | ✓ | × | partial | partial | stub | × |
| Unknown passthrough | ✓ | × | × | × | × | × |
| Signing server required | No | Yes | Yes | Yes | Yes | Yes |
“Ours” = all 12 PirateTok libraries — other columns show the single existing library per language
The TikTok Live open-source ecosystem was built around a single chokepoint: a proprietary signing server — every library in every language depended on this one piece of infrastructure to function
I run an NGO that fights child sexual abuse online — proactive monitoring of TikTok Live streams where predators target minors, gathering evidence and intervening before abuse happens rather than after
This kind of work requires volume — far more than any community tier could offer — I couldn't afford to depend on infrastructure I didn't control for work this critical
So I built my own signing server — and while optimizing it I discovered something: the signing server is unnecessary — the entire authentication ceremony — x_bogus, msToken, session tokens — none of it is validated at the WebSocket level
I rewrote everything from scratch — then I did it again in 11 more languages
No one gets to sit between you and a WebSocket connection
and decide whether your use case is worthy