@phxgg/kick.js 0.1.1 → 0.1.2

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.
Files changed (2) hide show
  1. package/README.md +444 -448
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,448 +1,444 @@
1
- # @phxgg/kick.js
2
-
3
- A JavaScript/TypeScript client for the [Kick.com public API](https://docs.kick.com).
4
-
5
- > [!WARNING]
6
- > This project is in early development. The API may change significantly between releases.
7
-
8
- ## Installation
9
-
10
- ```bash
11
- npm install @phxgg/kick.js
12
- ```
13
-
14
- ## Table of contents
15
-
16
- - [Setup](#setup)
17
- - [OAuth](#oauth)
18
- - [Users](#users)
19
- - [Channels](#channels)
20
- - [Livestreams](#livestreams)
21
- - [Categories](#categories)
22
- - [Chat](#chat)
23
- - [Channel Rewards](#channel-rewards)
24
- - [Moderation](#moderation)
25
- - [KICKs](#kicks)
26
- - [Event Subscriptions](#event-subscriptions)
27
- - [Webhooks](#webhooks)
28
- - [Error handling](#error-handling)
29
-
30
- ---
31
-
32
- ## Setup
33
-
34
- ```ts
35
- import { KickClient } from '@phxgg/kick.js';
36
-
37
- const client = new KickClient({
38
- clientId: 'YOUR_CLIENT_ID',
39
- clientSecret: 'YOUR_CLIENT_SECRET',
40
- redirectUri: 'https://yourapp.com/oauth/callback', // required for OAuth flows
41
- });
42
- ```
43
-
44
- Once you have a user token (obtained via [OAuth](#oauth)), attach it to the client:
45
-
46
- ```ts
47
- client.setToken({
48
- access_token: 'USER_ACCESS_TOKEN',
49
- refresh_token: 'USER_REFRESH_TOKEN',
50
- token_type: 'bearer',
51
- expires_in: 3600,
52
- scope: 'user:read channel:read',
53
- });
54
- ```
55
-
56
- After `setToken` is called, the client automatically fetches the authenticated user in the background and registers itself in the global event manager so that [webhook events](#webhooks) can be routed to it.
57
-
58
- ---
59
-
60
- ## OAuth
61
-
62
- ### Generate authorization URL
63
-
64
- Redirect your user to this URL to begin the authorization flow. Store the `codeVerifier` in the session — you'll need it in the next step.
65
-
66
- ```ts
67
- const { url, codeVerifier } = await client.oauth.generateAuthorizeURL();
68
- // redirect user to `url`
69
- ```
70
-
71
- ### Exchange code for token
72
-
73
- ```ts
74
- const token = await client.oauth.exchangeToken(code, codeVerifier);
75
- client.setToken(token);
76
- ```
77
-
78
- ### Generate an app token (client credentials)
79
-
80
- Use this for API calls that don't require a user context.
81
-
82
- ```ts
83
- const appToken = await client.oauth.generateAppToken();
84
- client.appToken = appToken;
85
- ```
86
-
87
- ### Refresh a token
88
-
89
- ```ts
90
- const refreshed = await client.oauth.refreshToken(token.refresh_token);
91
- client.setToken(refreshed);
92
- ```
93
-
94
- ### Revoke a token
95
-
96
- ```ts
97
- import { TokenHintType } from '@phxgg/kick.js';
98
-
99
- await client.oauth.revokeToken(token.access_token, TokenHintType.ACCESS_TOKEN);
100
- ```
101
-
102
- ### Introspect a token
103
-
104
- ```ts
105
- const info = await client.oauth.introspect();
106
- console.log(info.active, info.scope);
107
- ```
108
-
109
- ---
110
-
111
- ## Users
112
-
113
- Required scope: `user:read`
114
-
115
- ```ts
116
- // Fetch the authenticated user
117
- const me = await client.users.me();
118
- console.log(me.name, me.email, me.userId);
119
-
120
- // Fetch specific users by ID
121
- const users = await client.users.fetch([123, 456]);
122
- ```
123
-
124
- ---
125
-
126
- ## Channels
127
-
128
- Required scope: `channel:read` (read), `channel:write` (update)
129
-
130
- ```ts
131
- // Fetch by slug
132
- const channel = await client.channels.fetchBySlug('monstercat');
133
-
134
- // Fetch by broadcaster user ID
135
- const channel = await client.channels.fetchById(123);
136
-
137
- // Fetch multiple at once
138
- const channels = await client.channels.fetch({ slug: ['monstercat', 'kick'] });
139
- // or: client.channels.fetch({ broadcasterUserId: [123, 456] })
140
-
141
- // Update the authenticated user's channel
142
- await client.channels.update({
143
- streamTitle: 'My new stream title',
144
- categoryId: 15,
145
- customTags: ['gaming', 'chill'],
146
- });
147
- ```
148
-
149
- ---
150
-
151
- ## Livestreams
152
-
153
- ```ts
154
- // Fetch live streams (no scope required)
155
- const streams = await client.livestreams.fetch({
156
- broadcasterUserId: [123, 456],
157
- categoryId: 15,
158
- language: 'en',
159
- limit: 20,
160
- sort: 'viewer_count', // or 'started_at'
161
- });
162
-
163
- // Total live stream count
164
- const stats = await client.livestreams.fetchStats();
165
- console.log(stats.total_count);
166
- ```
167
-
168
- ---
169
-
170
- ## Categories
171
-
172
- ```ts
173
- // v1: fetch all categories
174
- const categories = await client.categories.fetch();
175
-
176
- // v2: search with pagination
177
- const results = await client.categoriesV2.search({ query: 'gaming', limit: 10 });
178
-
179
- // v2: fetch a single category by ID
180
- const category = await client.categoriesV2.fetch(15);
181
- ```
182
-
183
- ---
184
-
185
- ## Chat
186
-
187
- Required scope: `chat:write` (send), `moderation:chat_message:manage` (delete)
188
-
189
- ```ts
190
- import { ChatMessageType } from '@phxgg/kick.js';
191
-
192
- // Send a bot message (default)
193
- const message = await client.chat.send({
194
- content: 'Hello from kick.js!',
195
- });
196
-
197
- // Send a message as the authenticated user to a specific channel
198
- const message = await client.chat.send({
199
- content: 'Hello!',
200
- broadcasterUserId: 123,
201
- type: ChatMessageType.USER,
202
- });
203
-
204
- // Reply to a message
205
- const reply = await client.chat.send({
206
- content: 'Nice catch!',
207
- replyToMessageId: 'some-message-id',
208
- });
209
-
210
- // Delete a message
211
- await client.chat.delete('some-message-id');
212
- ```
213
-
214
- ---
215
-
216
- ## Channel Rewards
217
-
218
- Required scope: `channel:rewards:read` (read), `channel:rewards:write` (create / update / delete)
219
-
220
- ```ts
221
- // Fetch all rewards for the authenticated broadcaster
222
- const rewards = await client.channelRewards.fetch();
223
-
224
- // Create a reward
225
- const reward = await client.channelRewards.create({
226
- title: 'Hydrate!',
227
- cost: 500,
228
- description: 'Make the streamer drink water.',
229
- isEnabled: true,
230
- isUserInputRequired: false,
231
- });
232
-
233
- // Update a reward
234
- await client.channelRewards.update(reward.id, {
235
- cost: 1000,
236
- isPaused: false,
237
- });
238
-
239
- // Delete a reward
240
- await client.channelRewards.delete(reward.id);
241
-
242
- // Fetch redemptions (defaults to pending)
243
- const redemptions = await client.channelRewards.getRedemptions({
244
- rewardId: reward.id,
245
- status: 'pending',
246
- });
247
-
248
- // Accept / reject redemptions (up to 25 per call)
249
- await client.channelRewards.acceptRedemptions({ ids: [redemptions[0].id] });
250
- await client.channelRewards.rejectRedemptions({ ids: [redemptions[1].id] });
251
- ```
252
-
253
- ---
254
-
255
- ## Moderation
256
-
257
- Required scope: `moderation:ban`
258
-
259
- ```ts
260
- // Ban a user permanently
261
- await client.moderation.banUser({
262
- broadcasterUserId: 123,
263
- userId: 456,
264
- reason: 'Spamming',
265
- });
266
-
267
- // Timeout a user (duration in minutes, max 10080 = 7 days)
268
- await client.moderation.timeoutUser({
269
- broadcasterUserId: 123,
270
- userId: 456,
271
- duration: 10,
272
- reason: 'Cool off.',
273
- });
274
-
275
- // Remove a ban or timeout
276
- await client.moderation.removeBan({
277
- broadcasterUserId: 123,
278
- userId: 456,
279
- });
280
- ```
281
-
282
- ---
283
-
284
- ## KICKs
285
-
286
- Required scope: `kicks:read`
287
-
288
- ```ts
289
- // Fetch the KICKs leaderboard for the authenticated broadcaster
290
- const leaderboard = await client.kicks.fetchLeaderboard({ top: 10 });
291
- ```
292
-
293
- ---
294
-
295
- ## Event subscriptions
296
-
297
- Required scope: `events:subscribe`
298
-
299
- Subscribe your app to receive webhook events for a broadcaster.
300
-
301
- ```ts
302
- import { WebhookEvents } from '@phxgg/kick.js';
303
-
304
- // Subscribe to a single event
305
- await client.events.subscribe({
306
- broadcasterUserId: 123,
307
- event: { name: WebhookEvents.CHAT_MESSAGE_SENT, version: 1 },
308
- });
309
-
310
- // Subscribe to multiple events at once
311
- await client.events.subscribeMultiple({
312
- broadcasterUserId: 123,
313
- events: [
314
- { name: WebhookEvents.CHANNEL_FOLLOWED, version: 1 },
315
- { name: WebhookEvents.LIVESTREAM_STATUS_UPDATED, version: 1 },
316
- ],
317
- });
318
-
319
- // List active subscriptions
320
- const subs = await client.events.fetch();
321
-
322
- // Unsubscribe
323
- await client.events.unsubscribe(subs[0].id);
324
- await client.events.unsubscribeMultiple(subs.map((s) => s.id));
325
- ```
326
-
327
- ---
328
-
329
- ## Webhooks
330
-
331
- kick.js provides framework-agnostic primitives so you can handle Kick webhook deliveries in any HTTP server.
332
-
333
- ### Verify & dispatch
334
-
335
- ```ts
336
- import {
337
- verifyKickSignature,
338
- dispatchWebhookEvent,
339
- getKickPublicKey,
340
- } from '@phxgg/kick.js';
341
-
342
- // Inside your POST /webhooks/kick handler:
343
- const publicKey = await getKickPublicKey(); // cached, refreshes every hour
344
-
345
- const valid = verifyKickSignature({
346
- messageId: req.headers['kick-event-message-id'],
347
- messageTimestamp: req.headers['kick-event-message-timestamp'],
348
- rawBody: rawBody, // Buffer or string — must be read before JSON.parse
349
- signature: req.headers['kick-event-signature'],
350
- publicKey,
351
- });
352
-
353
- if (!valid) return res.sendStatus(403);
354
-
355
- const eventType = req.headers['kick-event-type'];
356
- const payload = JSON.parse(rawBody);
357
-
358
- // Route to whichever KickClient is registered for this broadcaster
359
- dispatchWebhookEvent(eventType, payload);
360
-
361
- res.sendStatus(200);
362
- ```
363
-
364
- ### Per-client listeners
365
-
366
- After calling `client.setToken()`, the client registers itself so that `dispatchWebhookEvent` can route events to the correct instance. Use `client.on()` to react to events:
367
-
368
- ```ts
369
- import { WebhookEvents } from '@phxgg/kick.js';
370
-
371
- client.on(WebhookEvents.CHAT_MESSAGE_SENT, (payload) => {
372
- console.log(`${payload.sender.username}: ${payload.content}`);
373
- });
374
-
375
- client.on(WebhookEvents.CHANNEL_FOLLOWED, (payload) => {
376
- console.log(`${payload.follower.username} followed the channel!`);
377
- });
378
-
379
- client.on(WebhookEvents.LIVESTREAM_STATUS_UPDATED, (payload) => {
380
- console.log('Stream is now', payload.is_live ? 'live' : 'offline');
381
- });
382
-
383
- // Remove a listener
384
- client.off(WebhookEvents.CHAT_MESSAGE_SENT, myListener);
385
-
386
- // Remove all listeners
387
- client.removeAllListeners();
388
-
389
- // Clean up the client and deregister it from the event manager
390
- client.destroy();
391
- ```
392
-
393
- **Supported webhook events**
394
-
395
- | Event | Constant |
396
- |---|---|
397
- | `chat.message.sent` | `WebhookEvents.CHAT_MESSAGE_SENT` |
398
- | `channel.followed` | `WebhookEvents.CHANNEL_FOLLOWED` |
399
- | `channel.subscription.new` | `WebhookEvents.CHANNEL_SUBSCRIPTION_NEW` |
400
- | `channel.subscription.renewal` | `WebhookEvents.CHANNEL_SUBSCRIPTION_RENEWAL` |
401
- | `channel.subscription.gifts` | `WebhookEvents.CHANNEL_SUBSCRIPTION_GIFTS` |
402
- | `channel.reward.redemption.updated` | `WebhookEvents.CHANNEL_REWARD_REDEMPTION_UPDATED` |
403
- | `livestream.status.updated` | `WebhookEvents.LIVESTREAM_STATUS_UPDATED` |
404
- | `livestream.metadata.updated` | `WebhookEvents.LIVESTREAM_METADATA_UPDATED` |
405
- | `moderation.banned` | `WebhookEvents.MODERATION_BANNED` |
406
- | `kicks.gifted` | `WebhookEvents.KICKS_GIFTED` |
407
-
408
- ---
409
-
410
- ## Error handling
411
-
412
- All methods throw typed errors on non-2xx responses:
413
-
414
- ```ts
415
- import {
416
- UnauthorizedError,
417
- ForbiddenError,
418
- NotFoundError,
419
- RateLimitError,
420
- BadRequestError,
421
- MissingScopeError,
422
- NoTokenSetError,
423
- } from '@phxgg/kick.js';
424
-
425
- try {
426
- const me = await client.users.me();
427
- } catch (err) {
428
- if (err instanceof UnauthorizedError) {
429
- // token expired — refresh and retry
430
- } else if (err instanceof MissingScopeError) {
431
- // the token is missing a required scope
432
- } else if (err instanceof RateLimitError) {
433
- // back off and retry
434
- } else {
435
- throw err;
436
- }
437
- }
438
- ```
439
-
440
- ---
441
-
442
- ## Example app
443
-
444
- A full Express + MongoDB reference implementation is available in [`examples/express-app`](https://github.com/phxgg/kick.js/tree/main/examples/express-app).
445
-
446
- ## License
447
-
448
- [MIT](./LICENSE)
1
+ # @phxgg/kick.js
2
+
3
+ A JavaScript/TypeScript client for the [Kick.com public API](https://docs.kick.com).
4
+
5
+ > [!WARNING]
6
+ > This project is in early development. The API may change significantly between releases.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @phxgg/kick.js
12
+ ```
13
+
14
+ ## Table of contents
15
+
16
+ - [Setup](#setup)
17
+ - [OAuth](#oauth)
18
+ - [Users](#users)
19
+ - [Channels](#channels)
20
+ - [Livestreams](#livestreams)
21
+ - [Categories](#categories)
22
+ - [Chat](#chat)
23
+ - [Channel Rewards](#channel-rewards)
24
+ - [Moderation](#moderation)
25
+ - [KICKs](#kicks)
26
+ - [Event Subscriptions](#event-subscriptions)
27
+ - [Webhooks](#webhooks)
28
+ - [Error handling](#error-handling)
29
+
30
+ ---
31
+
32
+ ## Setup
33
+
34
+ ```ts
35
+ import { KickClient } from '@phxgg/kick.js';
36
+
37
+ const client = new KickClient({
38
+ clientId: 'YOUR_CLIENT_ID',
39
+ clientSecret: 'YOUR_CLIENT_SECRET',
40
+ redirectUri: 'https://yourapp.com/oauth/callback', // required for OAuth flows
41
+ });
42
+ ```
43
+
44
+ Once you have a user token (obtained via [OAuth](#oauth)), attach it to the client:
45
+
46
+ ```ts
47
+ client.setToken({
48
+ access_token: 'USER_ACCESS_TOKEN',
49
+ refresh_token: 'USER_REFRESH_TOKEN',
50
+ token_type: 'bearer',
51
+ expires_in: 3600,
52
+ scope: 'user:read channel:read',
53
+ });
54
+ ```
55
+
56
+ After `setToken` is called, the client automatically fetches the authenticated user in the background and registers itself in the global event manager so that [webhook events](#webhooks) can be routed to it.
57
+
58
+ ---
59
+
60
+ ## OAuth
61
+
62
+ ### Generate authorization URL
63
+
64
+ Redirect your user to this URL to begin the authorization flow. Store the `codeVerifier` in the session — you'll need it in the next step.
65
+
66
+ ```ts
67
+ const { url, codeVerifier } = await client.oauth.generateAuthorizeURL();
68
+ // redirect user to `url`
69
+ ```
70
+
71
+ ### Exchange code for token
72
+
73
+ ```ts
74
+ const token = await client.oauth.exchangeToken(code, codeVerifier);
75
+ client.setToken(token);
76
+ ```
77
+
78
+ ### Generate an app token (client credentials)
79
+
80
+ Use this for API calls that don't require a user context.
81
+
82
+ ```ts
83
+ const appToken = await client.oauth.generateAppToken();
84
+ client.appToken = appToken;
85
+ ```
86
+
87
+ ### Refresh a token
88
+
89
+ ```ts
90
+ const refreshed = await client.oauth.refreshToken(token.refresh_token);
91
+ client.setToken(refreshed);
92
+ ```
93
+
94
+ ### Revoke a token
95
+
96
+ ```ts
97
+ import { TokenHintType } from '@phxgg/kick.js';
98
+
99
+ await client.oauth.revokeToken(token.access_token, TokenHintType.ACCESS_TOKEN);
100
+ ```
101
+
102
+ ### Introspect a token
103
+
104
+ ```ts
105
+ const info = await client.oauth.introspect();
106
+ console.log(info.active, info.scope);
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Users
112
+
113
+ Required scope: `user:read`
114
+
115
+ ```ts
116
+ // Fetch the authenticated user
117
+ const me = await client.users.me();
118
+ console.log(me.name, me.email, me.userId);
119
+
120
+ // Fetch specific users by ID
121
+ const users = await client.users.fetch([123, 456]);
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Channels
127
+
128
+ Required scope: `channel:read` (read), `channel:write` (update)
129
+
130
+ ```ts
131
+ // Fetch by slug
132
+ const channel = await client.channels.fetchBySlug('monstercat');
133
+
134
+ // Fetch by broadcaster user ID
135
+ const channel = await client.channels.fetchById(123);
136
+
137
+ // Fetch multiple at once
138
+ const channels = await client.channels.fetch({ slug: ['monstercat', 'kick'] });
139
+ // or: client.channels.fetch({ broadcasterUserId: [123, 456] })
140
+
141
+ // Update the authenticated user's channel
142
+ await client.channels.update({
143
+ streamTitle: 'My new stream title',
144
+ categoryId: 15,
145
+ customTags: ['gaming', 'chill'],
146
+ });
147
+ ```
148
+
149
+ ---
150
+
151
+ ## Livestreams
152
+
153
+ ```ts
154
+ // Fetch live streams (no scope required)
155
+ const streams = await client.livestreams.fetch({
156
+ broadcasterUserId: [123, 456],
157
+ categoryId: 15,
158
+ language: 'en',
159
+ limit: 20,
160
+ sort: 'viewer_count', // or 'started_at'
161
+ });
162
+
163
+ // Total live stream count
164
+ const stats = await client.livestreams.fetchStats();
165
+ console.log(stats.total_count);
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Categories
171
+
172
+ ```ts
173
+ // v1: fetch all categories
174
+ const categories = await client.categories.fetch();
175
+
176
+ // v2: search with pagination
177
+ const results = await client.categoriesV2.search({ query: 'gaming', limit: 10 });
178
+
179
+ // v2: fetch a single category by ID
180
+ const category = await client.categoriesV2.fetch(15);
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Chat
186
+
187
+ Required scope: `chat:write` (send), `moderation:chat_message:manage` (delete)
188
+
189
+ ```ts
190
+ import { ChatMessageType } from '@phxgg/kick.js';
191
+
192
+ // Send a bot message (default)
193
+ const message = await client.chat.send({
194
+ content: 'Hello from kick.js!',
195
+ });
196
+
197
+ // Send a message as the authenticated user to a specific channel
198
+ const message = await client.chat.send({
199
+ content: 'Hello!',
200
+ broadcasterUserId: 123,
201
+ type: ChatMessageType.USER,
202
+ });
203
+
204
+ // Reply to a message
205
+ const reply = await client.chat.send({
206
+ content: 'Nice catch!',
207
+ replyToMessageId: 'some-message-id',
208
+ });
209
+
210
+ // Delete a message
211
+ await client.chat.delete('some-message-id');
212
+ ```
213
+
214
+ ---
215
+
216
+ ## Channel Rewards
217
+
218
+ Required scope: `channel:rewards:read` (read), `channel:rewards:write` (create / update / delete)
219
+
220
+ ```ts
221
+ // Fetch all rewards for the authenticated broadcaster
222
+ const rewards = await client.channelRewards.fetch();
223
+
224
+ // Create a reward
225
+ const reward = await client.channelRewards.create({
226
+ title: 'Hydrate!',
227
+ cost: 500,
228
+ description: 'Make the streamer drink water.',
229
+ isEnabled: true,
230
+ isUserInputRequired: false,
231
+ });
232
+
233
+ // Update a reward
234
+ await client.channelRewards.update(reward.id, {
235
+ cost: 1000,
236
+ isPaused: false,
237
+ });
238
+
239
+ // Delete a reward
240
+ await client.channelRewards.delete(reward.id);
241
+
242
+ // Fetch redemptions (defaults to pending)
243
+ const redemptions = await client.channelRewards.getRedemptions({
244
+ rewardId: reward.id,
245
+ status: 'pending',
246
+ });
247
+
248
+ // Accept / reject redemptions (up to 25 per call)
249
+ await client.channelRewards.acceptRedemptions({ ids: [redemptions[0].id] });
250
+ await client.channelRewards.rejectRedemptions({ ids: [redemptions[1].id] });
251
+ ```
252
+
253
+ ---
254
+
255
+ ## Moderation
256
+
257
+ Required scope: `moderation:ban`
258
+
259
+ ```ts
260
+ // Ban a user permanently
261
+ await client.moderation.banUser({
262
+ broadcasterUserId: 123,
263
+ userId: 456,
264
+ reason: 'Spamming',
265
+ });
266
+
267
+ // Timeout a user (duration in minutes, max 10080 = 7 days)
268
+ await client.moderation.timeoutUser({
269
+ broadcasterUserId: 123,
270
+ userId: 456,
271
+ duration: 10,
272
+ reason: 'Cool off.',
273
+ });
274
+
275
+ // Remove a ban or timeout
276
+ await client.moderation.removeBan({
277
+ broadcasterUserId: 123,
278
+ userId: 456,
279
+ });
280
+ ```
281
+
282
+ ---
283
+
284
+ ## KICKs
285
+
286
+ Required scope: `kicks:read`
287
+
288
+ ```ts
289
+ // Fetch the KICKs leaderboard for the authenticated broadcaster
290
+ const leaderboard = await client.kicks.fetchLeaderboard({ top: 10 });
291
+ ```
292
+
293
+ ---
294
+
295
+ ## Event subscriptions
296
+
297
+ Required scope: `events:subscribe`
298
+
299
+ Subscribe your app to receive webhook events for a broadcaster.
300
+
301
+ ```ts
302
+ import { WebhookEvents } from '@phxgg/kick.js';
303
+
304
+ // Subscribe to a single event
305
+ await client.events.subscribe({
306
+ broadcasterUserId: 123,
307
+ event: { name: WebhookEvents.CHAT_MESSAGE_SENT, version: 1 },
308
+ });
309
+
310
+ // Subscribe to multiple events at once
311
+ await client.events.subscribeMultiple({
312
+ broadcasterUserId: 123,
313
+ events: [
314
+ { name: WebhookEvents.CHANNEL_FOLLOWED, version: 1 },
315
+ { name: WebhookEvents.LIVESTREAM_STATUS_UPDATED, version: 1 },
316
+ ],
317
+ });
318
+
319
+ // List active subscriptions
320
+ const subs = await client.events.fetch();
321
+
322
+ // Unsubscribe
323
+ await client.events.unsubscribe(subs[0].id);
324
+ await client.events.unsubscribeMultiple(subs.map((s) => s.id));
325
+ ```
326
+
327
+ ---
328
+
329
+ ## Webhooks
330
+
331
+ kick.js provides framework-agnostic primitives so you can handle Kick webhook deliveries in any HTTP server.
332
+
333
+ ### Verify & dispatch
334
+
335
+ ```ts
336
+ import { verifyKickSignature, dispatchWebhookEvent, getKickPublicKey } from '@phxgg/kick.js';
337
+
338
+ // Inside your POST /webhooks/kick handler:
339
+ const publicKey = await getKickPublicKey(); // cached, refreshes every hour
340
+
341
+ const valid = verifyKickSignature({
342
+ messageId: req.headers['kick-event-message-id'],
343
+ messageTimestamp: req.headers['kick-event-message-timestamp'],
344
+ rawBody: rawBody, // Buffer or string — must be read before JSON.parse
345
+ signature: req.headers['kick-event-signature'],
346
+ publicKey,
347
+ });
348
+
349
+ if (!valid) return res.sendStatus(403);
350
+
351
+ const eventType = req.headers['kick-event-type'];
352
+ const payload = JSON.parse(rawBody);
353
+
354
+ // Route to whichever KickClient is registered for this broadcaster
355
+ dispatchWebhookEvent(eventType, payload);
356
+
357
+ res.sendStatus(200);
358
+ ```
359
+
360
+ ### Per-client listeners
361
+
362
+ After calling `client.setToken()`, the client registers itself so that `dispatchWebhookEvent` can route events to the correct instance. Use `client.on()` to react to events:
363
+
364
+ ```ts
365
+ import { WebhookEvents } from '@phxgg/kick.js';
366
+
367
+ client.on(WebhookEvents.CHAT_MESSAGE_SENT, (payload) => {
368
+ console.log(`${payload.sender.username}: ${payload.content}`);
369
+ });
370
+
371
+ client.on(WebhookEvents.CHANNEL_FOLLOWED, (payload) => {
372
+ console.log(`${payload.follower.username} followed the channel!`);
373
+ });
374
+
375
+ client.on(WebhookEvents.LIVESTREAM_STATUS_UPDATED, (payload) => {
376
+ console.log('Stream is now', payload.is_live ? 'live' : 'offline');
377
+ });
378
+
379
+ // Remove a listener
380
+ client.off(WebhookEvents.CHAT_MESSAGE_SENT, myListener);
381
+
382
+ // Remove all listeners
383
+ client.removeAllListeners();
384
+
385
+ // Clean up the client and deregister it from the event manager
386
+ client.destroy();
387
+ ```
388
+
389
+ **Supported webhook events**
390
+
391
+ | Event | Constant |
392
+ | ----------------------------------- | ------------------------------------------------- |
393
+ | `chat.message.sent` | `WebhookEvents.CHAT_MESSAGE_SENT` |
394
+ | `channel.followed` | `WebhookEvents.CHANNEL_FOLLOWED` |
395
+ | `channel.subscription.new` | `WebhookEvents.CHANNEL_SUBSCRIPTION_NEW` |
396
+ | `channel.subscription.renewal` | `WebhookEvents.CHANNEL_SUBSCRIPTION_RENEWAL` |
397
+ | `channel.subscription.gifts` | `WebhookEvents.CHANNEL_SUBSCRIPTION_GIFTS` |
398
+ | `channel.reward.redemption.updated` | `WebhookEvents.CHANNEL_REWARD_REDEMPTION_UPDATED` |
399
+ | `livestream.status.updated` | `WebhookEvents.LIVESTREAM_STATUS_UPDATED` |
400
+ | `livestream.metadata.updated` | `WebhookEvents.LIVESTREAM_METADATA_UPDATED` |
401
+ | `moderation.banned` | `WebhookEvents.MODERATION_BANNED` |
402
+ | `kicks.gifted` | `WebhookEvents.KICKS_GIFTED` |
403
+
404
+ ---
405
+
406
+ ## Error handling
407
+
408
+ All methods throw typed errors on non-2xx responses:
409
+
410
+ ```ts
411
+ import {
412
+ UnauthorizedError,
413
+ ForbiddenError,
414
+ NotFoundError,
415
+ RateLimitError,
416
+ BadRequestError,
417
+ MissingScopeError,
418
+ NoTokenSetError,
419
+ } from '@phxgg/kick.js';
420
+
421
+ try {
422
+ const me = await client.users.me();
423
+ } catch (err) {
424
+ if (err instanceof UnauthorizedError) {
425
+ // token expired — refresh and retry
426
+ } else if (err instanceof MissingScopeError) {
427
+ // the token is missing a required scope
428
+ } else if (err instanceof RateLimitError) {
429
+ // back off and retry
430
+ } else {
431
+ throw err;
432
+ }
433
+ }
434
+ ```
435
+
436
+ ---
437
+
438
+ ## Example app
439
+
440
+ A full Express + MongoDB reference implementation is available in [`examples/express-app`](https://github.com/phxgg/kick.js/tree/main/examples/express-app).
441
+
442
+ ## License
443
+
444
+ [MIT](./LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phxgg/kick.js",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "JavaScript/TypeScript client for the Kick.com public API.",
5
5
  "license": "MIT",
6
6
  "author": "phxgg",