# NinjaChatter automation guide

NinjaChatter provides room-scoped chat, bot publishing, and read-only live streams. An AI agent can use the same HTTP API as any other integration; no model account or special AI endpoint is required.

- API base: `https://api.ninjachatter.com`
- [Focused OpenAPI 3.1 reference](https://ninjachatter.com/docs/automation.openapi.json): readiness, public settings, publish, SSE, and owner-only bot key management. Import only the operations your integration needs. Some tool importers cannot consume long-lived SSE responses.
- [Tool input JSON Schema](https://ninjachatter.com/docs/send-room-message.schema.json): text-only input for a backend tool bound to one room; no credential or room arguments.
- [Backend JavaScript publisher](https://ninjachatter.com/docs/room-publisher.mjs): dependency-free module with a fixed room, bounded request size, timeout, and no automatic publishing retries.
- [Full API guide](https://ninjachatter.com/docs/api.html), [webhooks](https://ninjachatter.com/docs/ingress.html), and [OAuth/WebSocket integration](https://ninjachatter.com/docs/developers.html).

## Connect an agent to a room

1. Have the room owner select a room in the dashboard, open Integrations, and create a **send-only bot key**. Copy the key when it appears; it cannot be retrieved later.
2. Store the key in your backend's secret configuration. Bind the room and key in application code; expose only message text to the model's publishing tool. Send-only keys cannot delete messages, moderate, change settings, or mint privileged tokens. The separate legacy ingress key still has broader integration privileges.
3. Give the model a narrowly scoped tool such as `send_room_message(text)`, using `type: "chat"` and `payload.isBot: true`. Use a recognizable display name. Avoid automatic replies to every event, especially your own bot's events.
4. Treat incoming chat text, names, URLs, and metadata as untrusted user content. Keep them separate from system instructions and credentials, and require the operator's policy to authorize moderation or external actions.

The example was tested with Node.js 22. Download `room-publisher.mjs` into your backend and import it:

```js
import { createRoomPublisher } from './room-publisher.mjs';

const sendRoomMessage = createRoomPublisher({
  room: process.env.NINJACHATTER_ROOM,
  apiKey: process.env.NINJACHATTER_BOT_KEY,
});

// Connect this function to your agent's tool handler. Room and key stay server-side.
const result = await sendRoomMessage({ text: 'Your task is complete.' });
// result.status is "accepted" or "filtered"; accepted is not a viewer receipt.
```

Never embed either key in a public page, an iframe URL, chat content, or the model's prompt. For a viewing UI, use an overlay or the documented guest/OAuth join flow. Human provider verification still requires the normal OAuth interaction.

### Manage a send-only key

Each room can have one active send-only key. It remains valid until replaced, revoked, or the room is deleted; it does not expire automatically. Creating another immediately replaces the previous key. Rotation of legacy ingress keys and webhook secrets is independent.

The owner can use `GET`, `POST`, and `DELETE /rooms/{room}/bot-key` with `Authorization: Bearer <owner JWT>` to check status, create/replace, and revoke respectively. No request body is needed. Status and revocation return `{ "active": true/false }`; creation also returns `key` once. Responses have `Cache-Control: no-store`. Only a SHA-256 hash is stored. Do not give an agent the owner JWT or key-management tools.

Revocation is checked against shared storage on every ingress request. It blocks subsequent authentication; a request already authorized may finish. A restored backup may restore the credential state captured in that backup, so revoke/replace keys after a recovery if necessary.

Send-only keys accept only `type: "chat"`, optional `id` (1–128 UTF-8 bytes), and `payload` containing nonblank `text` (at most 4,096 Unicode characters), optional nonblank `displayName` (at most 80 characters), and optional `isBot: true`. Other fields, event types, and identity overrides are rejected with `400`. The server assigns a stable room bot identity and bot role. Content filtering and existing ingress limits still apply. The backend publisher below supports both key types without changes.

## Publish with HTTP

`POST /rooms/{room}/ingress` accepts a JSON envelope. The entire UTF-8 request body must be no more than 65,536 bytes. For visible bot messages use:

```json
{
  "type": "chat",
  "id": "your-correlation-id",
  "payload": {
    "text": "Your task is complete.",
    "displayName": "Assistant",
    "isBot": true
  }
}
```

Choose one authentication method:

- `X-API-Key: <send-only bot key or room ingress API key>`
- `Authorization: Bearer <send-only bot key or room ingress API key>` — a viewer or owner JWT is not an ingress key.
- `X-Chat-Signature: <Base64 HMAC-SHA256 digest>` using the room webhook secret and exact raw request bytes. Do not prepend `sha256=` and do not send the secret itself.

A Node.js signing example:

```js
import { createHmac } from 'node:crypto';
const body = JSON.stringify({ type: 'chat', payload: { text: 'Hello', isBot: true } });
const signature = createHmac('sha256', process.env.NINJACHATTER_WEBHOOK_SECRET)
  .update(body, 'utf8').digest('base64');
// Send exactly body, with Content-Type: application/json and X-Chat-Signature: signature.
```

A successful response contains `message_id` and, if supplied, `external_id`. For a chat publication, `message_id: "filtered"` with no `external_id` means the room's content filter blocked publication. If your supplied ID is literally `filtered`, the accepted response also includes `external_id: "filtered"`; that is not a filter rejection. An ID you supply is a correlation value, not an idempotency key: repeating a request can broadcast it again. A successful response acknowledges the broker publication; it does not prove that a viewer received or displayed it.

## Read live chat

`GET /rooms/{room}/public` returns public settings without secrets. `enabled` and `allow_viewers` must both be true for public read-only streams. `allow_guest` controls guest chat joins separately.

`GET /stream/{room}/sse` is a long-lived `text/event-stream` response with chat envelopes in `data:` fields and comment keep-alives. It emits chat only; it is not a moderation or deletion feed. Use the documented WebSocket integration when those events are needed. No token is needed for an enabled public viewer stream. Do not enable public viewers on a private room merely to connect a bot; use an authorized WebSocket integration instead.

There is no durable history guarantee or `Last-Event-ID` resumption. Events during outages or disconnection can be lost. Reconnect reads with bounded exponential backoff and jitter; stop on permission failures. Consumers that fall behind may be disconnected.

## Handle failures deliberately

- `400`: fix the request; do not retry unchanged. The ingress limit is 64 KiB; authenticated chat text has a separate default limit of 4,096 bytes.
- `401` / `403`: fix authentication or permission; do not loop retries.
- `404`: check the configured room; it may have been deleted.
- `429`: honor `Retry-After` when present. Rate limits vary by endpoint; do not assume one service-wide quota.
- `5xx`, timeouts, or network failures: publishing may have an unknown outcome. Do not blindly retry a message that could already have been accepted. Decide using the integration's duplicate-handling policy.

Application errors normally return `{ "error": "..." }`; proxy and framework errors may be plain text or HTML. Check HTTP status and content type before decoding JSON. Never log authorization headers.

`GET /readyz` returns `200` with `ok` when dependencies are ready. It is a readiness check, not proof of end-to-end message delivery.

## Host slow mode

Pro hosts can open **Dashboard → Room Settings → Slow mode (seconds)**, choose a delay from 1 to 300 seconds, and save. Set it to 0 and save to turn it off. Through the owner API, update `features.slow_mode_secs` with `PUT /rooms/{room}`.

The cooldown applies per viewer to chat messages and is shared across app nodes and connections. Chat displays the remaining wait after a throttled send. Bot/webhook ingress keeps its existing rate limits. Slow mode can make busy chat easier to follow, but it does not cap the room's total publication rate when many different viewers send messages.
