# Linear Void API Reference Base URLs: Public/admin app `https://linearvoid.tv` · WebSocket `wss://linearvoid.tv/ws` · Local fallback `http://localhost` / `ws://localhost/ws` ## Authentication Admin REST endpoints use a cookie session: - Cookie: `linearvoid_admin_session` — `HttpOnly`, `SameSite=Strict`, `Secure` when request is HTTPS (or `X-Forwarded-Proto=https`) - Session TTL: `ADMIN_SESSION_TTL_HOURS` (default 12h) ## Chat WebSocket API Connect: `GET /ws?username={name}&room={room}` — client/server exchange JSON messages. | Message (client→server) | Field | Notes | | ----------------------- | -------------------------------------------------------------------------------- | ----- | | `type` | `public` for room chat, `private` for DM routing; `system` for moderation/events | | | `room` | `main`; private DMs use `dm:userA-userB` (usernames sorted) | | | `username` | sender | | | `text` | message body | | Server→client envelope adds `"timestamp"` (ISO-8601, e.g. `2026-08-04T12:34:56Z`). ```json { "room": "main", "username": "viewer123", "text": "hello world", "type": "public" } ``` ## Admin Session API | Method & Path | Auth | Description | | ------------------------ | ----------------------------------- | ----------------------------- | | `POST /api/admin/login` | none | Authenticate | | `POST /api/admin/logout` | optional (clears cookie either way) | Logout | | `GET /api/admin/session` | cookie required | Session check; 401 if invalid | Login request: ```json { "username": "admin", "password": "your-password" } ``` Login/logout/session success response (session returns 401 on failure): ```json { "authenticated": true, "username": "admin" } ``` Logout response: `{ "authenticated": false }` ## Admin Data API (auth required) | Method & Path | Returns | | ----------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `GET /api/admin/overview` | Aggregate counters + runtime/system snapshot | | `GET /api/admin/playlist` | Playlist | | `PUT /api/admin/playlist` | Set playlist (body: `{ "content": "file '/app/media/video1.mp4'\nfile '/app/media/video2.mp4'\n" }`) | | `GET /api/admin/schedule` | Parsed playlist entries + matched media metadata/runtime | | `GET /api/admin/moderation/logs?limit=50` | Moderation logs | | `POST /api/admin/moderation/actions` | Perform moderation action | | `GET /api/admin/analytics` | Totals (messages, moderation actions, rooms) + top room summary | | `GET /api/admin/system` | Health snapshot (chat/playout/redis/database) + config metadata | | `GET /api/admin/users` | Presence users (room + username) when Redis available | Moderation action request: ```json { "action": "mute", "target": "username-or-id", "moderator": "admin", "reason": "spam", "room": "main" } ``` ## Media Library API | Method & Path | Description | | ------------------------------ | ------------------------------------------------- | | `GET /api/admin/media` | List media | | `POST /api/admin/media` | Multipart upload (legacy), form field name `file` | | `GET /api/admin/media/{id}` | Get item | | `PUT /api/admin/media/{id}` | Update item | | `DELETE /api/admin/media/{id}` | Delete item | PUT media body: ```json { "status": "ready", "title": "My Clip", "tags": "music,live" } ``` ## Resumable Upload API Max file size: **2GB** · Default chunk size: **8MB** | Method & Path | Description | | ------------------------------------------------------------------ | ----------------------------------------------------------------- | | `POST /api/admin/media/uploads` | Initialize or resume session | | `GET /api/admin/media/uploads/{uploadId}` | Check upload status | | `PUT /api/admin/media/uploads/{uploadId}/chunk?index={chunkIndex}` | Upload chunk (`Content-Type: application/octet-stream`) | | `POST /api/admin/media/uploads/{uploadId}/complete` | Complete upload → returns media listing (`files` + `generatedAt`) | Initialize request: ```json { "uploadId": "optional-existing-id", "fileName": "clip.mp4", "fileSize": 123456789, "mimeType": "video/mp4", "chunkSize": 8388608, "totalChunks": 15 } ``` Status envelope (returned by init, status check, and chunk upload): ```json { "uploadId": "hex-token", "fileName": "clip.mp4", "fileSize": 123456789, "chunkSize": 8388608, "totalChunks": 15, "uploadedChunks": 3, "progress": 20 } ``` ## Metrics & Health - `GET /metrics` (chat) — Prometheus: connected clients, stored messages, moderation actions, Redis indicators, active admin sessions, build info - `GET /health` — chat service, playout service, and nginx reverse proxy each expose it ## HTTP Status Conventions | Code | Meaning | When | | ---- | --------------------- | ----------------------------------------------------------- | | 200 | OK | Successful GET, PUT, action | | 201 | Created | Successful POST (upload init, override created) | | 204 | No Content | Successful DELETE, CORS preflight | | 400 | Bad Request | Malformed JSON, missing required fields, invalid input | | 401 | Unauthorized | Missing/expired admin session | | 403 | Forbidden | Valid session, insufficient permissions | | 404 | Not Found | Resource doesn't exist | | 405 | Method Not Allowed | Wrong method for endpoint | | 409 | Conflict | Schedule override overlap, upload ID mismatch | | 413 | Payload Too Large | Upload exceeds 2 GB limit | | 422 | Unprocessable | File fails normalization check (codec, fps, resolution) | | 429 | Too Many Requests | Rate limit exceeded (chat, WS, login) | | 500 | Internal Server Error | Unexpected server failure | | 503 | Service Unavailable | Redis unavailable for user actions, playout health degraded | ## Rate Limiting Per-IP (from `X-Forwarded-For` or `RemoteAddr`) unless noted. | Resource | Limit | Window | Response | | --------------------- | ---------------------------------- | ---------------------------------------- | ------------------------------------------------------ | | Chat messages | `CHAT_RATE_LIMIT_MAX` (default 10) | `CHAT_RATE_LIMIT_WINDOW_S` (default 10s) | 429 + `{"type":"system","text":"Rate limit exceeded"}` | | WebSocket connections | 5 | 60s | 429 + `"too many connections from this IP"` | | Admin login | 5 | 5min | 429 + `"too many login attempts, try again later"` | Rate limit headers (applicable responses): `X-RateLimit-Limit` (max requests/window), `Retry-After` (seconds until reset). 413 = payload too large; 500 = internal server error.