Pirate Tok
Downloads

TikTok Live
unchained

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

Browse libraries →
12
languages
60
events decoded
0
signing servers
0BSD
license

The fleet

12 native implementations — same protocol — same events — same freedom

Others

Every other TikTok Live library depends on a proprietary signing server — a single point of failure no one controls

Known issues

× Sign server 500s — entire ecosystem goes dark with no ETA
× API key required — 1,000 req/day free tier, pay for more
× Region blocks — official advice is “use a VPN”
× Gifts/combos randomly break between versions
× Dependency hell — pyee version conflicts, breaking upgrades
× Best event coverage across all of them: 29/60
× 3,400–15,700 LOC with heavy transitive deps

PirateTok

✓ Direct WebSocket — no signing server, no intermediary
✓ No API keys, no rate limits, no fees, no accounts
✓ CDN selection — EU, US, or Global endpoints built in
✓ 60 events decoded across all 12 libraries
✓ Unknown event passthrough — future events work day one
✓ 500–3,600 LOC, minimal deps — 3 libraries have zero
✓ 0BSD — full source, zero restrictions, no copyleft

Features

Every library ships the same feature set — same protocol, same helpers, same control

▶ 🗜
Compression Toggle
Trade bandwidth for CPU — gzip on by default, raw protobuf when you need throughput
.compress(false)

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.

Rust
Python
JS
Go
C#
Java
Lua
Elixir
Dart
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 disconnect
const 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 disconnect
client := 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 disconnect
var 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 disconnect
var 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 disconnect
local 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")
end
final 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 disconnect
▶ 🔄
Auto-Reconnect
Stale detection, exponential backoff, fresh auth on every retry — zero user code
.stale_timeout() .max_retries()

If 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.

Rust
Python
JS
Go
C#
Java
Lua
Elixir
Dart
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")
end
final 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();
▶ 👤
Profile Cache
TTL-cached SIGI scrape — HD avatars, follower stats, live status, negative caching
ProfileCache

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.

Rust
Python
JS
Go
C#
Java
Lua
Elixir
Dart
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");
▶ 🛡
Proxy Support
HTTP, HTTPS, SOCKS5 — one call routes all traffic through your proxy or Tor
.proxy("socks5://...")

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.

Rust
Python
JS
Go
C#
Java
Lua
Elixir
Dart
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")
end
final 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();
▶ ❤
Like Accumulator
Monotonizes TikTok’s backwards-jumping like counter from sharded servers
LikeAccumulator

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.

Rust
Python
JS
Go
C#
Java
Lua
Elixir
Dart
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")
end
final 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();
▶ 🧪
Replay Testing
Frozen binary WSS captures replayed across all 12 libraries — same input, same output
cargo test / pytest / npm test

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.

Rust
Python
JS
Go
C#
Java
Lua
Elixir
Dart
# 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 manifest
▶ 🔓
Age-Restricted Rooms
18+ event streams work out of the box — cookies only needed for room metadata
.cookies("sessionid=...")

18+ 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().

Rust
Python
JS
Go
C#
Java
Lua
Elixir
Dart
// 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}");

Decode Matrix

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✓×partialpartialstub×
Unknown passthrough✓×××××
Signing server requiredNoYesYesYesYesYes

“Ours” = all 12 PirateTok libraries — other columns show the single existing library per language

The Saga

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

Come aboard →
Treasure