@toktikhq/sdk-js 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1754 @@
1
+ /** Honest position of a response inside the product's published Coverage Set. */
2
+ declare const COVERAGE_STATUSES: readonly ["observed", "partial", "not-covered", "not-offered", "stale"];
3
+ type CoverageStatus = (typeof COVERAGE_STATUSES)[number];
4
+ /** How current the persisted observation is; refreshes are always asynchronous. */
5
+ declare const FRESHNESS_STATUSES: readonly ["near_realtime", "historical", "stale"];
6
+ type FreshnessStatus = (typeof FRESHNESS_STATUSES)[number];
7
+ interface Provenance {
8
+ /** RFC 3339 instant of the newest observation represented by this response. */
9
+ observedAt: string;
10
+ freshness: FreshnessStatus;
11
+ /** Collector or persisted dataset that produced the observation. */
12
+ source: string;
13
+ coverageStatus: CoverageStatus;
14
+ /** Version of the calculation or projection used to produce the data. */
15
+ methodologyVersion: string;
16
+ }
17
+ /** Every customer-facing data resource is wrapped so provenance cannot be omitted. */
18
+ interface ProvenanceEnvelope<TData> {
19
+ data: TData;
20
+ provenance: Provenance;
21
+ }
22
+
23
+ interface LiveSessionGiftRollup {
24
+ giftId: string;
25
+ giftName: string | null;
26
+ giftType: number | null;
27
+ giftCount: number;
28
+ diamondTotal: number;
29
+ senderCount: number;
30
+ }
31
+ interface LiveSessionGifterRollup {
32
+ gifterUserId: string;
33
+ gifterUniqueId: string | null;
34
+ gifterNickname: string | null;
35
+ giftCount: number;
36
+ diamondTotal: number;
37
+ }
38
+ interface LiveSessionSummary {
39
+ id: string;
40
+ roomKey: string;
41
+ roomId: string;
42
+ creatorUniqueId: string | null;
43
+ /** Observed avatar for `creatorUniqueId`, joined best-effort from `creator_profiles` (null = never profiled). */
44
+ creatorAvatarUrl: string | null;
45
+ startedAt: string;
46
+ endedAt: string | null;
47
+ durationSeconds: number;
48
+ peakViewerCount: number;
49
+ eventCount: number;
50
+ chatCount: number;
51
+ giftCount: number;
52
+ totalGiftDiamonds: number;
53
+ }
54
+ interface LiveSessionRecord extends LiveSessionSummary {
55
+ giftRollups: LiveSessionGiftRollup[];
56
+ gifterRollups: LiveSessionGifterRollup[];
57
+ }
58
+ interface LiveCreatorPerformanceRecord {
59
+ sessionId: string;
60
+ sessionStartedAt: string;
61
+ creatorUniqueId: string;
62
+ roomKey: string;
63
+ durationSeconds: number;
64
+ peakViewerCount: number;
65
+ eventCount: number;
66
+ chatCount: number;
67
+ giftCount: number;
68
+ totalGiftDiamonds: number;
69
+ distinctGifterCount: number;
70
+ }
71
+ interface LiveSessionListResponse {
72
+ items: LiveSessionSummary[];
73
+ nextCursor: string | null;
74
+ /**
75
+ * P1.7 — the caller's plan-tier history read window in days (`null` = unlimited). Sessions that
76
+ * started before `now - historyWindowDays` are withheld; a higher tier reads further back.
77
+ */
78
+ historyWindowDays: number | null;
79
+ /** True when sessions older than the window exist and were withheld by it (not by paging). */
80
+ historyTruncated: boolean;
81
+ }
82
+ interface LiveCreatorPerformanceListResponse {
83
+ items: LiveCreatorPerformanceRecord[];
84
+ nextCursor: string | null;
85
+ /** P1.7 — plan-tier history read window in days (`null` = unlimited). See {@link LiveSessionListResponse}. */
86
+ historyWindowDays: number | null;
87
+ /** True when performance rows older than the window exist and were withheld by it. */
88
+ historyTruncated: boolean;
89
+ }
90
+
91
+ /** Rank movement of a creator versus the immediately-prior published period. */
92
+ declare const RANKING_MOVEMENTS: readonly ["up", "down", "same", "new"];
93
+ type RankingMovement = (typeof RANKING_MOVEMENTS)[number];
94
+ interface RankingEntry {
95
+ rank: number;
96
+ creatorUid: string;
97
+ uniqueId: string | null;
98
+ nickname: string | null;
99
+ /**
100
+ * Creator avatar URL, extracted from the captured board row (no new TikTok call). `null` only when
101
+ * the board row carried none. Shown even on a `masked` row (owner decision 2026-08-01: the photo is
102
+ * public TikTok data; masking withholds the handle/name, not the avatar). `secUid` is never included
103
+ * in a ranking entry, masked or not.
104
+ */
105
+ avatarUrl: string | null;
106
+ /**
107
+ * Whether the creator is live *right now* in a room the platform is currently observing (joined
108
+ * from `room_catalog` runtime state — best-effort over the tracked set, never a new upstream call).
109
+ */
110
+ isLive: boolean;
111
+ /** The observed live room id when `isLive`, else `null`. */
112
+ roomId: string | null;
113
+ /** Board score (TikTok board points/diamonds). */
114
+ score: number;
115
+ leagueClass: string | null;
116
+ /**
117
+ * Where `leagueClass` came from. `"observed"` = the row's own board carries the class (a league
118
+ * board). `"last_phase"` = the class was carried over from the most recent league phase this
119
+ * creator was observed in, because the board being read (Daily, Popular LIVE, Gaming) does not
120
+ * carry one and TikTok publishes no league board between competition phases. `null` when
121
+ * `leagueClass` is null. A carried-over class is real observed data, never inferred — but it
122
+ * describes a past phase, so `leagueClassAsOf` dates it.
123
+ */
124
+ leagueClassSource: "observed" | "last_phase" | null;
125
+ /** When `leagueClassSource` is `"last_phase"`, when that class was observed; else `null`. */
126
+ leagueClassAsOf: string | null;
127
+ movement: RankingMovement;
128
+ /** Creator's rank in the prior published period, or null when new/unknown. */
129
+ previousRank: number | null;
130
+ /**
131
+ * R5.1 paywall — when `true`, `uniqueId`/`nickname` are masked (e.g. `•••••ry`); `rank`/`score`/
132
+ * `movement`/`isLive`/`avatarUrl` still show (teaser — avatar shown even when masked, owner decision
133
+ * 2026-08-01). Enforced at the API serializer by the viewer's plan tier, not just hidden in the UI.
134
+ * `unmaskTier` names the tier that unlocks the handle/name.
135
+ */
136
+ masked: boolean;
137
+ /**
138
+ * When `masked`, the plan tier that unlocks the real identity (for an Unlock CTA); else `null`.
139
+ * P1.10 — this is the unlock tier for the BOARD being read, not a constant: standard boards unlock
140
+ * at `pro`, league boards at `scale`, gaming boards at `agency` (`@v2/billing` `UNMASK_TIER_BY_BOARD`).
141
+ */
142
+ unmaskTier: string | null;
143
+ }
144
+ interface OfficialRankingBoard {
145
+ kind: "official";
146
+ /** TikTok board key (e.g. "daily", "hourly", "popular_live", "gaming_daily", "shopping_daily", "shopping_weekly"). */
147
+ board: string;
148
+ /** Region = ISO country code the board was captured for. */
149
+ region: string;
150
+ /** Board period key from the captured snapshot (empty when gated/absent). */
151
+ periodKey: string;
152
+ /**
153
+ * ISO instant at which the board's CURRENT period next rolls — what the UI counts down to
154
+ * ("Next update in …"). For boards whose `periodKey` is an `ends-at:<ISO>` (hourly/daily/gaming/
155
+ * popular/shopping) this is that same instant. For the league boards, whose `periodKey` is a 2-hour
156
+ * sampling bucket (an intra-phase observation, not a period with a reset), it is the board's own
157
+ * `next_period_time_stamp` captured in the snapshot evidence — so a league board can show an honest
158
+ * countdown too. `null` when no reset instant is known (a gated board, or a `previous-of:` / bare
159
+ * legacy key that states none). A past instant means the period is rolling; the client renders that
160
+ * as "updating" rather than a negative remainder. Optional so an older client/fixture that omits
161
+ * it stays valid; the live API always sets it (to an instant or `null`).
162
+ */
163
+ resetAt?: string | null;
164
+ /**
165
+ * Whether THIS board is inside the Coverage Set — i.e. its own latest snapshot is `complete`.
166
+ * When false the board is **gated**: `entries` is empty and `coverageStatus` explains whether
167
+ * collection is incomplete (`not-covered`) or TikTok does not publish it here (`not-offered`).
168
+ * The gating unit is the board, not
169
+ * the region: a region's other boards (including league classes the product never publishes,
170
+ * which TikTok withdraws entirely between competition phases) no longer withhold a board that
171
+ * is itself complete and fresh.
172
+ */
173
+ publishable: boolean;
174
+ /** Coverage of the board's region — reported as context, not the gate. */
175
+ coverage: RankingCoverage;
176
+ /**
177
+ * Present only on a per-game gaming board (`board = "gaming_game"`, R6.4): which game this board
178
+ * is, so the client need not echo its own request. `gameKey` is TikTok's stable game identity
179
+ * (e.g. `topic_game_roblox`); `gameTitle` is the display name at capture time (may be null before
180
+ * any snapshot). Absent on every non-game board.
181
+ */
182
+ game?: {
183
+ gameKey: string;
184
+ gameTitle: string | null;
185
+ };
186
+ entries: RankingEntry[];
187
+ }
188
+ interface RankingCoverage {
189
+ /** Complete boards observed for the region. */
190
+ completeBoards: number;
191
+ /** Boards expected for full coverage (22 per ADR-006). */
192
+ expectedBoards: number;
193
+ /** completeBoards / expectedBoards, clamped to [0,1]. */
194
+ ratio: number;
195
+ /** Publish threshold this board was gated against, in [0,1]. */
196
+ threshold: number;
197
+ }
198
+ /**
199
+ * One retained period of an Official board, as captured (R5.5).
200
+ *
201
+ * A period is served as it was observed: there is no live-join (a past board's `isLive` is always
202
+ * `false` and its `roomId` `null` — today's rooms say nothing about a board captured weeks ago) and
203
+ * no movement, which is a property of the *current* board versus the one before it, not of a period
204
+ * sitting in history. The only added field is `leagueClass`, carried over from the creator's last
205
+ * observed league phase and marked `last_phase` with `leagueClassAsOf` — the boards customers read
206
+ * publish no class at all, so without it the `leagueTier` filter would match nothing.
207
+ *
208
+ * Periods that were only partially captured, or that TikTok did not publish at all, are returned
209
+ * WITH their own `coverageStatus` rather than hidden: a gap in the record is part of the record.
210
+ */
211
+ interface RankingHistoryPeriod {
212
+ /** Board period key from the captured snapshot (e.g. `ends-at:2026-07-29T17:00:00.000Z`). */
213
+ periodKey: string;
214
+ /** When this period's board was captured (ISO 8601). */
215
+ observedAt: string;
216
+ /** This period's own coverage: `observed` when completely captured, else `partial`. */
217
+ coverageStatus: CoverageStatus;
218
+ /** Whether this period was completely captured (an incomplete period is shown, not published). */
219
+ publishable: boolean;
220
+ entries: RankingEntry[];
221
+ }
222
+ /**
223
+ * Retained Official-ranking history for the AUTHENTICATED lane (R5.5).
224
+ *
225
+ * The public showroom has its own history feed, but it applies the public teaser rule
226
+ * (`masked = isLive || rank > openTopN`) — which withholds exactly the rows a paying customer
227
+ * bought. This response applies the same plan-tier masking as `/v1/rankings/official`, so `/v1`
228
+ * never returns less identity than the public projection (ADR-014).
229
+ */
230
+ interface RankingHistoryResponse {
231
+ /** TikTok board key the history was read for. */
232
+ board: string;
233
+ /** Region = ISO country code. */
234
+ region: string;
235
+ /** Retained periods, newest first. */
236
+ periods: RankingHistoryPeriod[];
237
+ /** Opaque cursor for the next (older) page, or `null` when the last page was returned. */
238
+ nextCursor: string | null;
239
+ /** Whether more retained periods may exist beyond the oldest one returned. */
240
+ hasMore: boolean;
241
+ /**
242
+ * P1.7 — the caller's plan-tier history read window in days (`null` = unlimited). The response is
243
+ * capped to periods within it; a higher tier reads further back into the same retained data.
244
+ */
245
+ historyWindowDays: number | null;
246
+ /**
247
+ * True when older retained periods exist beyond `historyWindowDays` and were withheld by the
248
+ * window (not by paging). Lets the UI say "N more at a higher tier" instead of a silent cut that
249
+ * reads as "we have no more data".
250
+ */
251
+ historyTruncated: boolean;
252
+ }
253
+ interface OfficialRankingResponse {
254
+ board: OfficialRankingBoard;
255
+ }
256
+ /**
257
+ * LT1.3 — region "movers": the creators whose rank changed most between a board's current and
258
+ * previous published period, split into gainers (moved up) and losers (moved down). Derived from the
259
+ * same official board read (`movement`/`previousRank` already carried per entry), so identity is
260
+ * masked by the viewer's plan tier exactly as `OfficialRankingResponse` — never an unmask backdoor.
261
+ */
262
+ interface RankMoversBoard {
263
+ board: string;
264
+ region: string;
265
+ periodKey: string;
266
+ /** Gainers, biggest rank gain first (rank number fell the most). Empty on a gated/unpublished board. */
267
+ gainers: RankingEntry[];
268
+ /** Losers, biggest rank drop first (rank number rose the most). */
269
+ losers: RankingEntry[];
270
+ }
271
+ interface RankMoversResponse {
272
+ board: RankMoversBoard;
273
+ }
274
+ /**
275
+ * One region a customer can actually read a rank board for (R5.7).
276
+ *
277
+ * `region` is the **bucket key** every other rankings route accepts — `US+`, `MENA`, `LATAM`, … —
278
+ * not an ISO country code. TikTok's boards are keyed by bucket, and a bucket can span several
279
+ * countries (`ES+` contains Mexico; `UK+` is anglophone-Africa-heavy). Asking for `US`, `GB` or
280
+ * `DE` matches no board at all, which is exactly the bug this list exists to close: a UI that
281
+ * invents ISO codes shows a dozen regions that can only ever come back coverage-gated.
282
+ */
283
+ interface RankingRegion {
284
+ /** Bucket key, verbatim — pass this to `board`/`history` reads. Never normalised. */
285
+ region: string;
286
+ /**
287
+ * Human label. A multi-country bucket says so (`"UK+ · United Kingdom & anglophone Africa"`)
288
+ * rather than flying one country's flag over several. An unrecognised bucket labels itself.
289
+ */
290
+ label: string;
291
+ /** This region's coverage, on the same terms `/v1/rankings/official` reports it. */
292
+ coverage: RankingCoverage;
293
+ /**
294
+ * Board keys TikTok currently offers in this region and that we have captured at least once,
295
+ * sorted. A listed board can still be gated while its latest capture is incomplete.
296
+ */
297
+ boards: string[];
298
+ /**
299
+ * Board keys proven absent from TikTok's product in this region, sorted — the set a client may
300
+ * present as unavailable.
301
+ *
302
+ * Deliberately its own field rather than "whatever is missing from `boards`". Absence from
303
+ * `boards` means only "no capture yet", which is also true of a board we have simply never
304
+ * reached; a client that read absence as proof of upstream absence would tell a customer "TikTok
305
+ * does not publish this board here" on the strength of our own gap — the exact false reason R5.6
306
+ * exists to remove — and, by disabling the control, leave no way to reach the honest
307
+ * `not-covered` answer.
308
+ */
309
+ notOfferedBoards: string[];
310
+ }
311
+ /**
312
+ * The regions a customer can read, built from what has actually been captured (R5.7).
313
+ *
314
+ * Deliberately NOT the configured allowlist: a region we intend to crawl but have never captured
315
+ * would be an invitation to an empty board. Empty is a valid answer — an empty array, not an error.
316
+ */
317
+ interface RankingRegionsResponse {
318
+ regions: RankingRegion[];
319
+ }
320
+ /**
321
+ * One per-game gaming board a region offers (R6.4) — the source the games selector reads so the UI
322
+ * never hardcodes a game list (same principle as `RankingRegionsResponse`).
323
+ */
324
+ interface GamingGame {
325
+ /** TikTok's stable game identity — pass this verbatim as the `game` param on a `gaming_game` read. */
326
+ gameKey: string;
327
+ /** Display name from the latest capture (session-locale default pattern); null before any snapshot. */
328
+ gameTitle: string | null;
329
+ /**
330
+ * `offered` = advertised by TikTok's game switcher (or captured at least once); `not_offered` =
331
+ * proven pulled. A client presents `not_offered` games as unavailable rather than hiding them.
332
+ */
333
+ status: "offered" | "not_offered";
334
+ /** ISO instant this game's board next resets (from the latest snapshot's `ends-at:` period); null if unknown. */
335
+ resetAt: string | null;
336
+ }
337
+ /**
338
+ * The per-game gaming boards a region offers, built from captured snapshots + availability (R6.4).
339
+ * Empty array = the region runs the aggregate gaming board but TikTok has enabled no per-game tabs
340
+ * there — a valid answer, not an error, and the UI shows no game selector.
341
+ */
342
+ interface GamingGamesResponse {
343
+ region: string;
344
+ games: GamingGame[];
345
+ }
346
+
347
+ /**
348
+ * The discrete observation window every gifter read is scoped to. There is no "all-time" option: the
349
+ * store is presence-based and coverage-biased, so an unbounded total would read as a lifetime figure
350
+ * it is not. The window is echoed on every response so a number is never shown unlabelled.
351
+ */
352
+ declare const GIFTER_WINDOWS: readonly ["24h", "7d", "30d"];
353
+ type GifterWindow = (typeof GIFTER_WINDOWS)[number];
354
+ /**
355
+ * Stable public identity of a gifter. `gifterKey` is what every other endpoint takes:
356
+ * the TikTok `sec_uid` when the upstream event carried one, else a `uid:<userId>`
357
+ * fallback. `identity` says which, so a caller never mistakes a fallback for a sec_uid.
358
+ */
359
+ declare const GIFTER_IDENTITY_KINDS: readonly ["sec_uid", "user_id_fallback"];
360
+ type GifterIdentityKind = (typeof GIFTER_IDENTITY_KINDS)[number];
361
+ /**
362
+ * LT3.1 — a gifter archetype, derived (first-match) from the OBSERVED figures below. Like every gifter
363
+ * number it is a proxy over the rooms we watched, never a claim about the gifter's true TikTok-wide
364
+ * behaviour (LT3.2 "true LTV" is a separate, gated question). Rules, in precedence order:
365
+ * - `whale` — high absolute observed diamonds (`observedDiamonds >= WHALE_DIAMONDS`).
366
+ * - `one_time` — seen giving in a single session (`sessionCount <= 1`).
367
+ * - `rising` — recently arrived (`firstSeenAt` within `RISING_TENURE_DAYS`) and already multi-session.
368
+ * - `dormant` — established but gone quiet (`now - lastSeenAt > DORMANT_DAYS`).
369
+ * - `regular` — the rest: sustained, active, sub-whale.
370
+ */
371
+ declare const GIFTER_SEGMENTS: readonly ["whale", "one_time", "rising", "dormant", "regular"];
372
+ type GifterSegment = (typeof GIFTER_SEGMENTS)[number];
373
+ interface GifterSummary {
374
+ gifterKey: string;
375
+ identity: GifterIdentityKind;
376
+ secUid: string | null;
377
+ uniqueId: string | null;
378
+ nickname: string | null;
379
+ /**
380
+ * PD4 (#4) — resolved gifter avatar, best-effort via the signed profile endpoint and cached
381
+ * (`gifter_avatars`), bounded to the gifters on the page. Null when the gifter is anonymous (no
382
+ * resolvable id), the resolve failed, or the resolver is disabled — the surface then falls back to
383
+ * a gradient. Never an invented image.
384
+ */
385
+ avatarUrl: string | null;
386
+ /** Diamonds observed from this gifter in the requested window (union-max per session, summed). */
387
+ observedDiamonds: number;
388
+ /**
389
+ * PD5 (#10) — `observedDiamonds` estimated in USD at the configured `DIAMOND_USD_RATE` (creator-
390
+ * received value). An "est." over an observed, windowed figure — never a settled payout. Null when
391
+ * the amount is not estimable.
392
+ */
393
+ estimatedValueUsd: number | null;
394
+ /** Distinct rooms/sessions this gifter was observed giving in, in the window. */
395
+ sessionCount: number;
396
+ /** Distinct creators this gifter was observed funding in the window. */
397
+ creatorCount: number;
398
+ /** R6 — the ranked board's ISO country this gifter was observed in (the whale board's Region
399
+ * column). MAX across their observations, so a whale seen in several regions gets one label. Null
400
+ * until region coverage (0065) reaches them. */
401
+ region: string | null;
402
+ /** R6 — distinct regions observed (0066): 0 none, 1 single (`region` names it), >1 multi-region
403
+ * (the board shows "Multi (n)" rather than one arbitrary country). */
404
+ regionCount: number;
405
+ /** LT3.1 — the gifter's archetype, derived from the observed figures (see {@link GifterSegment}). */
406
+ segment: GifterSegment;
407
+ firstSeenAt: string;
408
+ lastSeenAt: string;
409
+ /**
410
+ * P-gifter-mask — identity paywall, mirroring the rankings board rule (`rankUnmaskPolicy`, `standard`
411
+ * board: unmask at Pro, teaser top 10). When `true`, `nickname`/`uniqueId`/`secUid`/`gifterKey` are
412
+ * withheld (`nickname`/`uniqueId` → `•••••XX`, `secUid` → `null`, `gifterKey` → a non-resolvable
413
+ * masked token); `avatarUrl`/`observedDiamonds`/`segment`/counts still show (owner decision: the
414
+ * avatar shows even on a masked row, exactly like a rankings entry). The top-N teaser rows are `false`.
415
+ */
416
+ masked: boolean;
417
+ /** When `masked`, the plan tier that unlocks the real identity (for an Unlock CTA); else `null`. */
418
+ unmaskTier: string | null;
419
+ }
420
+ /** One creator a gifter funds, with what they were observed giving to that creator in the window. */
421
+ interface FundedCreator {
422
+ creatorUniqueId: string;
423
+ /** Observed display name, joined best-effort from `creator_profiles` (null = never profiled). */
424
+ creatorDisplayName: string | null;
425
+ /** Observed avatar for `creatorUniqueId`, joined best-effort from `creator_profiles` (null = never profiled). */
426
+ creatorAvatarUrl: string | null;
427
+ observedDiamonds: number;
428
+ /** PD5 (#10) — `observedDiamonds` as an "est." USD value at `DIAMOND_USD_RATE`. Null when not estimable. */
429
+ estimatedValueUsd: number | null;
430
+ sessionCount: number;
431
+ firstSeenAt: string;
432
+ lastSeenAt: string;
433
+ }
434
+ /** One gifter funding a creator, with what they were observed giving to that creator in the window. */
435
+ interface CreatorGifter {
436
+ gifterKey: string;
437
+ identity: GifterIdentityKind;
438
+ secUid: string | null;
439
+ uniqueId: string | null;
440
+ nickname: string | null;
441
+ /** PD4 (#4) — resolved gifter avatar (see {@link GifterSummary.avatarUrl}); null → gradient fallback. */
442
+ avatarUrl: string | null;
443
+ observedDiamonds: number;
444
+ /** PD5 (#10) — `observedDiamonds` as an "est." USD value at `DIAMOND_USD_RATE`. Null when not estimable. */
445
+ estimatedValueUsd: number | null;
446
+ sessionCount: number;
447
+ firstSeenAt: string;
448
+ lastSeenAt: string;
449
+ /** P-gifter-mask — identity paywall, same rule as {@link GifterSummary.masked} (top-N teaser per
450
+ * creator unmasked, the rest withheld below the unlock tier). */
451
+ masked: boolean;
452
+ /** When `masked`, the plan tier that unlocks the real identity (for an Unlock CTA); else `null`. */
453
+ unmaskTier: string | null;
454
+ }
455
+ interface GifterListResponse {
456
+ gifters: GifterSummary[];
457
+ /** Echo of the applied window + creator filter, so a cached page is self-describing. */
458
+ filter: {
459
+ window: GifterWindow;
460
+ creatorUniqueId: string | null;
461
+ };
462
+ }
463
+ interface GifterProfileResponse {
464
+ gifter: GifterSummary;
465
+ fundedCreators: FundedCreator[];
466
+ /** The window the profile's figures are scoped to. */
467
+ filter: {
468
+ window: GifterWindow;
469
+ };
470
+ }
471
+ /** Aggregate over ALL of a creator's observed gifters in the window (not just the returned page) —
472
+ * the "total" strip on the Whale intel modal. */
473
+ interface CreatorGifterTotals {
474
+ observedDiamonds: number;
475
+ gifterCount: number;
476
+ /** `observedDiamonds` × `DIAMOND_USD_RATE`; null when not estimable. */
477
+ estimatedValueUsd: number | null;
478
+ }
479
+ interface CreatorGiftersResponse {
480
+ creatorUniqueId: string;
481
+ gifters: CreatorGifter[];
482
+ totals: CreatorGifterTotals;
483
+ filter: {
484
+ window: GifterWindow;
485
+ };
486
+ }
487
+
488
+ /** One retained observation of a creator's metrics over time. */
489
+ interface CreatorProfileHistoryPoint {
490
+ /** RFC 3339 capture instant. */
491
+ capturedAt: string;
492
+ followerCount: number | null;
493
+ followingCount: number | null;
494
+ awemeCount: number | null;
495
+ totalFavorited: number | null;
496
+ favoritingCount: number | null;
497
+ region: string | null;
498
+ displayName: string | null;
499
+ verified: boolean | null;
500
+ }
501
+ interface CreatorProfileData {
502
+ /** Stable numeric TikTok user id. */
503
+ tiktokUserId: string;
504
+ /** Current handle (may be recycled across accounts over time). */
505
+ uniqueId: string;
506
+ secUid: string | null;
507
+ displayName: string | null;
508
+ region: string | null;
509
+ language: string | null;
510
+ signature: string | null;
511
+ avatarUrl: string | null;
512
+ followerCount: number | null;
513
+ followingCount: number | null;
514
+ awemeCount: number | null;
515
+ totalFavorited: number | null;
516
+ favoritingCount: number | null;
517
+ verified: boolean | null;
518
+ customVerify: string | null;
519
+ enterpriseVerifyReason: string | null;
520
+ instagramId: string | null;
521
+ youtubeChannelId: string | null;
522
+ twitterId: string | null;
523
+ /** RFC 3339 first observation / newest refresh instants. */
524
+ firstSeenAt: string;
525
+ lastRefreshedAt: string;
526
+ /** Newest-first metric history (bounded). */
527
+ history: CreatorProfileHistoryPoint[];
528
+ }
529
+ type CreatorProfileResponse = ProvenanceEnvelope<{
530
+ creator: CreatorProfileData;
531
+ }>;
532
+ /** A lightweight creator row for the search/discovery grid (no history). */
533
+ interface CreatorSearchResult {
534
+ tiktokUserId: string;
535
+ uniqueId: string;
536
+ displayName: string | null;
537
+ region: string | null;
538
+ avatarUrl: string | null;
539
+ followerCount: number | null;
540
+ verified: boolean | null;
541
+ /** LT2.1 — whether this creator is live right now in a room the platform is observing. */
542
+ isLive: boolean;
543
+ /**
544
+ * LT2.1 recruiting-finder paywall — `true` when the caller's plan tier hides this creator's
545
+ * identity (`uniqueId`/`displayName` are `•••••…`); `region`/`followerCount`/`isLive`/`avatarUrl`
546
+ * still show. Only the filter-driven finder masks (a `q` handle-search never does — you already
547
+ * know who you searched for). `unmaskTier` names the tier that reveals the identity.
548
+ */
549
+ masked: boolean;
550
+ /** When `masked`, the plan tier that unlocks the real identity (for an Unlock CTA); else `null`. */
551
+ unmaskTier: string | null;
552
+ }
553
+ /** Search/browse over the already-observed creator set (never a live TikTok search). An empty
554
+ * query browses the most-followed observed creators. `nextCursor` pages the recruiting finder
555
+ * (region/minFollowers/liveNow/sort filters); `null` on the last page (and on a plain `q` search). */
556
+ type CreatorSearchResponse = ProvenanceEnvelope<{
557
+ query: string;
558
+ results: CreatorSearchResult[];
559
+ nextCursor: string | null;
560
+ }>;
561
+ interface CreatorProfileChange {
562
+ /** Tracked field name, e.g. `uniqueId`, `displayName`, `avatarUrl`. */
563
+ field: string;
564
+ oldValue: string | null;
565
+ newValue: string | null;
566
+ /** RFC 3339 instant the change was first observed (not when it happened on TikTok). */
567
+ observedAt: string;
568
+ /** Collector that observed it, e.g. `profile-resolve`. */
569
+ source: string;
570
+ }
571
+ interface CreatorChangesData {
572
+ tiktokUserId: string;
573
+ uniqueId: string;
574
+ /** Newest-first. Empty when nothing has changed since we started observing. */
575
+ changes: CreatorProfileChange[];
576
+ }
577
+ type CreatorChangesResponse = ProvenanceEnvelope<CreatorChangesData>;
578
+ type FollowListDirection = "following" | "follower";
579
+ /** One account in a follow list, with the profile fields TikTok embeds on every entry. */
580
+ interface FollowListEntry {
581
+ tiktokUserId: string;
582
+ uniqueId: string;
583
+ secUid: string | null;
584
+ displayName: string | null;
585
+ avatarUrl: string | null;
586
+ region: string | null;
587
+ signature: string | null;
588
+ followerCount: number | null;
589
+ followingCount: number | null;
590
+ awemeCount: number | null;
591
+ verified: boolean | null;
592
+ /** RFC 3339 instants for the edge itself, not the account. */
593
+ firstSeenAt: string;
594
+ lastRefreshedAt: string;
595
+ }
596
+ interface FollowListData {
597
+ /** The account whose list this is. */
598
+ tiktokUserId: string;
599
+ uniqueId: string;
600
+ direction: FollowListDirection;
601
+ entries: FollowListEntry[];
602
+ /** Opaque; pass back as `?cursor=`. `null` when there is nothing further to serve. */
603
+ nextCursor: string | null;
604
+ hasMore: boolean;
605
+ /** TikTok's own follower/following count — for `follower` this dwarfs `storedCount` by design. */
606
+ totalReported: number | null;
607
+ /** How many entries we actually hold for this list. */
608
+ storedCount: number;
609
+ /** True when TikTok reports the account hides this list; nothing can be collected. */
610
+ isPrivate: boolean;
611
+ }
612
+ type FollowListResponse = ProvenanceEnvelope<FollowListData>;
613
+
614
+ interface CreatorAnalysisSubject {
615
+ tiktokUserId: string;
616
+ uniqueId: string;
617
+ displayName: string | null;
618
+ /** Observed avatar from `creator_profiles` — the same photo the profile surface shows. */
619
+ avatarUrl: string | null;
620
+ region: string | null;
621
+ verified: boolean | null;
622
+ followerCount: number | null;
623
+ followingCount: number | null;
624
+ /** Videos TikTok says the account has, which is normally far more than we analysed. */
625
+ awemeCount: number | null;
626
+ totalFavorited: number | null;
627
+ firstSeenAt: string;
628
+ lastRefreshedAt: string;
629
+ }
630
+ /** What the report was actually computed over. */
631
+ interface CreatorAnalysisSample {
632
+ videosAnalyzed: number;
633
+ /** RFC 3339 create-time of the oldest / newest analysed video (`null` if none carried one). */
634
+ oldestVideoAt: string | null;
635
+ newestVideoAt: string | null;
636
+ /** Days between oldest and newest analysed video. `null` with fewer than two dated videos. */
637
+ spanDays: number | null;
638
+ /** `videosAnalyzed / awemeCount`, 0..1 — how much of the account this report saw. */
639
+ shareOfCatalog: number | null;
640
+ }
641
+ /**
642
+ * Engagement over the sample. Rates are **sums over sums** (total likes ÷ total plays), not the
643
+ * mean of per-video rates: one viral video should move the number in proportion to its size, and
644
+ * averaging ratios lets a 200-view video weigh as much as a 20M-view one.
645
+ */
646
+ interface CreatorAnalysisEngagement {
647
+ totalPlays: number;
648
+ totalLikes: number;
649
+ totalComments: number;
650
+ totalShares: number;
651
+ avgPlays: number;
652
+ medianPlays: number;
653
+ avgLikes: number;
654
+ avgComments: number;
655
+ avgShares: number;
656
+ /** (likes + comments + shares) ÷ plays. `null` when no analysed video carries a play count. */
657
+ engagementRate: number | null;
658
+ likeRate: number | null;
659
+ commentRate: number | null;
660
+ shareRate: number | null;
661
+ /** avgPlays ÷ followers — reach per follower, `null` without a follower count. */
662
+ viewsPerFollower: number | null;
663
+ }
664
+ /** Posting rhythm over the sample window. */
665
+ interface CreatorAnalysisCadence {
666
+ postsPerWeek: number | null;
667
+ avgDaysBetweenPosts: number | null;
668
+ longestGapDays: number | null;
669
+ /** 0 = Sunday … 6 = Saturday (UTC), by post count. `null` when nothing is dated. */
670
+ busiestWeekday: number | null;
671
+ /** Post counts per weekday, index 0 = Sunday (UTC). */
672
+ postsByWeekday: number[];
673
+ }
674
+ interface CreatorAnalysisVideo {
675
+ videoId: string;
676
+ description: string | null;
677
+ createTime: string | null;
678
+ playCount: number | null;
679
+ diggCount: number | null;
680
+ commentCount: number | null;
681
+ shareCount: number | null;
682
+ /** This video's own (likes + comments + shares) ÷ plays. */
683
+ engagementRate: number | null;
684
+ }
685
+ /** Comment-side signal, present only for videos whose comments were collected (R4.3). */
686
+ interface CreatorAnalysisAudience {
687
+ commentsAnalyzed: number;
688
+ videosWithComments: number;
689
+ distinctCommenters: number;
690
+ avgLikesPerComment: number | null;
691
+ /** Commenters seen most often across the analysed comments, most-frequent first. */
692
+ topCommenters: {
693
+ uniqueId: string | null;
694
+ nickname: string | null;
695
+ avatarUrl: string | null;
696
+ comments: number;
697
+ likes: number;
698
+ }[];
699
+ /** Language mix of the analysed comments as a share of the total, most-common first. */
700
+ languages: {
701
+ language: string;
702
+ share: number;
703
+ }[];
704
+ }
705
+ /** Follower movement across retained profile snapshots. */
706
+ interface CreatorAnalysisGrowth {
707
+ fromObservedAt: string;
708
+ toObservedAt: string;
709
+ days: number;
710
+ startFollowers: number;
711
+ endFollowers: number;
712
+ followerDelta: number;
713
+ /** Fractional change (0.042 = +4.2%). `null` when the first observation had no followers. */
714
+ followerDeltaRate: number | null;
715
+ }
716
+ /** A machine-readable caveat. The UI shows `message`; automation can branch on `code`. */
717
+ interface CreatorAnalysisLimit {
718
+ code: "thin-video-sample" | "no-videos" | "no-play-counts" | "undated-videos" | "single-observation" | "no-comments";
719
+ message: string;
720
+ }
721
+ interface CreatorAnalysisData {
722
+ creator: CreatorAnalysisSubject;
723
+ sample: CreatorAnalysisSample;
724
+ /** `null` when we hold no videos for the creator — the report then covers profile counts only. */
725
+ engagement: CreatorAnalysisEngagement | null;
726
+ cadence: CreatorAnalysisCadence | null;
727
+ topByPlays: CreatorAnalysisVideo[];
728
+ topByEngagement: CreatorAnalysisVideo[];
729
+ /** `null` until R4.3 has collected comments for at least one of the analysed videos. */
730
+ audience: CreatorAnalysisAudience | null;
731
+ /** `null` until the creator has been observed at least twice. */
732
+ growth: CreatorAnalysisGrowth | null;
733
+ limits: CreatorAnalysisLimit[];
734
+ }
735
+ type CreatorAnalysisResponse = ProvenanceEnvelope<CreatorAnalysisData>;
736
+
737
+ /** One retained observation of a video's metrics over time. */
738
+ interface VideoMetricHistoryPoint {
739
+ capturedAt: string;
740
+ playCount: number | null;
741
+ diggCount: number | null;
742
+ commentCount: number | null;
743
+ shareCount: number | null;
744
+ collectCount: number | null;
745
+ }
746
+ interface VideoData {
747
+ videoId: string;
748
+ tiktokUserId: string;
749
+ uniqueId: string;
750
+ description: string | null;
751
+ createTime: string | null;
752
+ coverUrl: string | null;
753
+ durationSeconds: number | null;
754
+ playCount: number | null;
755
+ diggCount: number | null;
756
+ commentCount: number | null;
757
+ shareCount: number | null;
758
+ collectCount: number | null;
759
+ firstSeenAt: string;
760
+ lastRefreshedAt: string;
761
+ }
762
+ /** A single video with its metric history. */
763
+ interface VideoWithHistory extends VideoData {
764
+ history: VideoMetricHistoryPoint[];
765
+ }
766
+ type CreatorVideosResponse = ProvenanceEnvelope<{
767
+ uniqueId: string;
768
+ tiktokUserId: string;
769
+ videos: VideoData[];
770
+ }>;
771
+ type VideoDetailResponse = ProvenanceEnvelope<{
772
+ video: VideoWithHistory;
773
+ }>;
774
+ /**
775
+ * D6.1 — one entry in a creator's active story tray. Ephemeral content fetched fresh through the
776
+ * signed mobile path (never stored), so a response is always the tray as it was at `observedAt`.
777
+ * Mirrors `PublicCreatorStory` in `@v2/tiktok-runtime` (kept here so `@v2/contracts` stays a leaf).
778
+ */
779
+ interface StoryData {
780
+ /** Stable story/aweme id (string — exceeds 2^53, never JSON-parsed as a number). */
781
+ itemId: string;
782
+ kind: "photo" | "video";
783
+ /** Browser-renderable still: the photo, or the video cover. */
784
+ imageUrl: string | null;
785
+ /** Playable video address for a video story; `null` for a photo story. */
786
+ videoUrl: string | null;
787
+ createdAt: string | null;
788
+ expiresAt: string | null;
789
+ viewed: boolean;
790
+ isSubscriberOnly: boolean;
791
+ }
792
+ type CreatorStoriesResponse = ProvenanceEnvelope<{
793
+ uniqueId: string;
794
+ stories: StoryData[];
795
+ }>;
796
+ /**
797
+ * One public comment on a video (R4.3). Collected anonymously from TikTok's web comment endpoint
798
+ * and served in the order TikTok ranks them (its own relevance model, not chronological).
799
+ */
800
+ interface VideoCommentData {
801
+ commentId: string;
802
+ videoId: string;
803
+ /** `null` for a top-level comment; the comment this one answers otherwise. */
804
+ parentCommentId: string | null;
805
+ authorUserId: string | null;
806
+ authorSecUid: string | null;
807
+ authorUniqueId: string | null;
808
+ authorNickname: string | null;
809
+ authorAvatarUrl: string | null;
810
+ text: string;
811
+ likeCount: number | null;
812
+ /** Replies under this comment upstream — fetch them with `?parentCommentId=<commentId>`. */
813
+ replyCount: number | null;
814
+ /** When the comment was posted; `null` when TikTok omits it. */
815
+ commentedAt: string | null;
816
+ language: string | null;
817
+ pinnedByAuthor: boolean;
818
+ likedByAuthor: boolean;
819
+ firstSeenAt: string;
820
+ lastRefreshedAt: string;
821
+ }
822
+ /**
823
+ * A page of comments. `nextCursor` is opaque — pass it back verbatim as `?cursor=`; `null` means
824
+ * there is nothing more to serve. `totalReported` is TikTok's own count for the thread, which is
825
+ * normally far larger than what we hold (the collector pages a bounded depth per request).
826
+ */
827
+ type VideoCommentsResponse = ProvenanceEnvelope<{
828
+ videoId: string;
829
+ parentCommentId: string | null;
830
+ comments: VideoCommentData[];
831
+ nextCursor: string | null;
832
+ hasMore: boolean;
833
+ totalReported: number | null;
834
+ storedCount: number;
835
+ }>;
836
+
837
+ type TrendType = "hashtag" | "sound";
838
+ interface TrendData {
839
+ trendType: TrendType;
840
+ trendId: string;
841
+ region: string;
842
+ title: string | null;
843
+ rank: number | null;
844
+ videoCount: number | null;
845
+ viewCount: number | null;
846
+ firstSeenAt: string;
847
+ lastRefreshedAt: string;
848
+ }
849
+ type TrendsResponse = ProvenanceEnvelope<{
850
+ region: string;
851
+ trends: TrendData[];
852
+ }>;
853
+
854
+ type RealtimeSubscriptionStatus = "queued" | "active" | "unavailable" | "offline" | "unsubscribed";
855
+ interface RealtimeStatusFrame {
856
+ type: "status";
857
+ creatorId: string;
858
+ status: RealtimeSubscriptionStatus;
859
+ roomId?: string;
860
+ reason?: string;
861
+ requestId?: string;
862
+ }
863
+ /**
864
+ * Public realtime event frame. The event names deliberately match the event set stream-worker
865
+ * actually publishes; synthetic names are not advertised ahead of an upstream implementation.
866
+ */
867
+ interface RealtimeEventFrame<T extends LiveEventType = LiveEventType> {
868
+ type: "event";
869
+ event: T;
870
+ eventId: string;
871
+ creatorId: string;
872
+ roomId: string;
873
+ sequence: number;
874
+ roomState: Extract<RoomRuntimeState, "active" | "ended" | "degraded">;
875
+ data: LiveEventPayloadByType[T];
876
+ provenance: Provenance;
877
+ }
878
+ interface RealtimeErrorFrame {
879
+ type: "error";
880
+ code: "invalid_message" | "invalid_creator" | "subscription_failed";
881
+ message: string;
882
+ requestId?: string;
883
+ retryable: boolean;
884
+ }
885
+
886
+ type ExportDataset = "live_sessions" | "rankings" | "gifters" | "creator_roster";
887
+ /**
888
+ * The file formats the export writer can actually produce — the single runtime source of truth for
889
+ * "does the system support this format at all". `SUPPORTED_FORMATS` in the exports service derives
890
+ * from this list, and the pricing-page anti-drift test (P1.9 §3) asserts every `<format> export`
891
+ * line the pricing page advertises is a member of this set, so the shop window can never promise a
892
+ * format the code cannot emit. Tier *entitlement* (which formats YOUR plan may use) is a separate
893
+ * axis, enforced from `TIER_FEATURES.exportFormats` in `@v2/billing`.
894
+ */
895
+ declare const EXPORT_FORMATS: readonly ["csv", "xlsx"];
896
+ type ExportFormat = (typeof EXPORT_FORMATS)[number];
897
+ type ExportJobStatus = "pending" | "ready" | "failed";
898
+ interface ExportJob {
899
+ id: string;
900
+ dataset: ExportDataset;
901
+ format: ExportFormat;
902
+ status: ExportJobStatus;
903
+ rowCount: number;
904
+ filename: string;
905
+ /** Relative download path, present once the job is `ready`. */
906
+ downloadUrl: string | null;
907
+ error: string | null;
908
+ createdAt: string;
909
+ completedAt: string | null;
910
+ }
911
+ interface ExportJobListResponse {
912
+ items: ExportJob[];
913
+ }
914
+
915
+ /** Capability modules a workspace can be entitled to; sold separately or bundled (P1.1). */
916
+ type EntitlementModule = "live" | "lookup";
917
+ /** Commercial tier a workspace's plan normalises to. `agency` is the top published tier (P1.3). */
918
+ type EntitlementTier = "free" | "starter" | "pro" | "scale" | "agency" | "enterprise";
919
+ interface EntitlementLimits {
920
+ /** Max API-key requests per `rateWindowMs` (per key), matching the enforced sliding window. */
921
+ requestsPerWindow: number;
922
+ rateWindowMs: number;
923
+ /**
924
+ * P1.4 B4 — the same per-second ceiling the pricing page publishes, expressed per second regardless
925
+ * of how `rateWindowMs` is configured, so a client can size its own concurrency without inferring it
926
+ * from the window. `requestsPerMinute` is exactly this × 60.
927
+ */
928
+ apiRequestsPerSecond: number;
929
+ /** The enforced rolling per-UTC-day request quota. 0 = unlimited. Exceeding it returns 429. */
930
+ requestsPerDay: number;
931
+ /** Active API keys this workspace may hold; `POST /v1/api-keys` rejects beyond it. 0 = unlimited. */
932
+ maxApiKeys: number;
933
+ /** Realtime WS API: max concurrent subscribed rooms (demand-controller admission, D2.4). */
934
+ wsConcurrencyCap: number;
935
+ /** Realtime WS API: monthly room-minute ceiling. */
936
+ maxRoomMinutesPerMonth: number;
937
+ /**
938
+ * P1.7 — how far back (days from now) this workspace may READ its own history on the rankings /
939
+ * live-session / export routes. `null` = unlimited (the full retained record). This is a read
940
+ * window, not retention: raising the tier instantly widens it, it never deletes data.
941
+ */
942
+ historyWindowDays: number | null;
943
+ /**
944
+ * P1.8 — max active seats (members + pending invites) this workspace's plan allows. `0` = unlimited.
945
+ * `POST /v1/workspace/members/invite` rejects `409 seat_limit_reached` beyond it.
946
+ */
947
+ maxSeats: number;
948
+ /**
949
+ * P1.8 — seats in use right now (active members + pending invites), so the UI can show "N/M seats"
950
+ * and warn when a downgrade left the workspace over its cap, without recomputing it client-side.
951
+ */
952
+ seatsUsed: number;
953
+ /**
954
+ * P1.9 — the export file formats this workspace's plan is entitled to (`csv` / `xlsx`). `[]` = no
955
+ * export at all (Free). `POST /v1/exports` refuses `403 export_format_not_entitled` for a valid
956
+ * format not in this list, so the UI can render only the formats the plan can actually run.
957
+ */
958
+ exportFormats: ExportFormat[];
959
+ }
960
+ interface EntitlementCredits {
961
+ /** Billing period this balance covers, `YYYY-MM-01`. */
962
+ period: string;
963
+ monthlyAllotment: number;
964
+ hardCap: number;
965
+ spent: number;
966
+ remaining: number;
967
+ }
968
+ interface Entitlement {
969
+ workspaceId: string;
970
+ /** Raw `plans.code` currently assigned, or null when the workspace has no assignment. */
971
+ planCode: string | null;
972
+ tier: EntitlementTier;
973
+ /** Modules the workspace BOUGHT (or the bundle) — used at its own tier. */
974
+ modules: EntitlementModule[];
975
+ /**
976
+ * Modules the workspace did not buy but may use at FREE-tier terms (`FREE_FALLBACK_MODULES` in
977
+ * `@v2/billing` — today Lookup for a Live-only buyer; never Live). Empty for a bundle. A surface whose
978
+ * module is here is reachable (free caps/history), not locked behind the Plans upsell.
979
+ */
980
+ freeFallbackModules: EntitlementModule[];
981
+ /** Limits of the PURCHASED tier. Requests on a `freeFallbackModules` module run under `TIER_CAPS.free`
982
+ * (their own throttle bucket), not these. */
983
+ limits: EntitlementLimits;
984
+ credits: EntitlementCredits;
985
+ }
986
+ /** `GET /v1/entitlements` response. */
987
+ type EntitlementResponse = ProvenanceEnvelope<Entitlement>;
988
+
989
+ type RoomRuntimeState = "discovered" | "pending_bootstrap" | "connecting" | "active" | "degraded" | "ended" | "closed";
990
+
991
+ type LiveEventType = "chat" | "gift" | "member" | "like" | "social" | "roomUser" | "control" | "envelope" | "goodyBag" | "subscribe" | "linkMicBattle" | "linkMicArmies" | "hourlyRank" | "liveStart" | "unknown";
992
+ interface ImageData {
993
+ urls: string[];
994
+ uri: string | null;
995
+ width: number | null;
996
+ height: number | null;
997
+ }
998
+ interface TextData {
999
+ displayType: string;
1000
+ defaultPattern: string;
1001
+ }
1002
+ interface CommonMessageData {
1003
+ method: string;
1004
+ msgId: string;
1005
+ roomId: string;
1006
+ createTime: string;
1007
+ logId: string;
1008
+ displayText: TextData | null;
1009
+ }
1010
+ /**
1011
+ * One of the user's live badges (D2.15). TikTok leads a feed row with the viewer's level/grade badge
1012
+ * and any subscriber/fans badge; the level/tier is encoded in the badge ARTWORK
1013
+ * (`grade_badge_icon_lite_lv15…`, `fans_badge_icon_lv1…`), so `imageUrl` is the icon a client renders
1014
+ * — never a level integer we would have to infer. `type` is TikTok's own badge-kind discriminator
1015
+ * (observed 20 = grade/level, 30 = fans/subscriber) carried verbatim for ordering/styling.
1016
+ */
1017
+ interface LiveUserBadge {
1018
+ type: number;
1019
+ imageUrl: string;
1020
+ }
1021
+ interface UserData {
1022
+ userId: string;
1023
+ nickname: string;
1024
+ uniqueId: string;
1025
+ secUid: string;
1026
+ verified: boolean;
1027
+ avatar: ImageData | null;
1028
+ /** Level/grade and subscriber badges, in TikTok's own order; empty when the frame carried none. */
1029
+ badges: LiveUserBadge[];
1030
+ }
1031
+ interface GiftDetailsData {
1032
+ id: string;
1033
+ giftName: string;
1034
+ describe: string;
1035
+ diamondCount: number;
1036
+ giftType: number;
1037
+ combo: boolean;
1038
+ image: ImageData | null;
1039
+ previewImage: ImageData | null;
1040
+ }
1041
+ interface ChatMessageData {
1042
+ common: CommonMessageData | null;
1043
+ user: UserData | null;
1044
+ comment: string;
1045
+ }
1046
+ interface GiftMessageData {
1047
+ common: CommonMessageData | null;
1048
+ user: UserData | null;
1049
+ toUser: UserData | null;
1050
+ giftId: string;
1051
+ fanTicketCount: number;
1052
+ groupCount: number;
1053
+ repeatCount: number;
1054
+ comboCount: number;
1055
+ repeatEnd: boolean;
1056
+ groupId: string;
1057
+ logId: string;
1058
+ isStreaking: boolean;
1059
+ isCompleted: boolean;
1060
+ gift: GiftDetailsData | null;
1061
+ }
1062
+ interface MemberMessageData {
1063
+ common: CommonMessageData | null;
1064
+ user: UserData | null;
1065
+ operator: UserData | null;
1066
+ action: number;
1067
+ memberCount: number;
1068
+ actionDescription: string;
1069
+ userId: string;
1070
+ }
1071
+ interface LikeMessageData {
1072
+ common: CommonMessageData | null;
1073
+ user: UserData | null;
1074
+ likeCount: number;
1075
+ totalLikeCount: number;
1076
+ color: number;
1077
+ }
1078
+ type SocialType = "follow" | "share" | "other";
1079
+ interface SocialMessageData {
1080
+ common: CommonMessageData | null;
1081
+ user: UserData | null;
1082
+ socialType: SocialType;
1083
+ action: string;
1084
+ shareType: string;
1085
+ shareTarget: string;
1086
+ followCount: number;
1087
+ shareDisplayStyle: string;
1088
+ shareCount: number;
1089
+ signature: string;
1090
+ signatureVersion: string;
1091
+ showDurationMs: string;
1092
+ displayType: string | null;
1093
+ }
1094
+ interface ContributorData {
1095
+ coinCount: number;
1096
+ rank: number;
1097
+ delta: string;
1098
+ user: UserData | null;
1099
+ }
1100
+ interface RoomUserMessageData {
1101
+ common: CommonMessageData | null;
1102
+ viewerCount: number;
1103
+ totalUser: number;
1104
+ popularity: string;
1105
+ anonymous: string;
1106
+ popStr: string;
1107
+ topViewers: ContributorData[];
1108
+ seats: ContributorData[];
1109
+ }
1110
+ type ControlActionType = "STREAM_PAUSED" | "STREAM_UNPAUSED" | "STREAM_ENDED" | "STREAM_SUSPENDED" | "UNKNOWN";
1111
+ interface ControlExtraData {
1112
+ banInfoUrl: string;
1113
+ reasonNo: string;
1114
+ title: TextData | null;
1115
+ violationReason: TextData | null;
1116
+ content: TextData | null;
1117
+ gotItButton: TextData | null;
1118
+ banDetailButton: TextData | null;
1119
+ source: string;
1120
+ }
1121
+ interface ControlMessageData {
1122
+ common: CommonMessageData | null;
1123
+ action: ControlActionType;
1124
+ actionCode: number;
1125
+ isStreamEnd: boolean;
1126
+ tips: string;
1127
+ floatStyle: number;
1128
+ floatText: TextData | null;
1129
+ perceptionAudienceText: TextData | null;
1130
+ extra: ControlExtraData | null;
1131
+ }
1132
+ interface EnvelopeInfo {
1133
+ envelopeId: string;
1134
+ businessType: number;
1135
+ envelopeIdc: string;
1136
+ sendUserName: string;
1137
+ diamondCount: number;
1138
+ peopleCount: number;
1139
+ unpackAt: number;
1140
+ sendUserId: string;
1141
+ createAt: string;
1142
+ roomId: string;
1143
+ followShowStatus: number;
1144
+ skinId: number;
1145
+ }
1146
+ interface EnvelopeMessageData {
1147
+ display: number;
1148
+ envelopeInfo: EnvelopeInfo | null;
1149
+ }
1150
+ interface GoodyBagMessageData {
1151
+ roomId: string;
1152
+ goodyBagId: string;
1153
+ biz: string;
1154
+ diamondCount: number;
1155
+ peopleCount: number;
1156
+ countDownMinutes: number;
1157
+ unpackAt: number;
1158
+ joinedHeadcount: number;
1159
+ participateMethod: string;
1160
+ participateMethodContent: string;
1161
+ participateThresholdSelected: string | null;
1162
+ }
1163
+ /** A new/renewed channel subscription (WebcastSubNotifyMessage) — a direct monetization signal. */
1164
+ interface SubscribeMessageData {
1165
+ common: CommonMessageData | null;
1166
+ user: UserData | null;
1167
+ /** Consecutive months the user has been subscribed (the headline value). */
1168
+ subMonth: number;
1169
+ /** TikTok's subscribe-type discriminator (new vs renew vs gifted), carried verbatim. */
1170
+ subscribeType: number;
1171
+ /** Prior subscription status code, verbatim. */
1172
+ oldSubscribeStatus: number;
1173
+ /** Current subscription status code, verbatim. */
1174
+ subscribingStatus: number;
1175
+ /** UI exhibition-type code, verbatim. */
1176
+ exhibitionType: number;
1177
+ }
1178
+ /** One creator on a PK/LinkMic battle roster (WebcastLinkMicBattle → battleUsers). */
1179
+ interface LinkMicBattleParticipant {
1180
+ user: UserData | null;
1181
+ }
1182
+ /** The start of a PK / versus battle — who is competing (WebcastLinkMicBattle). */
1183
+ interface LinkMicBattleData {
1184
+ participants: LinkMicBattleParticipant[];
1185
+ }
1186
+ /** One supporter "army" behind a host in a live PK, with its running score. */
1187
+ interface LinkMicArmiesGroup {
1188
+ /** The team's current battle points (the live PK scoreboard value). */
1189
+ points: number;
1190
+ /** Supporters counted into this team's points. */
1191
+ users: UserData[];
1192
+ }
1193
+ /** One host's side of a PK battle (WebcastLinkMicArmies → battleItems). */
1194
+ interface LinkMicArmiesTeam {
1195
+ hostUserId: string;
1196
+ groups: LinkMicArmiesGroup[];
1197
+ }
1198
+ /** Live PK battle status + per-side scores (WebcastLinkMicArmies) — the running versus scoreboard. */
1199
+ interface LinkMicArmiesData {
1200
+ /** TikTok battle-status code (e.g. in-progress vs ended), carried verbatim. */
1201
+ battleStatus: number;
1202
+ teams: LinkMicArmiesTeam[];
1203
+ }
1204
+ /** The room's hourly-rank standing (WebcastHourlyRankMessage). */
1205
+ interface HourlyRankData {
1206
+ /** Ranking board type key, verbatim. */
1207
+ type: string;
1208
+ /** Human-readable rank label TikTok renders (e.g. the rank position text). */
1209
+ label: string;
1210
+ /** Rank item id, verbatim (bigint-safe string). */
1211
+ rankId: string;
1212
+ /** Rank item colour token, verbatim. */
1213
+ colour: string;
1214
+ }
1215
+ interface RawLiveEventPayload {
1216
+ messageType: string;
1217
+ msgId: string;
1218
+ msgType: number;
1219
+ offset: string;
1220
+ isHistory: boolean;
1221
+ payloadBase64: string;
1222
+ score?: number;
1223
+ decodeError?: string;
1224
+ /**
1225
+ * Fields the decoder recognised but refused to read because their wire type no longer matches
1226
+ * what our accessor expects — i.e. TikTok reshaped that field. The message still decodes (the
1227
+ * field is skipped rather than misread, which would desync everything after it), so this is the
1228
+ * ONLY signal that the value is missing rather than genuinely absent.
1229
+ *
1230
+ * It exists because the alternative is silent: a reshaped `diamondCount` would otherwise produce
1231
+ * a perfectly-shaped gift worth 0 diamonds, feeding whale rollups and alert thresholds with
1232
+ * zeroes and tripping no alarm. Each entry is `"<decoder>.<fieldNo>: expected wire N, got M"`.
1233
+ */
1234
+ decodeWarnings?: string[];
1235
+ }
1236
+ interface ChatLiveEventPayload extends RawLiveEventPayload {
1237
+ decoded: ChatMessageData;
1238
+ }
1239
+ interface GiftLiveEventPayload extends RawLiveEventPayload {
1240
+ decoded: GiftMessageData;
1241
+ }
1242
+ interface MemberLiveEventPayload extends RawLiveEventPayload {
1243
+ decoded: MemberMessageData;
1244
+ }
1245
+ interface LikeLiveEventPayload extends RawLiveEventPayload {
1246
+ decoded: LikeMessageData;
1247
+ }
1248
+ interface SocialLiveEventPayload extends RawLiveEventPayload {
1249
+ decoded: SocialMessageData;
1250
+ }
1251
+ interface RoomUserLiveEventPayload extends RawLiveEventPayload {
1252
+ decoded: RoomUserMessageData;
1253
+ }
1254
+ interface ControlLiveEventPayload extends RawLiveEventPayload {
1255
+ decoded: ControlMessageData;
1256
+ }
1257
+ interface EnvelopeLiveEventPayload extends RawLiveEventPayload {
1258
+ viewerCountSnapshot?: number | null;
1259
+ decoded: EnvelopeMessageData;
1260
+ }
1261
+ interface GoodyBagLiveEventPayload {
1262
+ messageType: "GoodyBagRoomResponse";
1263
+ source: "mobile-api";
1264
+ fetchedAt: string;
1265
+ score: number;
1266
+ viewerCountSnapshot?: number | null;
1267
+ decoded: GoodyBagMessageData;
1268
+ }
1269
+ interface SubscribeLiveEventPayload extends RawLiveEventPayload {
1270
+ decoded: SubscribeMessageData;
1271
+ }
1272
+ interface LinkMicBattleLiveEventPayload extends RawLiveEventPayload {
1273
+ decoded: LinkMicBattleData;
1274
+ }
1275
+ interface LinkMicArmiesLiveEventPayload extends RawLiveEventPayload {
1276
+ decoded: LinkMicArmiesData;
1277
+ }
1278
+ interface HourlyRankLiveEventPayload extends RawLiveEventPayload {
1279
+ decoded: HourlyRankData;
1280
+ }
1281
+ /**
1282
+ * LT1.5 — a synthetic "creator went live" marker emitted by stream-worker when a room transitions to
1283
+ * `active` (it is NOT decoded from a TikTok frame). Distinct `type` (not `control`, which is also the
1284
+ * session-END marker) so an alert rule can target "went live" without also firing on "ended". Its
1285
+ * `roomState` is `"active"` and it is deliberately never `isStreamEnd`, so the history session-end
1286
+ * consumer ignores it.
1287
+ */
1288
+ interface LiveStartMessageData {
1289
+ /** ISO instant the room was observed to transition to active. */
1290
+ startedAt: string;
1291
+ }
1292
+ interface LiveStartLiveEventPayload extends RawLiveEventPayload {
1293
+ decoded: LiveStartMessageData;
1294
+ }
1295
+ interface UnknownLiveEventPayload extends RawLiveEventPayload {
1296
+ decoded?: never;
1297
+ }
1298
+ interface LiveEventPayloadByType {
1299
+ chat: ChatLiveEventPayload;
1300
+ gift: GiftLiveEventPayload;
1301
+ member: MemberLiveEventPayload;
1302
+ like: LikeLiveEventPayload;
1303
+ social: SocialLiveEventPayload;
1304
+ roomUser: RoomUserLiveEventPayload;
1305
+ control: ControlLiveEventPayload;
1306
+ envelope: EnvelopeLiveEventPayload;
1307
+ goodyBag: GoodyBagLiveEventPayload;
1308
+ subscribe: SubscribeLiveEventPayload;
1309
+ linkMicBattle: LinkMicBattleLiveEventPayload;
1310
+ linkMicArmies: LinkMicArmiesLiveEventPayload;
1311
+ hourlyRank: HourlyRankLiveEventPayload;
1312
+ liveStart: LiveStartLiveEventPayload;
1313
+ unknown: UnknownLiveEventPayload;
1314
+ }
1315
+
1316
+ interface TokTikResponse {
1317
+ ok: boolean;
1318
+ status: number;
1319
+ text(): Promise<string>;
1320
+ }
1321
+ type TokTikFetch = (url: string, init: {
1322
+ method: "GET" | "POST";
1323
+ headers: Record<string, string>;
1324
+ body?: string;
1325
+ }) => Promise<TokTikResponse>;
1326
+ interface TokTikClientOptions {
1327
+ apiKey: string;
1328
+ baseUrl?: string;
1329
+ fetch?: TokTikFetch;
1330
+ }
1331
+ /**
1332
+ * A non-2xx answer from the API.
1333
+ *
1334
+ * The status is deliberately surfaced rather than folded into a generic failure: the four the API
1335
+ * uses to say something actionable each need a different reaction from the caller, and swallowing
1336
+ * them is the failure mode this SDK exists to prevent.
1337
+ *
1338
+ * - `401` bad/expired key · `403` the key lacks the scope this resource is sold under
1339
+ * - `402` out of credits — buying more is the only fix; retrying will not help
1340
+ * - `429` rate limited — retry after backing off
1341
+ */
1342
+ declare class TokTikApiError extends Error {
1343
+ readonly status: number;
1344
+ readonly code: string | undefined;
1345
+ readonly requestId: string | undefined;
1346
+ constructor(status: number, code: string | undefined, requestId: string | undefined, message: string);
1347
+ /** True when retrying the identical request could plausibly succeed. */
1348
+ get retryable(): boolean;
1349
+ /** Out of credits. Distinct from `403`: the key is valid and scoped, the wallet is empty. */
1350
+ get isPaymentRequired(): boolean;
1351
+ /** The key is missing the scope this endpoint is sold under (or is not a data-plane key at all). */
1352
+ get isForbidden(): boolean;
1353
+ get isUnauthorized(): boolean;
1354
+ get isRateLimited(): boolean;
1355
+ }
1356
+ declare function buildQuery(parameters?: Record<string, unknown>): string;
1357
+ /** Shared request plumbing. Deliberately has no retries, caching, or credential storage. */
1358
+ declare class HttpTransport {
1359
+ readonly baseUrl: string;
1360
+ private readonly fetchImpl;
1361
+ private readonly apiKey;
1362
+ constructor(options: TokTikClientOptions);
1363
+ get<T>(path: string, parameters?: Record<string, unknown>): Promise<T>;
1364
+ post<T>(path: string, body?: unknown): Promise<T>;
1365
+ /**
1366
+ * For endpoints that answer with something other than JSON (today: the CSV export download).
1367
+ * Errors are still parsed as JSON, because a failure is always the JSON error envelope.
1368
+ */
1369
+ getText(path: string, parameters?: Record<string, unknown>): Promise<string>;
1370
+ private request;
1371
+ }
1372
+
1373
+ /**
1374
+ * Every option name below is transcribed from the route's own Fastify schema in
1375
+ * `apps/platform-api/src/routes/**`. Those schemas set `additionalProperties: false`, so an invented
1376
+ * name is a hard 400 — not a silently ignored extra. Do not add a field here without reading the
1377
+ * matching schema first.
1378
+ */
1379
+ /** `/v1/live/sessions` — `paginationProperties`: `cursor` (ISO date-time keyset) + `limit`. */
1380
+ interface LiveSessionsOptions {
1381
+ creatorUniqueId?: string;
1382
+ cursor?: string;
1383
+ limit?: number;
1384
+ }
1385
+ interface CursorPageOptions {
1386
+ cursor?: string;
1387
+ limit?: number;
1388
+ }
1389
+ /** `/v1/rankings/official` — `board` is REQUIRED. */
1390
+ interface OfficialRankingOptions {
1391
+ board: string;
1392
+ region?: string;
1393
+ limit?: number;
1394
+ }
1395
+ /** `/v1/rankings/movers` — `board` is REQUIRED; `limit` (1–99, default 20) caps each of gainers/losers. */
1396
+ interface RankMoversOptions {
1397
+ board: string;
1398
+ region?: string;
1399
+ limit?: number;
1400
+ }
1401
+ /** `/v1/rankings/history` — `board` is REQUIRED; `limit` counts PERIODS (1–24), not rows. */
1402
+ interface RankingHistoryOptions {
1403
+ board: string;
1404
+ region?: string;
1405
+ limit?: number;
1406
+ cursor?: string;
1407
+ leagueTier?: string;
1408
+ }
1409
+ /** `/v1/rankings/gaming/games` — the only filter is `region` (a BUCKET key); omit it for the server default. */
1410
+ interface GamingGamesOptions {
1411
+ region?: string;
1412
+ }
1413
+ /** `/v1/creators` — search only takes `q` + `limit`. No cursor. */
1414
+ interface CreatorListOptions {
1415
+ q?: string;
1416
+ limit?: number;
1417
+ region?: string;
1418
+ minFollowers?: number;
1419
+ liveNow?: boolean;
1420
+ /** `followers` (default) or `rank` (the region's current Daily board order). */
1421
+ sort?: "followers" | "rank";
1422
+ cursor?: string;
1423
+ }
1424
+ /** `/v1/creators/:uid/changes` — `limit` ONLY; this route has no cursor. */
1425
+ interface CreatorChangesOptions {
1426
+ limit?: number;
1427
+ }
1428
+ /** `/v1/trends` — `type` is an enum of exactly these two. */
1429
+ interface TrendOptions {
1430
+ region?: string;
1431
+ type?: "hashtag" | "sound";
1432
+ }
1433
+ /** Gifter routes are scoped to a discrete observation window (24h/7d/30d, default 7d), not a cursor. */
1434
+ interface GifterWindowOptions {
1435
+ window?: GifterWindow;
1436
+ limit?: number;
1437
+ }
1438
+ interface GifterListOptions extends GifterWindowOptions {
1439
+ creator?: string;
1440
+ }
1441
+ interface CommentOptions extends CursorPageOptions {
1442
+ parentCommentId?: string;
1443
+ }
1444
+ /** `POST /v1/exports` body. `dataset`/`format` are REQUIRED single-value enums today. */
1445
+ interface ExportCreateInput {
1446
+ dataset: "live_sessions";
1447
+ format: "csv";
1448
+ rangeDays?: number;
1449
+ }
1450
+ /** Mirrors `DeveloperUsageResponse` in `apps/platform-api/src/services/usage/usage-service.ts`. */
1451
+ interface DeveloperUsage {
1452
+ period: string;
1453
+ balance: {
1454
+ remaining: number;
1455
+ spent: number;
1456
+ };
1457
+ items: Array<{
1458
+ id: string;
1459
+ apiKeyId: string | null;
1460
+ apiKeyPrefix: string | null;
1461
+ apiKeyLabel: string | null;
1462
+ operation: string;
1463
+ creditDelta: number;
1464
+ createdAt: string;
1465
+ }>;
1466
+ }
1467
+ interface UsageOptions {
1468
+ /** First day of a month: `YYYY-MM-01`. */
1469
+ period?: string;
1470
+ limit?: number;
1471
+ }
1472
+ /** `live:read` + `live:stream` — LIVE sessions, per-creator performance, realtime handshake token. */
1473
+ declare class LiveResource {
1474
+ private readonly http;
1475
+ constructor(http: HttpTransport);
1476
+ listSessions(options?: LiveSessionsOptions): Promise<ProvenanceEnvelope<LiveSessionListResponse>>;
1477
+ getSession(id: string): Promise<ProvenanceEnvelope<LiveSessionRecord>>;
1478
+ creatorPerformance(creatorUniqueId: string, options?: CursorPageOptions): Promise<ProvenanceEnvelope<LiveCreatorPerformanceListResponse>>;
1479
+ /**
1480
+ * Mint a short-lived realtime handshake token (D2.3). `client.live.stream()` calls this for you —
1481
+ * use it directly only when driving the WebSocket yourself.
1482
+ */
1483
+ streamToken(): Promise<{
1484
+ token: string;
1485
+ tokenType: "Bearer";
1486
+ expiresIn: number;
1487
+ wsUrl: string;
1488
+ }>;
1489
+ /**
1490
+ * LIVE video playback sources for a creator who is live now (D2.12). `live:stream`, not
1491
+ * `live:read` — this is the video, not the metrics. The customer's runtime fetches the stream
1492
+ * itself from TikTok's CDN. `status` is `live` (with `hlsUrl`/`flvUrl`), `no_playback`, or
1493
+ * `offline`; a room still being resolved answers `202` (poll again).
1494
+ */
1495
+ playback(creatorUniqueId: string): Promise<LivePlaybackResponse>;
1496
+ }
1497
+ /** `GET /v1/live/creators/:uid/playback` — polymorphic by `status`. */
1498
+ interface LivePlaybackResponse {
1499
+ status: "live" | "no_playback" | "offline";
1500
+ roomId?: string | null;
1501
+ title?: string | null;
1502
+ nickname?: string | null;
1503
+ hlsUrl?: string | null;
1504
+ flvUrl?: string | null;
1505
+ expiresAt?: string | null;
1506
+ startedAt?: string | null;
1507
+ avatarUrl?: string | null;
1508
+ verified?: boolean;
1509
+ followerCount?: number | null;
1510
+ region?: string | null;
1511
+ source?: string;
1512
+ cached?: boolean;
1513
+ message?: string | null;
1514
+ }
1515
+ /**
1516
+ * `rank:read`. Provenance-enveloped: `rankings-service.ts` returns
1517
+ * `ProvenanceEnvelope<OfficialRankingResponse>` — `{ data: { board }, provenance }`.
1518
+ */
1519
+ declare class RankingsResource {
1520
+ private readonly http;
1521
+ constructor(http: HttpTransport);
1522
+ /** `board` is required by the server; there is no default. */
1523
+ official(options: OfficialRankingOptions): Promise<ProvenanceEnvelope<OfficialRankingResponse>>;
1524
+ /**
1525
+ * Region "movers" — the creators whose rank changed most since the board's previous period, split
1526
+ * into `gainers` (moved up) and `losers` (moved down), each biggest-move-first and capped by
1527
+ * `limit` (1–99, default 20). Identity is masked by your plan tier exactly as `official`.
1528
+ */
1529
+ movers(options: RankMoversOptions): Promise<ProvenanceEnvelope<RankMoversResponse>>;
1530
+ /**
1531
+ * Retained periods of a board, newest first — the source for a rank trend.
1532
+ *
1533
+ * `limit` counts PERIODS (1–24, default 12), not rows: each period carries the whole 99-rank
1534
+ * board. Page with `cursor` (opaque). One request costs one credit regardless of how many
1535
+ * periods come back.
1536
+ */
1537
+ history(options: RankingHistoryOptions): Promise<ProvenanceEnvelope<RankingHistoryResponse>>;
1538
+ /**
1539
+ * The regions a board can be read for, with each one's coverage and the boards it has captured.
1540
+ *
1541
+ * Call this instead of guessing: boards are keyed by BUCKET — `US+`, `MENA`, `LATAM`, `DE+` — and
1542
+ * an ISO country code like `US` or `DE` matches no board at all, so it comes back coverage-gated
1543
+ * rather than empty. The list reflects what has actually been captured, not what is configured.
1544
+ *
1545
+ * Each region also carries `notOfferedBoards`: the boards proven absent from TikTok's product
1546
+ * there. Hide or disable only those. A board missing from `boards` may simply be one we have not
1547
+ * captured yet, and it still answers for itself with an honest `not-covered`.
1548
+ */
1549
+ regions(): Promise<ProvenanceEnvelope<RankingRegionsResponse>>;
1550
+ /**
1551
+ * The per-game gaming boards a region offers — the source for the `game` parameter of a
1552
+ * `board: "gaming_game"` read on `official` (and `movers` / `history`). Pass `games[].gameKey`
1553
+ * verbatim; never hardcode a game list (the R5.7 lesson applied to games).
1554
+ *
1555
+ * `status` is `offered` (readable now) or `not_offered` (a game TikTok has pulled). An empty
1556
+ * `games` array is a valid answer, not an error: the region runs only the aggregate gaming board,
1557
+ * so show no game selector. `region` is a BUCKET key exactly as `regions` reports; omit it to
1558
+ * accept the server default.
1559
+ */
1560
+ games(options?: GamingGamesOptions): Promise<ProvenanceEnvelope<GamingGamesResponse>>;
1561
+ }
1562
+ /** `creator:read` — profile, change history, analysis. Videos live on `content` (`content:read`). */
1563
+ declare class CreatorsResource {
1564
+ private readonly http;
1565
+ constructor(http: HttpTransport);
1566
+ /** Handle/name search. */
1567
+ list(options?: CreatorListOptions): Promise<CreatorSearchResponse>;
1568
+ get(uid: string): Promise<CreatorProfileResponse>;
1569
+ changes(uid: string, options?: CreatorChangesOptions): Promise<CreatorChangesResponse>;
1570
+ analysis(uid: string): Promise<CreatorAnalysisResponse>;
1571
+ /** R4.5 — the same read in both directions; a real upstream walk bills the heavier SKU. */
1572
+ following(uid: string, options?: CursorPageOptions): Promise<FollowListResponse>;
1573
+ followers(uid: string, options?: CursorPageOptions): Promise<FollowListResponse>;
1574
+ }
1575
+ /** `content:read` — a creator's videos, one video, its comments. Sold separately from the profile. */
1576
+ declare class ContentResource {
1577
+ private readonly http;
1578
+ constructor(http: HttpTransport);
1579
+ /** Takes no query parameters — the route declares no querystring schema and the service uses a
1580
+ * fixed internal limit. (A param passed here would be silently ignored, not rejected, which is why
1581
+ * an acceptance run cannot catch it — the route has no schema to reject against.) */
1582
+ creatorVideos(uid: string): Promise<CreatorVideosResponse>;
1583
+ /** D6.1 — the creator's active story tray, fetched fresh through the signed mobile path. Empty
1584
+ * tray (never an error) when no signed credential is configured. */
1585
+ creatorStories(uid: string): Promise<CreatorStoriesResponse>;
1586
+ video(videoId: string): Promise<VideoDetailResponse>;
1587
+ videoComments(videoId: string, options?: CommentOptions): Promise<VideoCommentsResponse>;
1588
+ }
1589
+ /** `gifter:read` — whale intelligence. Its own SKU, hence its own namespace. */
1590
+ declare class GiftersResource {
1591
+ private readonly http;
1592
+ constructor(http: HttpTransport);
1593
+ list(options?: GifterListOptions): Promise<ProvenanceEnvelope<GifterListResponse>>;
1594
+ get(gifterKey: string, options?: GifterWindowOptions): Promise<ProvenanceEnvelope<GifterProfileResponse>>;
1595
+ forCreator(handle: string, options?: GifterWindowOptions): Promise<ProvenanceEnvelope<CreatorGiftersResponse>>;
1596
+ }
1597
+ /** `trend:read` */
1598
+ declare class TrendsResource {
1599
+ private readonly http;
1600
+ constructor(http: HttpTransport);
1601
+ /** Note: this route takes no `limit`. */
1602
+ list(options?: TrendOptions): Promise<TrendsResponse>;
1603
+ }
1604
+ /** `export` — async bulk extracts. */
1605
+ declare class ExportsResource {
1606
+ private readonly http;
1607
+ constructor(http: HttpTransport);
1608
+ /** Takes no query parameters. */
1609
+ list(): Promise<ExportJobListResponse>;
1610
+ create(input: ExportCreateInput): Promise<ExportJob>;
1611
+ get(id: string): Promise<ExportJob>;
1612
+ /**
1613
+ * The finished extract itself — **`text/csv`, not JSON**, returned as a string. Parsing it as JSON
1614
+ * (as an earlier version of this SDK did) silently yields `undefined`.
1615
+ */
1616
+ download(id: string): Promise<string>;
1617
+ }
1618
+ /** `keys:manage` — what this key may do, and what it has spent. */
1619
+ declare class AccountResource {
1620
+ private readonly http;
1621
+ constructor(http: HttpTransport);
1622
+ entitlements(options?: {
1623
+ period?: string;
1624
+ }): Promise<EntitlementResponse>;
1625
+ /** Not provenance-enveloped — `usage-service.ts` returns this shape directly. */
1626
+ usage(options?: UsageOptions): Promise<DeveloperUsage>;
1627
+ }
1628
+
1629
+ /** Minimal WebSocket surface, so the SDK works on browser `WebSocket` and Node's `ws` alike. */
1630
+ interface RealtimeSocket {
1631
+ send(data: string): void;
1632
+ close(code?: number, reason?: string): void;
1633
+ onopen: ((event: unknown) => void) | null;
1634
+ onmessage: ((event: {
1635
+ data: unknown;
1636
+ }) => void) | null;
1637
+ onclose: ((event: {
1638
+ code?: number;
1639
+ reason?: string;
1640
+ }) => void) | null;
1641
+ onerror: ((event: unknown) => void) | null;
1642
+ }
1643
+ /**
1644
+ * Opens the WebSocket. The token MUST travel in a header or the `Sec-WebSocket-Protocol`
1645
+ * subprotocol — never the query string, where it would leak into access logs and browser history.
1646
+ * The default implementation uses the subprotocol form, which is the only one a browser
1647
+ * `WebSocket` can set.
1648
+ */
1649
+ type RealtimeSocketFactory = (url: string, token: string) => RealtimeSocket;
1650
+ interface RealtimeHandlers {
1651
+ onEvent?: (frame: RealtimeEventFrame) => void;
1652
+ onStatus?: (frame: RealtimeStatusFrame) => void;
1653
+ onError?: (error: RealtimeErrorFrame | Error) => void;
1654
+ /** Fired after a reconnect once every subscription has been re-sent. */
1655
+ onReconnect?: (attempt: number) => void;
1656
+ }
1657
+ interface RealtimeStreamOptions {
1658
+ /** Cap on reconnect attempts. `Infinity` (default) keeps trying forever with a bounded delay. */
1659
+ maxReconnectAttempts?: number;
1660
+ baseReconnectDelayMs?: number;
1661
+ maxReconnectDelayMs?: number;
1662
+ socketFactory?: RealtimeSocketFactory;
1663
+ /** Test seams. */
1664
+ setTimeoutImpl?: (handler: () => void, ms: number) => unknown;
1665
+ clearTimeoutImpl?: (handle: unknown) => void;
1666
+ random?: () => number;
1667
+ }
1668
+ interface TokenMinter {
1669
+ (): Promise<{
1670
+ token: string;
1671
+ wsUrl: string;
1672
+ }>;
1673
+ }
1674
+ /**
1675
+ * A live subscription set over one WebSocket.
1676
+ *
1677
+ * Owns three things the caller should not have to: token lifetime (re-minted per connection, so an
1678
+ * expiry mid-stream costs a reconnect rather than a dead socket), reconnection with exponential
1679
+ * backoff + jitter (a fleet reconnecting in lockstep after a gateway restart is a self-inflicted
1680
+ * thundering herd), and subscription resume — the gateway keeps no per-connection memory, so the
1681
+ * new socket must re-subscribe or the caller silently receives nothing.
1682
+ */
1683
+ declare class RealtimeStream {
1684
+ private readonly mintToken;
1685
+ private readonly handlers;
1686
+ private socket;
1687
+ private readonly creators;
1688
+ private closed;
1689
+ private attempt;
1690
+ /** True only between `onopen` and the socket going away — `send()` before this throws in real runtimes. */
1691
+ private opened;
1692
+ private reconnectTimer;
1693
+ /**
1694
+ * Bumped by `close()` and by each `open()`. An `open()` that started before the bump finds its
1695
+ * generation stale and discards the socket it just created — otherwise `close()` racing an
1696
+ * in-flight `open()` leaves a live, unreferenced socket behind (still billed, never closed).
1697
+ */
1698
+ private generation;
1699
+ private readonly options;
1700
+ constructor(mintToken: TokenMinter, handlers: RealtimeHandlers, options?: RealtimeStreamOptions);
1701
+ /** Creators currently subscribed (survives reconnects). */
1702
+ get subscriptions(): string[];
1703
+ /** True only once the socket is OPEN and accepting frames — not merely constructed. */
1704
+ get isConnected(): boolean;
1705
+ connect(creatorIds?: string[]): Promise<void>;
1706
+ subscribe(creatorId: string): void;
1707
+ unsubscribe(creatorId: string): void;
1708
+ /** Stop for good. Cancels reconnection — this is not a transient drop. */
1709
+ close(): void;
1710
+ private send;
1711
+ private open;
1712
+ private scheduleReconnect;
1713
+ }
1714
+ /** Mirrors the gateway's own normalisation so the SDK and server agree on subscription identity. */
1715
+ declare function normalizeCreatorId(value: string): string;
1716
+
1717
+ /**
1718
+ * Typed client for the TokTik Developer API.
1719
+ *
1720
+ * ```ts
1721
+ * const client = new TokTikClient({ apiKey: process.env.TOKTIK_API_KEY! });
1722
+ * const board = await client.rankings.official({ board: "hourly", region: "VN" });
1723
+ * const stream = await client.live.stream(["@creator"], { onEvent: (e) => console.log(e.event) });
1724
+ * ```
1725
+ *
1726
+ * Most data methods return the provenance envelope **unchanged** — `data` alongside the `provenance`
1727
+ * block describing how fresh the answer is and how it was obtained. The SDK never strips it, because
1728
+ * for observed (rather than officially published) data that context is part of the answer.
1729
+ * `exports.*` and `account.usage()` are the documented exceptions: the server does not envelope them.
1730
+ */
1731
+ declare class TokTikClient {
1732
+ private readonly http;
1733
+ readonly live: LiveResource & {
1734
+ /** Open a realtime stream. Mints its own handshake token and keeps it fresh across reconnects. */
1735
+ stream: (creatorIds: string[], handlers: RealtimeHandlers, options?: RealtimeStreamOptions) => Promise<RealtimeStream>;
1736
+ };
1737
+ readonly rankings: RankingsResource;
1738
+ readonly creators: CreatorsResource;
1739
+ readonly content: ContentResource;
1740
+ readonly gifters: GiftersResource;
1741
+ readonly trends: TrendsResource;
1742
+ readonly exports: ExportsResource;
1743
+ readonly account: AccountResource;
1744
+ constructor(options: TokTikClientOptions);
1745
+ /** @deprecated Use `client.live.listSessions()`. */
1746
+ listLiveSessions(options?: LiveSessionsOptions): Promise<ProvenanceEnvelope<LiveSessionListResponse>>;
1747
+ /** @deprecated Use `client.live.getSession()`. */
1748
+ getLiveSession(id: string): Promise<ProvenanceEnvelope<LiveSessionRecord>>;
1749
+ /** @deprecated Use `client.live.creatorPerformance()`. */
1750
+ listCreatorPerformance(creatorUniqueId: string, options?: CursorPageOptions): Promise<ProvenanceEnvelope<LiveCreatorPerformanceListResponse>>;
1751
+ }
1752
+
1753
+ export { AccountResource, ContentResource, CreatorsResource, ExportsResource, GiftersResource, HttpTransport, LiveResource, RankingsResource, RealtimeStream, TokTikApiError, TokTikClient, TrendsResource, buildQuery, normalizeCreatorId };
1754
+ export type { CommentOptions, CreatorChangesOptions, CreatorListOptions, CursorPageOptions, DeveloperUsage, ExportCreateInput, GamingGamesOptions, GifterListOptions, GifterWindowOptions, LivePlaybackResponse, LiveSessionsOptions, OfficialRankingOptions, RankMoversOptions, RankingHistoryOptions, RealtimeHandlers, RealtimeSocket, RealtimeSocketFactory, RealtimeStreamOptions, TokTikClientOptions, TokTikFetch, TokTikResponse, TrendOptions, UsageOptions };