Build custom overlays, integrations, and applications using the Ninja Chatter API.
Building a bot or AI integration? Read the automation guide and machine-readable API reference.
The easiest way to receive chat messages for overlays is using the read-only stream endpoints. No token is required when the room is enabled and allows public read-only streams. Do not enable public access on a private room just to connect an integration.
const ws = new WebSocket('wss://api.ninjachatter.com/stream/YOUR_ROOM_ID');
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'chat') console.log(`${msg.user.name}: ${msg.payload.text}`);
};
const events = new EventSource('https://api.ninjachatter.com/stream/YOUR_ROOM_ID/sse');
events.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'chat') console.log(`${msg.user.name}: ${msg.payload.text}`);
};
These endpoints provide read-only access to room chat messages. Perfect for building overlays, bots, and integrations.
| Endpoint | Protocol | Format | Description |
|---|---|---|---|
/stream/:room |
WS | Clean JSON | WebSocket stream with our Envelope format |
/stream/:room/sse |
GET | Clean JSON | Server-Sent Events stream |
/stream/:room/ssn |
WS | SSN Format | WebSocket stream in SSN-compatible format |
If you have existing SocialStream.ninja overlays, you can point them at our API with minimal changes.
Add the server parameter to your dock.html URL:
https://socialstream.ninja/dock.html?session=YOUR_ROOM_ID&server=wss://api.ninjachatter.com/stream/YOUR_ROOM_ID/ssn
YOUR_ROOM_ID with your actual room ID from the Dashboard.
This is a read-only display connection, not a two-way SSN bridge. The room must be enabled with read-only access allowed. The room ID in the stream URL selects the feed; no ingress key belongs in this URL. SSN’s initial join frame is ignored by the read-only endpoint. Use the SSN relay setup guide for the opposite direction.
The SSN adapter exposes a basic chat-row format; it does not preserve all SSN badges, donation fields, event metadata, or moderation/deletion commands. For a custom display requiring deletion events, consume the clean /stream/:room envelopes and handle their event types explicitly. Display adapters should render untrusted text safely.
The /stream/:room/ssn endpoint translates messages to SSN's format:
{
"id": "abc123def456",
"chatname": "Steve",
"chatmessage": "Hello world!",
"chatimg": "https://cdn.example.com/avatar.png",
"type": "twitch",
"hasDonation": null,
"membership": null,
"moderator": false,
"bot": false,
"userid": "user123",
"textonly": true
}
Used by /stream/:room and /stream/:room/sse:
{
"id": "abc123def456",
"type": "chat",
"room": "my-room-id",
"user": {
"id": "user123",
"name": "Steve",
"provider": "twitch",
"role": "viewer",
"avatar": "https://cdn.example.com/avatar.png"
},
"payload": {
"text": "Hello world!",
"source": "twitch",
"chatmessage": "<span>Hello world!</span>",
"textonly": false
},
"ts": 1702000000,
"meta": {}
}
| Field | Type | Description |
|---|---|---|
id | string | Unique message ID |
type | string | Always "chat" for stream endpoints |
room | string | Room ID |
user.id | string | User's unique ID |
user.name | string | Display name |
user.provider | string | Auth provider: discord, twitch, youtube, patreon, guest, webhook |
user.role | string | User role: viewer, publisher, mod, bot |
user.avatar | string? | Avatar URL (optional) |
payload.text | string | Plain text message content |
payload.source | string? | Original source platform (for SSN messages) |
payload.chatmessage | string? | HTML-formatted message (for SSN messages) |
payload.textonly | boolean? | If false, chatmessage contains HTML |
ts | number | Unix timestamp (seconds) |
meta | object | Additional metadata |
The SSE endpoint is a simpler alternative to WebSocket that works in more environments.
EventSource APIconst source = new EventSource('https://api.ninjachatter.com/stream/YOUR_ROOM_ID/sse');
source.onmessage = (event) => {
const msg = JSON.parse(event.data);
displayMessage(msg.user.name, msg.payload.text, msg.user.avatar);
};
source.onerror = (err) => {
console.log('Connection error, will auto-reconnect...');
};
For full chat participation (sending messages, reactions, etc.), use the authenticated WebSocket endpoint.
WS /ws/:room?token=JWT_TOKEN
Use the token endpoint to mint guest or authenticated integration tokens, or use OAuth login flows.
Load <script src="https://ninjachatter.com/embed.js"></script> before this code. Run the example inside an async function. Guest minting requires guest joins to be enabled; provider-restricted rooms require their authorized join flow. Keep ingress keys and owner credentials out of client code.
// Full-featured client with auto-reconnect
const client = ChatEmbed.createChatClient({
room: 'YOUR_ROOM_ID',
apiBase: 'https://api.ninjachatter.com',
wsBase: 'wss://api.ninjachatter.com',
onMessage: (msg) => {
if (msg.type === 'chat') console.log(`${msg.user.name}: ${msg.payload.text}`);
},
onStateChange: (state) => {
console.log('Connection state:', state);
}
});
// Mint a guest token and connect
const token = await ChatEmbed.mintGuestToken({
room: 'YOUR_ROOM_ID',
apiBase: 'https://api.ninjachatter.com',
username: 'Guest User'
});
client.connect(token);
Save this HTML as a local file, replace the room ID, then select that file in an OBS Browser Source. This minimal example displays chat only; a production integration should handle permission failures, reconnect backoff, and deletion events as needed.
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
font-family: sans-serif;
background: transparent;
}
#chat {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
}
.msg {
background: rgba(0,0,0,0.7);
color: white;
padding: 8px 12px;
border-radius: 8px;
animation: fadeIn 0.3s ease;
}
.msg img {
width: 24px;
height: 24px;
border-radius: 50%;
vertical-align: middle;
margin-right: 8px;
}
.msg .name { font-weight: bold; color: #38bdf8; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } }
</style>
</head>
<body>
<div id="chat"></div>
<script>
const ROOM_ID = 'YOUR_ROOM_ID'; // Change this!
const MAX_MESSAGES = 10;
const ws = new WebSocket(`wss://api.ninjachatter.com/stream/${ROOM_ID}`);
const chat = document.getElementById('chat');
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type !== 'chat') return;
const div = document.createElement('div');
div.className = 'msg';
// Treat sender names and message text as untrusted text.
if (msg.user.avatar) {
try {
const avatar = new URL(msg.user.avatar);
if (avatar.protocol === 'https:') {
const image = document.createElement('img');
image.src = avatar.href;
image.alt = '';
image.referrerPolicy = 'no-referrer';
div.appendChild(image);
}
} catch (_) { /* Ignore invalid avatar URLs. */ }
}
const name = document.createElement('span');
name.className = 'name';
name.textContent = `${msg.user.name}: `;
const text = document.createElement('span');
text.textContent = msg.payload.text;
div.append(name, text);
chat.appendChild(div);
// Remove old messages
while (chat.children.length > MAX_MESSAGES) {
chat.firstChild.remove();
}
};
ws.onclose = () => setTimeout(() => location.reload(), 5000);
</script>
</body>
</html>
import { useState, useEffect } from 'react';
function useChatStream(roomId) {
const [messages, setMessages] = useState([]);
const [connected, setConnected] = useState(false);
useEffect(() => {
const ws = new WebSocket(`wss://api.ninjachatter.com/stream/${roomId}`);
ws.onopen = () => setConnected(true);
ws.onclose = () => setConnected(false);
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'chat') setMessages(prev => [...prev.slice(-99), msg]);
};
return () => ws.close();
}, [roomId]);
return { messages, connected };
}
// Usage
function ChatOverlay({ roomId }) {
const { messages, connected } = useChatStream(roomId);
return (
<div>
{messages.map(msg => (
<div key={msg.id}>
<strong>{msg.user.name}:</strong> {msg.payload.text}
</div>
))}
</div>
);
}
import WebSocket from 'ws';
interface ChatMessage {
id: string;
type: 'chat' | 'system';
user: {
id: string;
name: string;
avatar?: string;
};
payload: {
text: string;
};
ts: number;
}
class NinjaChatClient {
private ws: WebSocket | null = null;
private roomId: string;
constructor(roomId: string) {
this.roomId = roomId;
}
connect(): Promise<void> {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(`wss://api.ninjachatter.com/stream/${this.roomId}`);
this.ws.on('open', () => {
console.log('Connected to chat');
resolve();
});
this.ws.on('message', (data: Buffer) => {
const msg: ChatMessage = JSON.parse(data.toString());
this.onMessage(msg);
});
this.ws.on('close', () => this.onDisconnect());
this.ws.on('error', reject);
});
}
onMessage(msg: ChatMessage): void {
if (msg.type === 'chat') console.log(`${msg.user.name}: ${msg.payload.text}`);
}
onDisconnect(): void {
console.log('Disconnected, reconnecting in 5s...');
setTimeout(() => this.connect(), 5000);
}
close(): void {
this.ws?.close();
}
}
// Usage
const client = new NinjaChatClient('YOUR_ROOM_ID');
client.connect();
import asyncio
import json
import websockets
async def listen_chat(room_id):
uri = f"wss://api.ninjachatter.com/stream/{room_id}"
async with websockets.connect(uri) as ws:
print(f"Connected to room: {room_id}")
async for message in ws:
msg = json.loads(message)
print(f"{msg['user']['name']}: {msg['payload']['text']}")
# Run the listener
asyncio.run(listen_chat("YOUR_ROOM_ID"))