Developer API

Docs
Home Dashboard

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.

Quick Start

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.

1. WebSocket Stream (Recommended)

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}`);
};

2. SSE Stream (Simpler)

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}`);
};

Read-Only Stream API

These endpoints provide read-only access to room chat messages. Perfect for building overlays, bots, and integrations.

Room owners can disable read-only stream access in the Dashboard under Room Settings > "Allow read-only stream access".

Endpoints

EndpointProtocolFormatDescription
/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

Features

SSN Overlay Integration

If you have existing SocialStream.ninja overlays, you can point them at our API with minimal changes.

Using SSN dock.html

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
Replace 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.

SSN Message Format

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
}

Message Formats

Our Clean Envelope Format

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 Reference

FieldTypeDescription
idstringUnique message ID
typestringAlways "chat" for stream endpoints
roomstringRoom ID
user.idstringUser's unique ID
user.namestringDisplay name
user.providerstringAuth provider: discord, twitch, youtube, patreon, guest, webhook
user.rolestringUser role: viewer, publisher, mod, bot
user.avatarstring?Avatar URL (optional)
payload.textstringPlain text message content
payload.sourcestring?Original source platform (for SSN messages)
payload.chatmessagestring?HTML-formatted message (for SSN messages)
payload.textonlyboolean?If false, chatmessage contains HTML
tsnumberUnix timestamp (seconds)
metaobjectAdditional metadata

SSE (Server-Sent Events)

The SSE endpoint is a simpler alternative to WebSocket that works in more environments.

Benefits

Usage

const 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...');
};

Full WebSocket API

For full chat participation (sending messages, reactions, etc.), use the authenticated WebSocket endpoint.

Endpoint

WS /ws/:room?token=JWT_TOKEN

Getting a Token

Use the token endpoint to mint guest or authenticated integration tokens, or use OAuth login flows.

Using embed.js SDK

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);

Code Examples

Minimal OBS Overlay

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>

React Hook

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>
  );
}

Node.js / TypeScript

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();

Python Bot

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"))

Related Documentation