{
  "openapi": "3.1.0",
  "info": {
    "title": "NinjaChatter Automation API",
    "version": "1.1.0",
    "description": "Focused reference for agents and bot backends: readiness, public settings, ingress, read-only chat, and owner-only bot key management. Keep owner credentials and management operations outside agent tools. This is a subset of the service API."
  },
  "servers": [
    {
      "url": "https://api.ninjachatter.com"
    }
  ],
  "externalDocs": {
    "url": "https://ninjachatter.com/docs/automation.md"
  },
  "paths": {
    "/readyz": {
      "get": {
        "operationId": "checkReadiness",
        "summary": "Check Redis and NATS readiness",
        "security": [],
        "responses": {
          "200": {
            "description": "Dependencies ready",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "const": "ok"
                }
              }
            }
          },
          "503": {
            "description": "Server draining"
          },
          "500": {
            "description": "Redis or NATS readiness check failed"
          }
        }
      }
    },
    "/rooms/{room}/public": {
      "get": {
        "operationId": "getPublicRoom",
        "summary": "Read public room settings",
        "security": [],
        "parameters": [
          {
            "name": "room",
            "in": "path",
            "required": true,
            "description": "Room ID provided by the operator.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Public settings; never contains the ingress key or webhook secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicRoom"
                }
              }
            }
          },
          "404": {
            "description": "Request rejected. Application errors use an error string; proxies may return non-JSON responses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Request rejected. Application errors use an error string; proxies may return non-JSON responses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/rooms/{room}/ingress": {
      "post": {
        "operationId": "publishRoomEvent",
        "summary": "Publish an event to one room",
        "description": "Use type chat for visible bot messages and payload.isBot=true. A 200 response acknowledges publication to the broker, not delivery to viewers. A filtered chat message returns message_id=filtered without external_id. A supplied ID named filtered is accepted normally and includes external_id=filtered. Supplied IDs are correlation IDs: retries are not deduplicated. Total raw body limit is 65,536 bytes. Keys also grant elevated integration capabilities; keep them in a trusted backend. Use exactly one authentication method. Send-only bot keys restrict requests to type chat, optional id (1\u2013128 UTF-8 bytes), and payload with nonblank text (up to 4096 Unicode characters), optional displayName (1\u201380 characters), and optional isBot true. No other fields are accepted. Identity is assigned by the server.",
        "security": [
          {
            "IngressApiKey": []
          },
          {
            "IngressBearer": []
          },
          {
            "IngressHmac": []
          }
        ],
        "parameters": [
          {
            "name": "room",
            "in": "path",
            "required": true,
            "description": "Room ID provided by the operator.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IngressPayload"
              },
              "example": {
                "type": "chat",
                "payload": {
                  "text": "Your task is complete.",
                  "displayName": "Assistant",
                  "isBot": true
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted by broker or filtered; not a delivery receipt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngressResponse"
                }
              }
            }
          },
          "400": {
            "description": "Request rejected. Application errors use an error string; proxies may return non-JSON responses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Request rejected. Application errors use an error string; proxies may return non-JSON responses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Request rejected. Application errors use an error string; proxies may return non-JSON responses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Request rejected. Application errors use an error string; proxies may return non-JSON responses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/stream/{room}/sse": {
      "get": {
        "operationId": "readRoomChat",
        "summary": "Read live chat events from a viewer-enabled room",
        "description": "Long-lived read-only Server-Sent Events. Requires an enabled room with allow_viewers=true, independent of allow_guest. Emits only chat events as JSON in data fields and periodic comment keep-alives. No durable replay or Last-Event-ID resumption. Disconnects when viewing is disabled or the consumer stalls. Many tool importers cannot consume long-lived responses; use an SSE client.",
        "security": [],
        "parameters": [
          {
            "name": "room",
            "in": "path",
            "required": true,
            "description": "Room ID provided by the operator.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Live chat envelope stream",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "Request rejected. Application errors use an error string; proxies may return non-JSON responses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Request rejected. Application errors use an error string; proxies may return non-JSON responses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Request rejected. Application errors use an error string; proxies may return non-JSON responses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Request rejected. Application errors use an error string; proxies may return non-JSON responses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/rooms/{room}/bot-key": {
      "get": {
        "operationId": "getBotKeyStatus",
        "summary": "Check whether a send-only key is active",
        "description": "Owner only. One key per room, valid until replaced, revoked, or room deletion. No request body. Subsequent authentication uses shared storage; already authorized requests may finish. Legacy ingress credentials are unaffected.",
        "security": [
          {
            "BotKeyOwner": []
          }
        ],
        "parameters": [
          {
            "name": "room",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current credential state. Cache-Control: no-store.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Contains no-store. Proxies may append additional compatible cache-control directives."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "active"
                  ],
                  "properties": {
                    "active": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid owner authentication"
          },
          "403": {
            "description": "Not the room owner"
          },
          "404": {
            "description": "Room not found"
          },
          "500": {
            "description": "Storage unavailable"
          }
        }
      },
      "post": {
        "operationId": "replaceBotKey",
        "summary": "Create or replace the room send-only key",
        "description": "Owner only. One key per room, valid until replaced, revoked, or room deletion. No request body. Subsequent authentication uses shared storage; already authorized requests may finish. Legacy ingress credentials are unaffected.",
        "security": [
          {
            "BotKeyOwner": []
          }
        ],
        "parameters": [
          {
            "name": "room",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current credential state. Cache-Control: no-store.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Contains no-store. Proxies may append additional compatible cache-control directives."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "active",
                    "key"
                  ],
                  "properties": {
                    "active": {
                      "type": "boolean"
                    },
                    "key": {
                      "type": "string",
                      "description": "Shown only in this response; store securely. Replaces any previous key."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid owner authentication"
          },
          "403": {
            "description": "Not the room owner"
          },
          "404": {
            "description": "Room not found"
          },
          "500": {
            "description": "Storage unavailable"
          }
        }
      },
      "delete": {
        "operationId": "revokeBotKey",
        "summary": "Revoke the room send-only key",
        "description": "Owner only. One key per room, valid until replaced, revoked, or room deletion. No request body. Subsequent authentication uses shared storage; already authorized requests may finish. Legacy ingress credentials are unaffected.",
        "security": [
          {
            "BotKeyOwner": []
          }
        ],
        "parameters": [
          {
            "name": "room",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current credential state. Cache-Control: no-store.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Contains no-store. Proxies may append additional compatible cache-control directives."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "active"
                  ],
                  "properties": {
                    "active": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid owner authentication"
          },
          "403": {
            "description": "Not the room owner"
          },
          "404": {
            "description": "Room not found"
          },
          "500": {
            "description": "Storage unavailable"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "IngressApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Send-only bot key or legacy room ingress API key. Bot keys accept only the restricted text chat schema described in the automation guide. Not an owner/viewer JWT."
      },
      "IngressBearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Send-only bot key or legacy room ingress API key. Bot keys accept only the restricted text chat schema described in the automation guide. Not an owner/viewer JWT."
      },
      "IngressHmac": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Chat-Signature",
        "description": "Base64 HMAC-SHA256 of the exact raw request bytes using the room webhook secret. No sha256= prefix. Compute a fresh signature for each distinct body; never send the secret itself."
      },
      "BotKeyOwner": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Room owner JWT; keep outside agent tools."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "IngressPayload": {
        "type": "object",
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "type": "string",
            "description": "Use chat for displayed messages. Other values pass through; delete removes a displayed target. This does not replace the moderation API."
          },
          "payload": {
            "description": "For chat, provide an object with text, optional displayName, userId, and isBot. Defaults to null if omitted."
          },
          "meta": {
            "description": "Optional metadata passed through unchanged."
          },
          "id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional caller correlation ID. Reusing it does not deduplicate publication."
          },
          "target_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "For type delete; alternatively supply payload.target_id."
          }
        }
      },
      "IngressResponse": {
        "type": "object",
        "required": [
          "message_id"
        ],
        "properties": {
          "message_id": {
            "type": "string",
            "description": "Generated or supplied ID. For chat, filtered with no external_id means no broadcast; external_id=filtered identifies a normally accepted caller ID. For delete, this is the target ID."
          },
          "external_id": {
            "type": "string",
            "description": "Supplied id when present on a normal publication."
          }
        }
      },
      "PublicRoom": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Whether the room accepts non-owner connections; the owner can still connect when false."
          },
          "owner_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Owner identity; compare together with owner_provider."
          },
          "owner_provider": {
            "type": [
              "string",
              "null"
            ],
            "description": "Provider namespace of the owner identity."
          },
          "room": {
            "type": "string"
          },
          "embed_url": {
            "type": "string"
          },
          "features": {
            "$ref": "#/components/schemas/RoomFeatures"
          },
          "plan": {
            "type": [
              "string",
              "null"
            ]
          },
          "allowed_providers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Provider"
            }
          },
          "allow_viewers": {
            "type": "boolean",
            "description": "Whether unauthenticated read-only viewers may subscribe; also requires enabled=true."
          }
        },
        "required": [
          "room",
          "enabled",
          "allow_viewers",
          "embed_url",
          "features"
        ]
      },
      "RoomFeatures": {
        "type": "object",
        "properties": {
          "filter_on": {
            "type": "boolean",
            "description": "Enable bad word filtering"
          },
          "cache_ms": {
            "type": "integer",
            "description": "Message cache duration in milliseconds"
          },
          "binary_allowed": {
            "type": "boolean",
            "description": "Allow binary WebSocket messages"
          },
          "allow_guest": {
            "type": "boolean",
            "description": "Allow guest authentication"
          },
          "slow_mode_secs": {
            "type": "integer",
            "minimum": 0,
            "description": "Minimum seconds between chat messages per user in paid rooms; zero disables slow mode and values above 300 are clamped to 300. Bot/webhook ingress uses its existing rate limits."
          }
        }
      },
      "Provider": {
        "type": "string",
        "enum": [
          "discord",
          "patreon",
          "twitch",
          "youtube",
          "google",
          "guest",
          "bot",
          "webhook"
        ]
      }
    }
  }
}
