Transport WebSocket
Recevez les événements sur une connexion WebSocket, sans serveur public : idéal pour les robots de clavardage et les applications de bureau.
Vue d’ensemble
Le transport WebSocket livre les événements EventSub sur une connexion que votre programme ouvre lui-même. Aucune URL publique n’est nécessaire : il convient aux robots de clavardage, aux applications de bureau et aux outils qui tournent sur le poste d’un utilisateur.
| WebSocket | Webhook | |
|---|---|---|
| Jeton pour créer les abonnements | Jeton d’utilisateur | Jeton d’application |
| Signature des messages | Aucune (connexion TLS) | HMAC-SHA256 |
| Nouvelles tentatives | Aucune : les événements survenus pendant une déconnexion sont perdus | Jusqu’à 5 reprises |
| Durée de vie des abonnements | Celle de la connexion | Jusqu’à leur suppression |
Les notions communes (types, conditions, autorisations) sont décrites dans EventSub.
Se connecter
Ouvrez une connexion WebSocket vers :
wss://api.woupi.buzz/eventsub- La connexion elle-même ne demande aucune authentification : le jeton sert au moment de créer les abonnements par l’API REST.
- N’envoyez jamais de message sur la connexion : elle est à sens unique. Tout message reçu du client ferme la connexion avec le code 4001.
Message d’accueil
Dès la connexion, le serveur envoie un message session_welcome. Conservez payload.session.id : c’est l’identifiant de session à fournir lors de la création des abonnements.
{
"metadata": {
"message_id": "0e5b8c3d-7f2a-4c61-9d8e-1a4b6c3f5e72",
"message_type": "session_welcome",
"message_timestamp": "2026-09-24T14:10:00.114Z"
},
"payload": {
"session": {
"id": "9b4e7c2a-51f3-4d8e-a0c6-3e7f2b1d9a54",
"status": "connected",
"connected_at": "2026-09-24T14:10:00.112Z",
"keepalive_timeout_seconds": 10,
"reconnect_url": null
}
}
}| Champ | Description |
|---|---|
session.id | Identifiant de la session, à utiliser dans transport.session_id. |
session.status | Toujours connected dans ce message. |
session.keepalive_timeout_seconds | Délai maximal sans message avant que le serveur envoie un session_keepalive (10 secondes). |
session.reconnect_url | Toujours null pour le moment : reconnectez-vous à l’URL de base. |
S’abonner dans les 10 secondes
Créez au moins un abonnement dans les 10 secondes qui suivent le message d’accueil, sinon le serveur ferme la connexion avec le code 4003. L’abonnement se crée par l’API REST, avec un jeton d’utilisateur et le transport { "method": "websocket", "session_id": "..." } :
curl -X POST https://api.woupi.buzz/v1/eventsub/subscriptions \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Client-Id: $CLIENT_ID" \
-H "Content-Type: application/json" \
-d '{
"type": "channel.chat.message",
"version": "1",
"condition": { "broadcaster_user_id": "1042", "user_id": "2177" },
"transport": {
"method": "websocket",
"session_id": "9b4e7c2a-51f3-4d8e-a0c6-3e7f2b1d9a54"
}
}'La réponse 202 contient l’abonnement, actif immédiatement :
{
"data": [
{
"id": "c7a1f3e9-2d4b-4e8a-b6c0-5f1d9e3a7b24",
"status": "enabled",
"type": "channel.chat.message",
"version": "1",
"condition": { "broadcaster_user_id": "1042", "user_id": "2177" },
"transport": {
"method": "websocket",
"session_id": "9b4e7c2a-51f3-4d8e-a0c6-3e7f2b1d9a54"
},
"created_at": "2026-09-24T14:10:01.530Z"
}
],
"total": 1,
"max_total_cost": 10000
}- Un jeton d’utilisateur est obligatoire; un jeton d’application est refusé.
- La première souscription lie la session à votre application et à l’utilisateur du jeton. Les abonnements suivants sur la même session doivent venir de ce même couple, sinon l’API répond 400
session belongs to another client. - Pour les types qui exigent une portée, l’utilisateur nommé dans la condition doit être celui du jeton. Par exemple,
channel.chat.messageexigeuser_idégal à l’utilisateur du jeton, qui doit avoir accordéchat:read.
Messages reçus
Chaque message est un objet JSON avec metadata et payload. Le champ metadata.message_type indique sa nature :
message_type | Description |
|---|---|
session_welcome | Premier message, avec l’identifiant de session. |
session_keepalive | Envoyé après 10 secondes sans autre message, pour signaler que la connexion est vivante. |
notification | Un événement. payload contient subscription et event, comme le corps d’un webhook. |
Message de maintien de connexion :
{
"metadata": {
"message_id": "5a2d9f1c-8e3b-4a7d-b0c4-6f1e8d2a9c53",
"message_type": "session_keepalive",
"message_timestamp": "2026-09-24T14:10:11.118Z"
},
"payload": {}
}Notification :
{
"metadata": {
"message_id": "b3e8d1a6-4c2f-4b9e-8a7d-2f5c1e9b6d30",
"message_type": "notification",
"message_timestamp": "2026-09-24T14:10:24.907Z",
"subscription_type": "channel.chat.message",
"subscription_version": "1"
},
"payload": {
"subscription": {
"id": "c7a1f3e9-2d4b-4e8a-b6c0-5f1d9e3a7b24",
"type": "channel.chat.message",
"version": "1",
"status": "enabled",
"condition": { "broadcaster_user_id": "1042", "user_id": "2177" },
"transport": { "method": "websocket" },
"created_at": "2026-09-24T14:10:01.530Z"
},
"event": {
"broadcaster_user_id": "1042",
"broadcaster_user_login": "lunaplays",
"broadcaster_user_name": "LunaPlays",
"chatter_user_id": "3318",
"chatter_user_login": "max_quebec",
"chatter_user_name": "Max",
"message_id": "1790266224905-x7k2qa",
"message": { "text": "Salut tout le monde!" },
"chatter_level": 7
}
}
}Les champs metadata.subscription_type et metadata.subscription_version permettent d’aiguiller le message sans lire payload. Le contenu de event pour chaque type est décrit dans EventSub.
Détecter une connexion perdue
Le serveur ne reste jamais plus de keepalive_timeout_seconds secondes sans rien envoyer. Si vous ne recevez aucun message pendant ce délai plus une marge (par exemple 5 secondes), considérez la connexion comme perdue, même si le système ne l’a pas encore signalé.
- Réarmez une minuterie à chaque message reçu, quel que soit son type.
- Quand la minuterie expire, fermez la connexion et ouvrez-en une nouvelle.
- À chaque nouvelle connexion, recréez tous vos abonnements avec le nouvel identifiant de session : ceux de l’ancienne session ont été supprimés.
- Espacez les tentatives de reconnexion (quelques secondes, puis davantage si les échecs se répètent).
Attention
Les événements survenus pendant une déconnexion ne sont pas renvoyés. Si vous ne pouvez rien manquer, utilisez un webhook ou relisez l’état par l’API REST après la reconnexion.
Codes de fermeture
| Code | Raison | Signification |
|---|---|---|
| 1000 | — | Fermeture normale. Reconnectez-vous, sauf si vous avez fermé la connexion vous-même. |
| 1006 | — | Fermeture anormale signalée par votre bibliothèque : connexion réseau perdue, sans trame de fermeture. Reconnectez-vous. |
| 4001 | client sent inbound traffic | Votre client a envoyé un message sur la connexion. N’envoyez rien. |
| 4003 | connection unused | Aucun abonnement n’a été créé dans les 10 secondes suivant le message d’accueil. |
À la fermeture, quelle qu’en soit la cause, le serveur supprime la session et tous ses abonnements.
Limites
| Limite | Valeur | Au-delà |
|---|---|---|
| Abonnements par session | 100 | La création est refusée. |
| Sessions par couple application et utilisateur | 3 | 429 too many websocket sessions |
| Délai pour le premier abonnement | 10 secondes | Fermeture 4003 |
| Messages envoyés par le client | 0 | Fermeture 4001 |
| Appels à l’API REST | 800 par 60 secondes | 429, voir Limites et erreurs |
Une session appartient au couple application et utilisateur de son premier abonnement. Pour suivre plus de 100 chaînes, ouvrez une deuxième ou une troisième connexion.
Exemple complet en Node.js
Ce programme se connecte, crée un abonnement channel.chat.message dès l’accueil, affiche chaque notification, détecte les connexions perdues et se reconnecte.
// npm install ws (Node.js 18+ for the global fetch)
import WebSocket from 'ws';
const CLIENT_ID = process.env.WOUPI_CLIENT_ID;
const USER_TOKEN = process.env.WOUPI_USER_TOKEN; // user access token with chat:read
const BROADCASTER_ID = process.env.WOUPI_BROADCASTER_ID; // channel to read
const USER_ID = process.env.WOUPI_USER_ID; // id of the token's user
const MARGIN_MS = 5000;
function connect() {
const ws = new WebSocket('wss://api.woupi.buzz/eventsub');
let timeoutMs = 10000 + MARGIN_MS;
let watchdog = null;
// No message for keepalive_timeout_seconds + margin: the connection is dead.
const arm = () => {
clearTimeout(watchdog);
watchdog = setTimeout(() => {
console.warn('no message received, reconnecting');
ws.terminate();
}, timeoutMs);
};
ws.on('open', arm);
ws.on('message', async (raw) => {
arm();
const { metadata, payload } = JSON.parse(raw.toString());
switch (metadata.message_type) {
case 'session_welcome': {
const { session } = payload;
timeoutMs = session.keepalive_timeout_seconds * 1000 + MARGIN_MS;
arm();
await subscribe(session.id); // must happen within 10 seconds
break;
}
case 'session_keepalive':
break; // nothing to do: the watchdog was re-armed
case 'notification':
console.log(metadata.subscription_type, payload.event);
break;
}
});
ws.on('close', (code, reason) => {
clearTimeout(watchdog);
console.log(`closed: ${code} ${reason}`);
// The server deleted this session's subscriptions: start over.
setTimeout(connect, 5000);
});
ws.on('error', (err) => console.error(err.message));
}
async function subscribe(sessionId) {
const res = await fetch('https://api.woupi.buzz/v1/eventsub/subscriptions', {
method: 'POST',
headers: {
Authorization: `Bearer ${USER_TOKEN}`,
'Client-Id': CLIENT_ID,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'channel.chat.message',
version: '1',
condition: { broadcaster_user_id: BROADCASTER_ID, user_id: USER_ID },
transport: { method: 'websocket', session_id: sessionId },
}),
});
if (res.status !== 202) {
console.error('subscription failed', res.status, await res.text());
}
}
connect();Pour un robot qui répond dans le clavardage, consultez Robots de clavardage.
