A Node.js library to receive live stream events such as comments and gifts in realtime from TikTok LIVE by connecting to TikTok's internal Webcast push service. This package includes a wrapper that connects to the Webcast service using just the username (@uniqueId). This allows you to connect to your own live chat as well as the live chat of other streamers. No credentials are required. Besides Chat Comments, other events such as Members Joining, Gifts, Subscriptions, Viewers, Follows, Shares, Questions, Likes and Battles can be tracked.
[!NOTE] This is not a production-ready API. It is a reverse engineering project. Use the WebSocket API for production.
[!TIP] An example project is available at https://tiktok-chat-reader.zerody.one/ - View Source
npm i tiktok-live-connector
// Username of someone who is currently live
import { TikTokLiveConnection, WebcastEvent } from 'tiktok-live-connector';
const tiktokUsername = 'officialgeilegisela';
// Create a new wrapper object and pass the username
const connection = new TikTokLiveConnection(tiktokUsername);
// Connect to the chat (await can be used as well)
connection.connect().then(state => {
console.info(`Connected to roomId ${state.roomId}`);
}).catch(err => {
console.error('Failed to connect', err);
});
// Define the events that you want to handle
// In this case we listen to chat messages (comments)
connection.on(WebcastEvent.CHAT, data => {
console.log(`${data.user.uniqueId} (userId:${data.user.uniqueId}) writes: ${data.comment}`);
});
// And here we receive gifts sent to the streamer
connection.on(WebcastEvent.GIFT, data => {
console.log(`${data.user.uniqueId} (userId:${data.user.userId}) sends ${data.giftId}`);
});
// ...and more events described in the documentation below
To create a new TikTokLiveConnection object the following parameters can be specified.
TikTokLiveConnection(uniqueId, [options])
| Param Name | Required | Description |
|---|---|---|
uniqueId |
Yes | The unique username of the broadcaster. You can find this name in the URL. Example: https://www.tiktok.com/@officialgeilegisela/live becomes officialgeilegisela. The leading @ and the full URL form are also accepted. |
options |
No | Optional connection properties. Defaults are applied when a value is not specified.signApiKey (default: undefined)Euler Stream API key. When provided, it is written to the global SignConfig.apiKey before the underlying Euler client is created. Ignored when eulerApiInstance is passed.eulerApiInstance (default: undefined)Pre-built EulerStreamApiClient to use for all sign-server traffic. Takes precedence over signApiKey.session (default: undefined)Authenticated session bundle. Pass session.cookie to seed the cookie jar with sessionid and tt-target-idc, and/or session.oAuthToken to send an OAuth token to the sign server. Required when authenticateWs or useMobile is true. See Authenticated Connection.authenticateWs (default: false)Forward the session cookies or OAuth token to the sign server so the WebSocket is authenticated. Disabled by default since signing is done by a third-party service; enabling it sends your session credentials to that service. useMobile (default: false)Use the mobile WebSocket flow. Implies authenticateWs: true and requires session.cookie.processInitialData (default: true)Decode and emit the message batch returned in the initial sign response (recent chat history etc.). fetchRoomInfoOnConnect (default: true)Fetch room info during connect. If the streamer is not currently live the connect rejects with UserOfflineError. The fetched info is stored on connection.roomInfo and is also accessible via fetchRoomInfo().enableExtendedGiftInfo (default: false)Fetch the room gift list during connect so WebcastGiftMessage events carry an extendedGiftInfo field with name, cost, and image data.clientPresets (default: randomized)Pre-built { device, screen, location } presets. Defaults to a freshly randomized set from getRandomPresets().webClientOptions (default: {})Extra options forwarded to the underlying got HTTP client (proxy agent, timeout, etc.). Headers and search params from this object are merged with the defaults; transport-only fields are passed through. See Proxied Connection.wsClientOptions (default: {})Extra options forwarded to the underlying ws WebSocket client. See Proxied Connection.webConfigOverrides (default: {})Partial overrides for the resolved WebcastWebConfigDefaults used by the HTTP client. Use this to customize DEFAULT_HTTP_CLIENT_PARAMS, DEFAULT_HTTP_CLIENT_HEADERS, etc.wsConfigOverrides (default: {})Partial overrides for the resolved WebcastWebSocketConfigDefaults used by the WebSocket client. Use this to customize DEFAULT_WS_CLIENT_PARAMS or DEFAULT_WS_CLIENT_HEADERS. |
const tikTokLiveConnection = new TikTokLiveConnection(tiktokUsername, {
signApiKey: 'your-api-key',
processInitialData: false,
enableExtendedGiftInfo: true,
webConfigOverrides: {
DEFAULT_HTTP_CLIENT_PARAMS: {
app_language: 'en-US',
device_platform: 'web_pc'
},
DEFAULT_HTTP_CLIENT_HEADERS: {
'X-Custom-Header': 'value'
}
},
wsConfigOverrides: {
DEFAULT_WS_CLIENT_PARAMS: {
app_language: 'en-US'
},
DEFAULT_WS_CLIENT_HEADERS: {
'X-Custom-Header': 'value'
}
},
webClientOptions: {
timeout: { request: 10000 }
},
wsClientOptions: {
handshakeTimeout: 10000
}
});
| Method Name | Description |
|---|---|
connect([roomId]) |
Connects to the live stream chat. Returns a Promise<TikTokLiveConnectionState> that resolves when the WebSocket is open and the room has been entered. Pass an explicit roomId to skip room-id resolution. |
disconnect() |
Closes the WebSocket and waits for the close event before resolving. Safe to call even when not connected. |
sendMessage(content, [roomId]) |
Sends a chat message to the connected room (or to roomId if provided). Requires signApiKey and an authenticated session. See Send Messages. |
fetchRoomId([uniqueId]) |
Resolves the room id for the configured username (or for uniqueId if provided). Tries HTML scrape, then the TikTok API, then Euler Stream as a fallback. |
fetchIsLive([uniqueId]) |
Resolves whether the user is currently live. Same fallback chain as fetchRoomId. |
waitUntilLive([seconds], [abortSignal]) |
Polls fetchIsLive every seconds seconds (minimum 30) until the streamer goes live. Reject via abortSignal to cancel. |
fetchRoomInfo([roomId]) |
Fetches room info from TikTok's webcast API. Caches the result on connection.roomInfo. Callable without an active connection. See Retrieve Room Info. |
fetchAvailableGifts() |
Fetches the room gift list. Caches on connection.availableGifts. Callable without an active connection. See Retrieve Available Gifts. |
| Property Name | Description |
|---|---|
webClient: WebcastHttpClient |
The HTTP client used to talk to TikTok's web and webcast APIs. Holds the cookie jar, the merged client params, and the merged client headers. |
apiClient: EulerStreamApiClient |
The Euler Stream API client used for sign-server traffic and other premium endpoints. Equivalent to webClient.apiClient. |
wsClient: WebcastWebSocketClient | null |
The active WebSocket client. null until the connection is established and after disconnect. |
options: TikTokLiveConnectionMutableOptions |
The subset of options retained on the instance (processInitialData, fetchRoomInfoOnConnect, enableExtendedGiftInfo, authenticateWs, useMobile). |
roomInfo: RoomInfo | null |
The room info object cached from the most recent fetchRoomInfo() call (or from connect, when fetchRoomInfoOnConnect is true). |
availableGifts: RoomGiftInfo | null |
The cached gift list. Populated when enableExtendedGiftInfo is true or after manually calling fetchAvailableGifts(). |
isConnecting: boolean |
True while connect() is in flight. |
isConnected: boolean |
True after a successful connect, until the WebSocket closes. |
clientParams: Record<string, string> |
Live URI parameters sent with every webClient request. Mutating this changes outgoing query strings. |
roomId: string |
The currently bound room id. Empty string until connect resolves or fetchRoomId() is called. |
state: TikTokLiveConnectionState |
Snapshot containing isConnected, isConnecting, roomId, roomInfo, and availableGifts. |
WebSocket URL signing is delegated to the Euler Stream sign server via the @eulerstream/euler-api-sdk package. Configuration is held on the mutable SignConfig singleton.
For most users, passing signApiKey to the TikTokLiveConnection constructor is enough. It writes the key to SignConfig.apiKey before the underlying Euler client is built.
For advanced cases (custom sign-server URL, extra headers, JWT auth), mutate SignConfig directly before constructing your first connection:
import { SignConfig } from 'tiktok-live-connector';
SignConfig.apiKey = 'your api key'; // An API key created at https://www.eulerstream.com
SignConfig.basePath = 'https://your-custom-sign-server.com';
SignConfig.baseOptions = SignConfig.baseOptions || {};
SignConfig.baseOptions.headers = {
...(SignConfig.baseOptions.headers || {}),
'X-Custom-Header': 'Custom-Header-Value'
};
[!IMPORTANT]
SignConfigis read once when the underlying Euler client is first instantiated and the result is cached. Mutate it before constructing the firstTikTokLiveConnection, or pass an expliciteulerApiInstanceto bypass the cache.
All HTTP route handlers live in the global RouteConfig registry and are also exported individually. Each route is a function that accepts a single options object and returns a Promise resolving to the route's response. The currently registered routes are:
fetchRoomInfo (TikTok webcast API)fetchRoomGifts (TikTok webcast API)fetchRoomInfoFromApiLive (TikTok web API)fetchRoomInfoFromHtml (HTML scrape with SIGI_STATE extraction)fetchRoomGiftsFromProvider (Euler Stream)fetchRoomGiftGalleryFromProvider (Euler Stream)fetchRoomIdFromProvider (Euler Stream)fetchRoomInfoFromProvider (Euler Stream)fetchSignedWebSocketFromProvider (Euler Stream)fetchWebcastSignatureFromProvider (Euler Stream)sendRoomChatFromProvider (Euler Stream)fetchRoomIdComposite (HTML, then API, then Euler fallback)fetchIsLiveComposite (HTML, then API, then Euler fallback)Routes need a webClient (and an apiClient for Euler routes), both of which are exposed on a TikTokLiveConnection instance:
import { TikTokLiveConnection, RouteConfig } from 'tiktok-live-connector';
const connection = new TikTokLiveConnection('officialgeilegisela');
const sigiState = await RouteConfig.fetchRoomInfoFromHtml({
webClient: connection.webClient,
uniqueId: 'officialgeilegisela'
});
console.log('SIGI_STATE liveRoom:', sigiState.liveRoom);
You can replace any handler in RouteConfig to inject custom behavior (caching, alternate sign provider, proxying through your own backend, etc.):
import { RouteConfig } from 'tiktok-live-connector';
const original = RouteConfig.fetchSignedWebSocketFromProvider;
RouteConfig.fetchSignedWebSocketFromProvider = async (args) => {
console.log('signing for room', args.roomId);
return original(args);
};
The Euler Stream API client is exposed as connection.apiClient. It wraps every endpoint published by the @eulerstream/euler-api-sdk package, including premium and analytics endpoints not bundled into RouteConfig.
import { TikTokLiveConnection } from 'tiktok-live-connector';
const connection = new TikTokLiveConnection('officialgeilegisela', {
signApiKey: 'your-api-key'
});
const response = await connection.apiClient.webcast.getRateLimits();
console.log('Rate Limits:', response.data);
When running the connector in a client-facing environment (bundled desktop app, browser worker, etc.) you should not ship your raw API key. Mint a short-lived JWT on your server and hand that to the client instead.
import { SignConfig, createEulerClient } from 'tiktok-live-connector';
SignConfig.apiKey = 'your_server_side_api_key';
const apiClient = createEulerClient();
const res = await apiClient.authentication.createJWT(
122, // Your account ID
{
limits: { minute: 5, hour: 5, day: 5 },
expireAfter: 60 * 60 * 2 // 2 hours, the maximum accepted value
}
);
console.log('Generated JWT:', res.data.token);
import { SignConfig, TikTokLiveConnection } from 'tiktok-live-connector';
SignConfig.baseOptions = SignConfig.baseOptions || {};
SignConfig.baseOptions.headers = {
...(SignConfig.baseOptions.headers || {}),
'x-jwt-key': 'generated-jwt-key'
};
const connection = new TikTokLiveConnection('tv_asahi_news');
A TikTokLiveConnection object has the following events which can be handled with an event listener.
The simplest event handler is a connect event:
const connection = new TikTokLiveConnection('officialgeilegisela');
connection.on(ControlEvent.CONNECTED, () => console.log("Connected!"));
ControlEvent.CONNECTED or "connected"ControlEvent.DISCONNECTED or "disconnected"ControlEvent.RAW_DATA or "rawData"ControlEvent.DECODED_DATA or "decodedData"ControlEvent.WEBSOCKET_CONNECTED or "websocketConnected"ControlEvent.ERROR or "error"ControlEvent.WEBSOCKET_DATA or "websocketData"WebcastEvent.CHAT or "chat"WebcastEvent.GIFT or "gift"WebcastEvent.MEMBER or "member"WebcastEvent.LIKE or "like"WebcastEvent.SOCIAL or "social"WebcastEvent.ENVELOPE or "envelope"WebcastEvent.QUESTION_NEW or "questionNew"WebcastEvent.LINK_MIC_BATTLE or "linkMicBattle"WebcastEvent.LINK_MIC_ARMIES or "linkMicArmies"WebcastEvent.LIVE_INTRO or "liveIntro"WebcastEvent.SUPER_FAN or "superFan"WebcastEvent.SUPER_FAN_JOIN or "superFanJoin"WebcastEvent.FOLLOW or "follow"WebcastEvent.SHARE or "share"WebcastEvent.STREAM_END or "streamEnd"WebcastEvent.ROOM_USER or "roomUser"WebcastEvent.EMOTE or "emote"WebcastEvent.GOAL_UPDATE or "goalUpdate"WebcastEvent.ROOM_MESSAGE or "roomMessage"WebcastEvent.CAPTION_MESSAGE or "captionMessage"WebcastEvent.IM_DELETE or "imDelete"WebcastEvent.IN_ROOM_BANNER or "inRoomBanner"WebcastEvent.RANK_UPDATE or "rankUpdate"WebcastEvent.POLL_MESSAGE or "pollMessage"WebcastEvent.RANK_TEXT or "rankText"WebcastEvent.LINK_MIC_BATTLE_PUNISH_FINISH or "linkMicBattlePunishFinish"WebcastEvent.LINK_MIC_BATTLE_TASK or "linkMicBattleTask"WebcastEvent.LINK_MIC_FAN_TICKET_METHOD or "linkMicFanTicketMethod"WebcastEvent.LINK_MIC_METHOD or "linkMicMethod"WebcastEvent.UNAUTHORIZED_MEMBER or "unauthorizedMember"WebcastEvent.OEC_LIVE_SHOPPING or "oecLiveShopping"WebcastEvent.MSG_DETECT or "msgDetect"WebcastEvent.LINK_MESSAGE or "linkMessage"WebcastEvent.ROOM_VERIFY or "roomVerify"WebcastEvent.LINK_LAYER or "linkLayer"WebcastEvent.ROOM_PIN or "roomPin"WebcastEvent.SUPER_FAN_BOX or "superFanBox"WebcastEvent.ACCESS_CONTROL or "accessControl"WebcastEvent.ACCESS_RECALL or "accessRecall"WebcastEvent.BOOST_CARD or "boostCard"WebcastEvent.BOTTOM_MESSAGE or "bottomMessage"WebcastEvent.CAPSULE or "capsule"WebcastEvent.GAME_RANK_NOTIFY or "gameRankNotify"WebcastEvent.GIFT_BROADCAST or "giftBroadcast"WebcastEvent.GIFT_DYNAMIC_RESTRICTION or "giftDynamicRestriction"WebcastEvent.GIFT_PANEL_UPDATE or "giftPanelUpdate"WebcastEvent.GIFT_PROMPT or "giftPrompt"WebcastEvent.GUIDE or "guide"WebcastEvent.HOURLY_RANK or "hourlyRank"WebcastEvent.LINK_MIC_LAYOUT_STATE or "linkMicLayoutState"WebcastEvent.LINK_STATE or "linkState"WebcastEvent.LIVE_GAME_INTRO or "liveGameIntro"WebcastEvent.MARQUEE_ANNOUNCEMENT or "marqueeAnnouncement"WebcastEvent.NOTICE or "notice"WebcastEvent.PARTNERSHIP_DROPS_UPDATE or "partnershipDropsUpdate"WebcastEvent.PARTNERSHIP_GAME_OFFLINE or "partnershipGameOffline"WebcastEvent.PARTNERSHIP_PUNISH or "partnershipPunish"WebcastEvent.PERCEPTION or "perception"WebcastEvent.ROOM_NOTIFY or "roomNotify"WebcastEvent.SPEAKER or "speaker"WebcastEvent.SUB_NOTIFY or "subNotify"WebcastEvent.SUB_PIN_EVENT or "subPinEvent"WebcastEvent.TOAST or "toast"WebcastEvent.VIEWER_PICKS_UPDATE or "viewerPicksUpdate"connectedTriggered when the connection is successfully established.
connection.on(
ControlEvent.CONNECTED,
(state: TikTokLiveConnectionState) => {
console.log(`Hurray! Connected to roomId ${state.roomId}`);
console.log(`Room info loaded: ${Boolean(state.roomInfo)}`);
}
);
disconnectedTriggered when the connection gets disconnected. In that case you can call connect() again to have a reconnect logic. Note that you should wait a little bit before attempting a reconnect to to avoid being rate-limited.
connection.on(ControlEvent.DISCONNECTED, ({ code, reason }) => {
console.log(`Disconnected :(${code})`);
if (reason) {
console.log(`Reason: ${reason}`);
}
});
rawDataTriggered every time a protobuf encoded webcast message arrives. You can deserialize the binary object depending on the use case with protobufjs.
connection.on(ControlEvent.RAW_DATA, (messageTypeName, binary) => {
console.log(messageTypeName, binary);
});
decodedDataTriggered every time a decoded message arrives. This is the same as the rawData event but the binary object is already decoded. You can use this event to handle the protobuf messages without the need of deserializing them yourself.
connection.on(ControlEvent.DECODED_DATA, (event: string, decodedData: any, binary: Uint8Array) => {
console.log(event, decodedData);
});
websocketConnectedWill be triggered as soon as a WebSocket connection is established. The WebSocket client object is passed.
connection.on(ControlEvent.WEBSOCKET_CONNECTED, (client: TikTokWsClient) => {
console.log('WebSocket open:', client.open);
});
websocketDataTriggered every time a WebSocket message arrives. This is the same as the rawData event but the binary object is
connection.on(ControlEvent.WEBSOCKET_DATA, (data: Uint8Array) => {
console.log(`WebSocket bytes: ${data.length}`);
});
errorGeneral error event. You should handle this.
connection.on(ControlEvent.ERROR, ({ info, exception }) => {
console.error('Error!', info, exception);
});
memberTriggered every time a new viewer joins the live stream.
connection.on(WebcastEvent.MEMBER, (data: WebcastMemberMessage) => {
const uniqueId = data.user?.uniqueId;
const nickname = data.user?.nickname;
if (uniqueId) {
console.log(`User uniqueId: ${uniqueId}`);
}
if (nickname) {
console.log(`User nickname: ${nickname}`);
}
if (uniqueId || nickname) {
console.log('User joined the stream!');
}
console.log(`Viewers: ${data.memberCount}`);
});
chatTriggered every time a new chat comment arrives.
connection.on(WebcastEvent.CHAT, (data: WebcastChatMessage) => {
const uniqueId = data.user?.uniqueId;
const nickname = data.user?.nickname;
if (uniqueId) {
console.log(`User uniqueId: ${uniqueId}`);
}
if (nickname) {
console.log(`User nickname: ${nickname}`);
}
if (data.comment) {
console.log(`Comment: ${data.comment}`);
}
});
giftTriggered every time a gift arrives. You will receive additional information via the extendedGiftInfo attribute when you enable the enableExtendedGiftInfo option.
NOTE: Users have the capability to send gifts in a streak. This increases the
repeatCountvalue until the user terminates the streak. During this time new gift events are triggered again and again with an increasedrepeatCountvalue. It should be noted that after the end of the streak, another gift event is triggered, which signals the end of the streak viarepeatEnd:true. This applies only to gifts withgiftType:1. This means that even if the user sends agiftType:1gift only once, you will receive the event twice. Once withrepeatEnd:falseand once withrepeatEnd:true. Therefore, the event should be handled as follows:
connection.on(WebcastEvent.GIFT, (data: WebcastGiftMessage) => {
const giftType = data.giftDetails?.giftType;
const giftName = data.giftDetails?.giftName;
const uniqueId = data.user?.uniqueId;
const nickname = data.user?.nickname;
if (giftType === 1 && !data.repeatEnd) {
// Streak in progress => show only temporary
console.log('Gift streak in progress');
} else {
// Streak ended or non-streakable gift => process the gift with final repeat_count
console.log('Gift streak ended or non-streakable gift');
}
if (uniqueId) {
console.log(`User uniqueId: ${uniqueId}`);
}
if (nickname) {
console.log(`User nickname: ${nickname}`);
}
console.log(`GiftId: ${data.giftId}, repeatCount: ${data.repeatCount}`);
if (giftName) {
console.log(`Gift name: ${giftName}`);
}
});
roomUserTriggered every time a statistic message arrives. This message currently contains the viewer count and a top gifter list.
connection.on(WebcastEvent.ROOM_USER, data => {
console.log(`Viewer Count: ${data.viewerCount}`);
const topGifter = data.ranksList[0];
if (topGifter?.user) {
const uniqueId = topGifter.user.uniqueId;
const nickname = topGifter.user.nickname;
if (uniqueId) {
console.log(`Top gifter uniqueId: ${uniqueId} (${topGifter.coinCount})`);
}
if (nickname) {
console.log(`Top gifter nickname: ${nickname} (${topGifter.coinCount})`);
}
}
});
likeTriggered when a viewer sends likes to the streamer. For streams with many viewers, this event is not always triggered by TikTok.
connection.on(WebcastEvent.LIKE, data => {
const uniqueId = data.user?.uniqueId;
const nickname = data.user?.nickname;
if (uniqueId) {
console.log(`User uniqueId: ${uniqueId}`);
}
if (nickname) {
console.log(`User nickname: ${nickname}`);
}
console.log(`Likes: ${data.likeCount}, total likes: ${data.totalLikeCount}`);
});
socialTriggered every time someone shares the stream or follows the host.
connection.on(WebcastEvent.SOCIAL, data => {
if (data.action) {
console.log(`Social action: ${data.action}`);
}
const uniqueId = data.user?.uniqueId;
const nickname = data.user?.nickname;
if (uniqueId) {
console.log(`User uniqueId: ${uniqueId}`);
}
if (nickname) {
console.log(`User nickname: ${nickname}`);
}
if (data.shareType || data.shareTarget) {
console.log(`Share type: ${data.shareType}, share target: ${data.shareTarget}`);
}
});
emoteTriggered every time a subscriber sends an emote (sticker).
connection.on(WebcastEvent.EMOTE, (data: WebcastEmoteChatMessage) => {
const uniqueId = data.user?.uniqueId;
const nickname = data.user?.nickname;
if (uniqueId) {
console.log(`User uniqueId: ${uniqueId}`);
}
if (nickname) {
console.log(`User nickname: ${nickname}`);
}
const emoteId = data.emoteList[0]?.emoteId;
if (emoteId) {
console.log(`Emote id: ${emoteId}`);
}
});
envelopeTriggered every time someone sends a treasure chest.
connection.on(WebcastEvent.ENVELOPE, data => {
const envelope = data.envelopeInfo;
if (envelope) {
if (envelope.envelopeId) {
console.log(`Envelope ${envelope.envelopeId}`);
}
if (envelope.sendUserName) {
console.log(`From: ${envelope.sendUserName}`);
}
console.log(`Diamonds: ${envelope.diamondCount}, People: ${envelope.peopleCount}`);
}
});
questionNewTriggered every time someone asks a new question via the question feature.
connection.on(WebcastEvent.QUESTION_NEW, data => {
const question = data.details?.questionText;
if (question) {
console.log(`Question: ${question}`);
}
const uniqueId = data.details?.user?.uniqueId;
const nickname = data.details?.user?.nickname;
if (uniqueId) {
console.log(`Asked by (uniqueId): ${uniqueId}`);
}
if (nickname) {
console.log(`Asked by (nickname): ${nickname}`);
}
});
linkMicBattleTriggered every time a battle starts.
connection.on(WebcastEvent.LINK_MIC_BATTLE, (data) => {
const participants = Object.values(data.anchorInfo)
.map(info => info.user?.displayId || info.user?.nickName)
.filter(Boolean);
console.log(`New Battle ${data.battleId}`);
if (participants.length) {
console.log(`Participants: ${participants.join(' VS ')}`);
}
});
linkMicArmiesTriggered every time a battle participant receives points. Contains the current status of the battle and the army that suported the group.
connection.on(WebcastEvent.LINK_MIC_ARMIES, (data) => {
console.log(`Battle ${data.battleId}: gift ${data.giftId} x${data.giftCount}`);
console.log(`Total diamonds: ${data.totalDiamondCount}`);
});
liveIntroTriggered when a live intro message appears.
connection.on(WebcastEvent.LIVE_INTRO, (msg) => {
const uniqueId = msg.host?.uniqueId;
const nickname = msg.host?.nickname;
if (uniqueId) {
console.log(`Host uniqueId: ${uniqueId}`);
}
if (nickname) {
console.log(`Host nickname: ${nickname}`);
}
if (msg.description) {
console.log(`Description: ${msg.description}`);
}
});
streamEndTriggered when the live stream gets terminated by the host. Will also trigger the disconnected event.
connection.on(WebcastEvent.STREAM_END, ({ action }: { action: ControlAction }) => {
if (action === ControlAction.CONTROL_ACTION_STREAM_ENDED) {
console.log('Stream ended by user');
}
if (action === ControlAction.CONTROL_ACTION_STREAM_SUSPENDED) {
console.log('Stream ended by platform moderator (ban)');
}
});
superFanTriggers when a user becomes a Super Fan.
connection.on(WebcastEvent.SUPER_FAN, (data) => {
if (data.content?.defaultPattern) {
console.log(data.content.defaultPattern);
}
if (data.commonBarrageContent?.defaultPattern) {
console.log(data.commonBarrageContent.defaultPattern);
}
});
superFanJoinTriggers when an existing Super Fan joins the live.
connection.on(WebcastEvent.SUPER_FAN_JOIN, (data) => {
if (data.content?.defaultPattern) {
console.log(data.content.defaultPattern);
}
if (data.commonBarrageContent?.defaultPattern) {
console.log(data.commonBarrageContent.defaultPattern);
}
});
superFanBoxTriggers when a user sends a Super Fan Box.
connection.on(WebcastEvent.SUPER_FAN_BOX, (data) => {
console.log('A Super Fan Box was sent!', data.envelopeInfo);
});
These events are based on message events.
followTriggers when a user follows the streamer. Based on social event.
connection.on(WebcastEvent.FOLLOW, (data) => {
const uniqueId = data.user?.uniqueId;
const nickname = data.user?.nickname;
if (uniqueId) {
console.log(`${uniqueId} followed!`);
}
if (nickname) {
console.log(`${nickname} followed!`);
}
});
shareTriggers when a user shares the stream. Based on social event.
connection.on(WebcastEvent.SHARE, (data) => {
const uniqueId = data.user?.uniqueId;
const nickname = data.user?.nickname;
if (uniqueId) {
console.log(`${uniqueId} shared the stream!`);
}
if (nickname) {
console.log(`${nickname} shared the stream!`);
}
})
goalUpdateTriggered when a channel goal is updated.
connection.on(WebcastEvent.GOAL_UPDATE, (data: WebcastGoalUpdateMessage) => {
const goalDescription = data.goal?.description;
const contributor = data.contributorDisplayId || data.contributorIdStr || data.contributorId;
if (goalDescription) {
console.log(`Goal update: ${goalDescription}`);
}
if (contributor) {
console.log(`Contributor: ${contributor}, count: ${data.contributeCount}, score: ${data.contributeScore}`);
}
});
roomMessageNo information available.
connection.on(WebcastEvent.ROOM_MESSAGE, (data: WebcastRoomMessage) => {
if (data.content) {
console.log(`Room message: ${data.content}`);
}
if (data.source) {
console.log(`Source: ${data.source}`);
}
console.log(`Scene: ${data.scene}`);
});
captionMessageNo information available.
connection.on(WebcastEvent.CAPTION_MESSAGE, (data: WebcastCaptionMessage) => {
if (data.content.length) {
const lines = data.content.map(c => `[${c.lang}] ${c.content}`).join(' ');
console.log(`Caption (${data.timestampMs}): ${lines}`);
}
});
imDeleteTriggered when a message is deleted in the chat.
connection.on(WebcastEvent.IM_DELETE, (data: WebcastImDeleteMessage) => {
if (data.deleteMsgIdsList.length) {
console.log(`Deleted messages: ${data.deleteMsgIdsList.join(', ')}`);
}
if (data.deleteUserIdsList.length) {
console.log(`Deleted users: ${data.deleteUserIdsList.join(', ')}`);
}
});
inRoomBannerTriggered when a banner is shown in the room.
connection.on(WebcastEvent.IN_ROOM_BANNER, (data: WebcastInRoomBannerMessage) => {
if (data.jsonData) {
console.log('Banner payload:', data.jsonData);
}
});
rankUpdateTriggered when a ranking update is received.
connection.on(WebcastEvent.RANK_UPDATE, (data: WebcastRankUpdateMessage) => {
console.log(`Rank updates: ${data.updatesList.length}`);
const firstUpdate = data.updatesList[0];
if (firstUpdate) {
console.log(`Rank type: ${firstUpdate.rankType}, owner rank: ${firstUpdate.ownerRank}`);
}
});
pollMessageTriggered when a poll-related message is sent in the room.
connection.on(WebcastEvent.POLL_MESSAGE, (data: WebcastPollMessage) => {
const title = data.pollBasicInfo?.title;
if (title) {
console.log(`Poll: ${title}`);
}
if (data.pollBasicInfo?.timeRemain) {
console.log(`Time remaining: ${data.pollBasicInfo.timeRemain}`);
}
});
rankTextTriggered when text related to rankings is displayed.
connection.on(WebcastEvent.RANK_TEXT, (data: WebcastRankTextMessage) => {
if (data.ownerIdxBeforeUpdate || data.ownerIdxAfterUpdate) {
console.log(`Rank change: ${data.ownerIdxBeforeUpdate} -> ${data.ownerIdxAfterUpdate}`);
}
if (data.selfGetBadgeMsg?.defaultPattern) {
console.log(`Self badge text: ${data.selfGetBadgeMsg.defaultPattern}`);
}
});
linkMicBattlePunishFinishTriggered when a link mic battle punishment is finished.
connection.on(WebcastEvent.LINK_MIC_BATTLE_PUNISH_FINISH, (data: WebcastLinkMicBattlePunishFinish) => {
console.log(`Battle punish finished: ${data.battleId}`);
console.log(`Reason: ${data.reason}, channel: ${data.channelId}`);
});
linkMicBattleTaskTriggered when a new task is issued during a link mic battle.
connection.on(WebcastEvent.LINK_MIC_BATTLE_TASK, (data: WebcastLinkmicBattleTaskMessage) => {
console.log(`Battle task (${data.battleId}) type: ${data.battleTaskMessageType}`);
if (data.taskUpdate) {
console.log(`Progress: ${data.taskUpdate.taskProgress}`);
}
if (data.taskSettle) {
console.log(`Result: ${data.taskSettle.taskResult}`);
}
});
linkMicFanTicketMethodTriggered when a fan ticket-related method is invoked during a link mic session.
connection.on(WebcastEvent.LINK_MIC_FAN_TICKET_METHOD, (data: WebcastLinkMicFanTicketMethod) => {
const notice = data.FanTicketRoomNotice;
if (notice) {
console.log(`Total fan tickets: ${notice.TotalLinkMicFanTicket}`);
console.log(`Users in notice: ${notice.UserFanTicketList.length}`);
}
});
linkMicMethodTriggered when a link mic method is used.
connection.on(WebcastEvent.LINK_MIC_METHOD, (data: WebcastLinkMicMethod) => {
console.log(`LinkMic messageType: ${data.messageType}`);
if (data.channelId || data.userId || data.toUserId) {
console.log(`Channel: ${data.channelId}, user: ${data.userId}, toUser: ${data.toUserId}`);
}
});
unauthorizedMemberTriggered when an unauthorized member tries to perform a restricted action.
connection.on(WebcastEvent.UNAUTHORIZED_MEMBER, (data: WebcastUnauthorizedMemberMessage) => {
console.log(`Unauthorized member action: ${data.action}`);
if (data.nickName) {
console.log(`Nickname: ${data.nickName}`);
}
if (data.enterText?.defaultPattern) {
console.log(`Enter text: ${data.enterText.defaultPattern}`);
}
});
oecLiveShoppingTriggered when a live shopping event occurs.
connection.on(WebcastEvent.OEC_LIVE_SHOPPING, (data: WebcastOecLiveShoppingMessage) => {
const shop = data.shopData;
if (shop) {
if (shop.title || shop.priceString) {
console.log(`Product: ${shop.title} (${shop.priceString})`);
}
if (shop.shopName || shop.shopUrl) {
console.log(`Shop: ${shop.shopName}, URL: ${shop.shopUrl}`);
}
}
});
msgDetectTriggered when the system detects a message for moderation or other purposes.
connection.on(WebcastEvent.MSG_DETECT, (data: WebcastMsgDetectMessage) => {
console.log(`Detect type: ${data.detectType}, triggerBy: ${data.triggerBy}`);
if (data.triggerCondition?.uplinkDetectWebSocket !== undefined) {
console.log(`WebSocket detect: ${data.triggerCondition.uplinkDetectWebSocket}`);
}
if (data.timeInfo?.apiRecvTimeMs) {
console.log(`API recv time: ${data.timeInfo.apiRecvTimeMs}`);
}
});
linkMessageTriggered during a link mic session for various communication purposes.
connection.on(WebcastEvent.LINK_MESSAGE, (data: WebcastLinkMessage) => {
console.log(`Link message type: ${data.MessageType}, scene: ${data.Scene}`);
if (data.InviteContent) {
console.log(`Invite from ${data.InviteContent.fromUserId}`);
if (data.InviteContent.actionId) {
console.log(`Action: ${data.InviteContent.actionId}`);
}
}
if (data.ReplyContent) {
console.log(`Reply status: ${data.ReplyContent.replyStatus}`);
if (data.ReplyContent.fromUserId || data.ReplyContent.toUserId) {
console.log(`From ${data.ReplyContent.fromUserId} to ${data.ReplyContent.toUserId}`);
}
}
});
roomVerifyTriggered when the system performs room verification.
connection.on(WebcastEvent.ROOM_VERIFY, (data: RoomVerifyMessage) => {
console.log(`Room verify: ${data.noticeType}`);
console.log(`Action: ${data.action}, closeRoom: ${data.closeRoom}`);
if (data.content) {
console.log(`Content: ${data.content}`);
}
});
linkLayerTriggered when a new link mic layer is added or updated.
connection.on(WebcastEvent.LINK_LAYER, (data: WebcastLinkLayerMessage) => {
console.log(`LinkLayer type: ${data.messageType}, channel: ${data.channelId}, scene: ${data.scene}`);
if (data.inviteContent) {
if (data.inviteContent.inviteeLinkMicId) {
console.log(`Invite linkMicId: ${data.inviteContent.inviteeLinkMicId}`);
}
}
if (data.createChannelContent) {
if (data.createChannelContent.ownerLinkMicId) {
console.log(`Owner linkMicId: ${data.createChannelContent.ownerLinkMicId}`);
}
}
});
roomPinTriggered when a message is pinned in the chat room.
connection.on(WebcastEvent.ROOM_PIN, (data: WebcastRoomPinMessage) => {
const operatorUniqueId = data.operator?.uniqueId;
const operatorNickname = data.operator?.nickname;
if (operatorUniqueId) {
console.log(`Pinned by (uniqueId): ${operatorUniqueId}`);
}
if (operatorNickname) {
console.log(`Pinned by (nickname): ${operatorNickname}`);
}
if (data.method) {
console.log(`Pin method: ${data.method}`);
}
const chatText = data.chatMessage?.comment;
const giftName = data.giftMessage?.giftDetails?.giftName;
const socialAction = data.socialMessage?.action;
const memberUniqueId = data.memberMessage?.user?.uniqueId;
const memberNickname = data.memberMessage?.user?.nickname;
const likeCount = data.likeMessage?.likeCount;
if (chatText) {
console.log(`Pinned chat: ${chatText}`);
}
if (giftName) {
console.log(`Pinned gift: ${giftName}`);
}
if (socialAction) {
console.log(`Pinned social action: ${socialAction}`);
}
if (memberUniqueId) {
console.log(`Pinned member (uniqueId): ${memberUniqueId}`);
}
if (memberNickname) {
console.log(`Pinned member (nickname): ${memberNickname}`);
}
if (typeof likeCount === 'number') {
console.log(`Pinned like count: ${likeCount}`);
}
});
const connection = new TikTokLiveConnection('@username');
connection.fetchRoomInfo().then((roomInfo: RoomInfoResponse) => {
console.log(roomInfo);
console.log(`Stream started timestamp: ${roomInfo.create_time}, Streamer bio: ${roomInfo.owner.bio_description}`);
console.log(`HLS URL: ${roomInfo.stream_url.hls_pull_url}`); // Can be played or recorded with e.g. VLC
}).catch(err => {
console.error(err);
});
const connection = new TikTokLiveConnection('@username');
connection.fetchAvailableGifts().then((giftList: RoomGiftInfo) => {
console.log(giftList);
giftList.forEach(gift => {
console.log(`id: ${gift.id}, name: ${gift.name}, cost: ${gift.diamond_count}`)
});
}).catch(err => {
console.error(err);
})
The HTTP layer uses got and the WebSocket layer uses ws. Both accept a Node http.Agent, which makes hpagent the recommended proxy integration. hpagent is already a direct dependency of this package, so no extra install is required.
webClientOptionsgot accepts an agent map keyed by protocol (http, https, http2). Provide proxy agents for each protocol the connector talks to:
import { TikTokLiveConnection } from 'tiktok-live-connector';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
const proxy = 'http://user:pwd@host:8080';
const connection = new TikTokLiveConnection('@username', {
webClientOptions: {
agent: {
http: new HttpProxyAgent({ proxy, keepAlive: true }),
https: new HttpsProxyAgent({ proxy, keepAlive: true })
}
}
});
This routes all TikTok web/webcast HTTP traffic (room id, room info, gift list, HTML scrape, sign requests) through the proxy.
wsClientOptionsws accepts a single agent (a Node http.Agent). Since the WebSocket upgrade goes over wss://, use the HTTPS variant:
import { TikTokLiveConnection } from 'tiktok-live-connector';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
const proxy = 'http://user:pwd@host:8080';
const connection = new TikTokLiveConnection('@username', {
webClientOptions: {
agent: {
http: new HttpProxyAgent({ proxy, keepAlive: true }),
https: new HttpsProxyAgent({ proxy, keepAlive: true })
}
},
wsClientOptions: {
agent: new HttpsProxyAgent({ proxy, keepAlive: true })
}
});
// Connect as usual
await connection.connect();
To connect as a specific TikTok account, log into TikTok in your browser and copy two cookies: sessionid and tt-target-idc. Both are required. sessionid is your account's session token (keep it secret), and tt-target-idc is the datacenter region your account belongs to (for example useast1a for North America).
Pass them via the session.cookie bundle. To force the WebSocket itself to authenticate (so the server treats you as that account in chat), also set authenticateWs: true.
import { TikTokLiveConnection } from 'tiktok-live-connector';
const connection = new TikTokLiveConnection(
'tv_asahi_news',
{
signApiKey: 'your-api-key',
authenticateWs: true,
session: {
cookie: {
type: 'cookie',
value: {
sessionId: '<account_session_id>',
ttTargetIdc: '<account_target_idc>'
}
}
}
}
);
await connection.connect();
console.log('Connected to TikTok LIVE chat!');
You can also set the session after construction by calling setSessionBundle on the cookie jar:
await connection.webClient.cookieJar.setSessionBundle({
type: 'cookie',
value: {
sessionId: '<account_session_id>',
ttTargetIdc: '<account_target_idc>'
}
});
sendMessage posts a chat message into the currently connected room. It requires a valid Euler Stream API key (see Signing Configuration) and an authenticated session bundle (cookies or OAuth token).
import { TikTokLiveConnection } from 'tiktok-live-connector';
const connection = new TikTokLiveConnection(
'tv_asahi_news',
{
signApiKey: 'your-api-key',
authenticateWs: true,
session: {
cookie: {
type: 'cookie',
value: {
sessionId: '<account_session_id>',
ttTargetIdc: '<account_target_idc>'
}
}
}
}
);
await connection.connect();
await connection.sendMessage('Hello world!');
console.log('Message sent!');
This project is licensed under the MIT License. See the LICENSE file for details.