API Documentation
Complete reference for the Open Relay Portal REST API and WebSocket interfaces.
Introduction
Open Relay Portal provides a unified API for managing remote access to your infrastructure. All API endpoints return JSON responses and require authentication unless otherwise noted.
https://portal.example.com)
Response Format
All successful responses return JSON. Error responses include an error field:
{
"error": "Unauthorized",
"status": 401
}
Authentication
Open Relay Portal supports the following authentication methods:
- JWT Tokens - Session-based authentication via cookies (used by the web UI)
- API Keys - Long-lived keys for programmatic access (prefixed
portal_) - RTMP Tokens - Temporary, single-use publish tokens for plain RTMP streaming (prefixed
rtmp_). Generated via API or the My Streams UI. Expire after 15 minutes with a 30-second grace period for reconnects.
Using API Keys
Include your API key in the Authorization header:
Authorization: Bearer YOUR_API_KEY
Or pass it as a query parameter (not recommended for sensitive operations):
GET /api/services?token=YOUR_API_KEY
Authenticate with username and password to receive a JWT token.
Request Body
| Parameter | Type | Description |
|---|---|---|
| username | string | Your username required |
| password | string | Your password required |
| totp_code | string | 6-digit 2FA code (if 2FA enabled) |
Example
curl -X POST https://portal.example.com/api/token \
-H "Content-Type: application/json" \
-d '{"username": "user", "password": "pass"}'
Response
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"expires_at": "2024-01-15T12:00:00Z",
"user": {
"id": 1,
"username": "user",
"is_admin": false
}
}
API Keys
API keys provide long-lived authentication for scripts and integrations. Keys can be scoped to limit access.
Request Body
| Parameter | Type | Description |
|---|---|---|
| name | string | Descriptive name for the key required |
| scopes | string | Comma-separated scopes (default: "*" for full access) |
| expires_in_days | integer | Days until expiration (null for never) |
Response
{
"id": 1,
"name": "My Integration",
"api_key": "portal_abc123...",
"key_prefix": "portal_abc",
"scopes": "*",
"expires_at": null
}
Returns all API keys for the authenticated user. Keys are shown with prefix only (not full key).
Revokes the API key. The key will no longer work for authentication but remains in your key list.
Permanently delete an API key from your account.
Token Management
Returns all active JWT tokens for the authenticated user.
Request Body
| Parameter | Type | Description |
|---|---|---|
| token_id | string | Token ID to revoke required |
Registration
Register a new account using an invite code. Rate limited to 1 account per IP per 24 hours.
Request Body
| Parameter | Type | Description |
|---|---|---|
| username | string | Desired username required |
| password | string | Password (min 8 chars) required |
| invite_code | string | Daily invite code required |
Response
{
"id": 5,
"username": "newuser",
"message": "Registration successful"
}
Public Stats
Public statistics available to all authenticated users.
Returns public metrics visible to all authenticated users, including live stream counts and online user counts.
Response
{
"live_streams": 3,
"online_users": 12
}
Fields
live_streams- Number of currently live public streamsonline_users- Number of unique users currently in chat
Services
Services are shared infrastructure endpoints configured by administrators. Users can list and connect to services they have access to.
Returns all services the authenticated user has access to based on their scopes.
Response
{
"services": [
{
"id": 1,
"name": "Home Server",
"plugin": "ssh",
"path": "/homeserver",
"icon": "terminal",
"category": "Servers",
"enabled": true
}
]
}
Try It
Returns detailed information about a specific service.
Checks if the backend service is reachable and responding.
Response
{
"healthy": true,
"latency_ms": 45
}
User Connections
User Connections are personal remote access points that you configure for your own use. Unlike Services (admin-configured), connections are private to each user.
aB3x_kLm9pQ2rStU), not sequential integers. Use the id field returned by the API for all operations.
Returns available connection types with their plugin schemas for form generation.
Response
{
"types": {
"ssh": {
"name": "SSH Terminal",
"icon": "terminal",
"default_port": 22,
"plugin": "ssh",
"config_schema": {...}
},
"vnc": {...},
"rdp": {...}
}
}
Returns all connections for the authenticated user.
Try It
Request Body
| Parameter | Type | Description |
|---|---|---|
| name | string | Display name required |
| type | string | Connection type (ssh, vnc, rdp, etc.) required |
| host | string | Remote host address required |
| port | integer | Port number (uses default if omitted) |
| config | object | Type-specific configuration (username, auth_method, etc.) |
Example
curl -X POST https://portal.example.com/api/connections \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "My Home Server",
"type": "ssh",
"host": "192.168.1.100",
"port": 22,
"config": {
"username": "admin",
"auth_method": "password"
}
}'
Returns details for a specific connection. Sensitive config fields (passwords, private keys) are redacted and replaced with has_<field> boolean flags.
Update a connection. Config fields are merged server-side with the existing config — sensitive fields not included in the request are preserved from the encrypted database record.
Request Body
| Parameter | Type | Description |
|---|---|---|
| name | string | Updated display name |
| host | string | Updated host address |
| port | integer | Updated port number |
| config | object | Updated type-specific configuration |
Permanently delete a connection.
Toggle whether a connection is pinned. Pinned connections sort to the top of the connections list for quick access.
Response
{
"is_pinned": true
}
Returns connection details and WebSocket URL needed to establish a connection. The dashboard uses this to open the appropriate viewer window.
Response
{
"connection": {
"id": 1,
"type": "ssh",
"name": "My Server",
"host": "10.0.0.5",
"port": 22,
"config": {}
},
"ssh_public_key": "ssh-ed25519 AAAA... (if SSH key is set)",
"websocket_url": "/ws/user-connection/1"
}
Connection Type Routing
| Type | Viewer | URL Pattern |
|---|---|---|
| ssh, terminal | Terminal (xterm.js) | /terminal/connect?connection={id} |
| vnc, rdp | VNC (noVNC) | /vnc/connect?connection={id} |
| spice | SPICE Console | /spice/connect?connection={id} |
| proxmox | Proxmox VE | /proxmox/connect?connection={id} |
| http, https, http_proxy | Embedded Browser | /browser?connection={id} |
| mediamtx, stream | Media Player | /media/connect?connection={id} |
| database, redis, tcp_tunnel, etc. | Info Toast | Shows host:port (no web viewer) |
All viewer pages connect via wss://portal/ws/user-connection/{id} which routes through the appropriate plugin based on the connection type.
User Streams
Stream live video using RTMPS, RTSPS, or WebRTC. Streams can be public (visible to all users) or private. Each stream gets an integrated chat channel for viewer interaction.
stream.example.com) for direct RTMPS access with Let's Encrypt TLS. Playback is proxied through Portal on port 443 via Cloudflare.
Streaming Configuration
| Purpose | Host | URL Format |
|---|---|---|
| RTMPS Publishing | stream.example.com:1936 | rtmps://stream.example.com:1936/live |
| HLS Playback | portal.example.com:443 | https://portal.example.com/api/stream/{stream_key}/hls/index.m3u8 |
| WebRTC Playback | portal.example.com:443 | https://portal.example.com/api/stream/{stream_key}/webrtc/whep |
| WebRTC Publishing | portal.example.com:443 | https://portal.example.com/api/stream/{stream_key}/webrtc/whip |
| RTMP Publishing | stream.example.com:1935 | rtmp://stream.example.com:1935/live |
OBS Studio Setup (Recommended)
To broadcast with OBS Studio using RTMPS (encrypted, recommended):
- Go to Settings → Stream
- Set Service to Custom
- Server:
rtmps://stream.example.com:1936/live - Stream Key: Your stream key (e.g.,
live_abc123...) - Click OK and Start Streaming
Plain RTMP (Alternative)
If your encoder does not support RTMPS, you can use standard RTMP with a temporary token. This requires enabling RTMP on your stream first.
- Enable RTMP on your stream (My Streams tab or
PUT /api/streams/{id}withrtmp_enabled: true) - Generate a temporary RTMP token via the My Streams tab or
POST /api/streams/{id}/rtmp-token - In OBS, go to Settings → Stream
- Set Service to Custom
- Server:
rtmp://stream.example.com:1935/live - Stream Key: Your temporary token (e.g.,
rtmp_abc123...) - Click OK and Start Streaming
RTMP_PLAIN_ENABLED setting must also be enabled by the server administrator.
Encoder Settings
Configure your encoder in Settings → Output → Streaming (Output Mode: Advanced).
NVIDIA NVENC (Recommended for NVIDIA GPUs)
Hardware encoding with NVENC provides excellent quality at low CPU usage.
| Setting | Recommended Value | Notes |
|---|---|---|
| Encoder | NVIDIA NVENC H.264 | Or NVENC HEVC for better compression (requires viewer support) |
| Rate Control | CBR | Constant bitrate for stable streaming |
| Bitrate | 4500-6000 kbps | Adjust based on your upload speed |
| Keyframe Interval | 2 | Required for HLS segmentation |
| Preset | Quality or Max Quality | P5-P7 on newer drivers |
| Profile | high | Best compatibility |
| Look-ahead | Enabled | Improves quality at cost of slight latency |
| Psycho Visual Tuning | Enabled | Better perceived quality |
| B-frames | 2 | Good balance of quality and latency |
AMD AMF (AMD GPUs)
| Setting | Recommended Value |
|---|---|
| Encoder | AMD HW H.264 (AVC) |
| Rate Control | CBR |
| Bitrate | 4500-6000 kbps |
| Keyframe Interval | 2 |
| Quality Preset | Quality |
x264 (CPU Encoding)
| Setting | Recommended Value |
|---|---|
| Encoder | x264 |
| Rate Control | CBR |
| Bitrate | 4500-6000 kbps |
| Keyframe Interval | 2 |
| CPU Usage Preset | veryfast to medium |
| Profile | high |
| Tune | zerolatency (optional) |
Video Settings (Settings → Video)
| Setting | Recommended Value |
|---|---|
| Output Resolution | 1920x1080 or 1280x720 |
| FPS | 30 or 60 |
REST API
Returns all streams owned by the authenticated user.
Response
{
"streams": [
{
"id": 1,
"title": "My Live Stream",
"stream_key": "sk_abc123...",
"is_live": 0, // 0=offline, 1=live, 2=encoding
"is_public": true,
"rtmp_enabled": false,
"viewer_count": 0,
"chat_channel_id": 5,
"created_at": "2024-01-15T10:00:00Z"
}
]
}
Try It
Creates a new stream with a unique stream key. An associated chat channel is automatically created.
Request Body
| Parameter | Type | Description |
|---|---|---|
| title | string | Stream title required |
| description | string | Stream description |
| is_public | boolean | Whether stream is publicly visible (default: true) |
| rtmp_enabled | boolean | Enable plain RTMP publishing with temporary tokens (default: false) |
Response
{
"id": 1,
"title": "My Stream",
"stream_key": "sk_abc123def456...",
"rtmp_enabled": false,
"chat_channel_id": 5,
"playback_urls": {
"hls": "https://portal.example.com/api/stream/sk_abc123/hls/index.m3u8",
"webrtc": "https://portal.example.com/api/stream/sk_abc123/webrtc/whep"
}
}
Returns all public streams (live, encoding, and offline). Used by the community streams page. The is_live field is an integer: 0=offline, 1=live, 2=encoding (VOD finalization in progress).
Try It
Returns all public streams that are currently live. Includes viewer counts and playback URLs.
Try It
Returns detailed information about a specific stream including playback URLs.
Update title, description, or visibility of your stream.
Request Body
| Parameter | Type | Description |
|---|---|---|
| title | string | New stream title |
| description | string | New description |
| is_public | boolean | Change visibility |
| rtmp_enabled | boolean | Enable or disable plain RTMP publishing with temporary tokens |
Generate a new stream key, invalidating the old one. Use this if your key is compromised.
Generate a temporary token for publishing via plain RTMP (unencrypted). The stream must have rtmp_enabled: true and the server must have RTMP_PLAIN_ENABLED set globally.
Prerequisites
- Stream must have
rtmp_enabledset totrue - Global
RTMP_PLAIN_ENABLEDmust be enabled by the server administrator - Authenticated as the stream owner (Bearer token)
Response
{
"token": "rtmp_abc123...",
"expires_in": 900,
"rtmp_url": "rtmp://stream.example.com:1935/live"
}
Internally, the token doubles as the MediaMTX publish path. Portal maps it back to the stream automatically, so all stream features (HLS playback, dynamic thumbnails, VOD recording) work seamlessly with plain RTMP.
Permanently delete a stream and its associated chat channel.
Upload a custom thumbnail image (owner only). Multipart form data with field thumbnail. Max 2MB, JPEG/PNG.
Remove a custom thumbnail for a stream (owner only). The stream will revert to using a dynamic thumbnail when live.
Returns a dynamic JPEG thumbnail captured from a live stream via ffmpeg. Cached for 15 seconds. Returns static thumbnail (or 404) if stream is offline or encoding. Accepts live_xxx or pub_xxx key.
Stream Moderation
Stream owners can ban users from their stream's chat channel.
List users banned from this stream's chat (owner only).
Request Body
| Parameter | Type | Description |
|---|---|---|
| user_id | integer | User to ban required |
| reason | string | Ban reason |
Remove a ban, allowing the user to chat again (owner only).
Watching Streams
All stream traffic routes through Portal on port 443. Viewers can watch streams using HLS.js in browser or any HLS-compatible player:
import Hls from 'hls.js'; const video = document.getElementById('video'); const hls = new Hls(); // All playback through Portal on port 443 hls.loadSource('https://portal.example.com/api/stream/sk_abc123/hls/index.m3u8'); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, () => { video.play(); });
Publishing via WHIP
Publish streams through port 443 using WebRTC WHIP (OBS 30.0+):
// OBS Settings: // Service: WHIP // Server: https://portal.example.com/api/stream/{stream_key}/webrtc/whip // Bearer Token: (leave empty - key is in URL)
Stream Relays
Relay your stream to external platforms (Twitch, YouTube, Kick, other Portal instances, or custom RTMP destinations). Relays start automatically when you go live and stop when your stream ends. Maximum 10 relay destinations per stream. Relay credentials are encrypted at rest.
Returns the list of supported relay platforms with their default RTMP ingest URLs.
{
"platforms": [
{ "id": "twitch", "name": "Twitch", "default_url": "rtmp://live.twitch.tv/app" },
{ "id": "youtube", "name": "YouTube", "default_url": "rtmp://a.rtmp.youtube.com/live2" },
{ "id": "kick", "name": "Kick", "default_url": "rtmps://fa723fc1b171.global-contribute.live-video.net/app" },
{ "id": "portal", "name": "Portal", "default_url": "" },
{ "id": "custom", "name": "Custom", "default_url": "" }
]
}
Returns all relay destinations configured for this stream (owner only). Stream keys are never returned; a has_stream_key boolean indicates whether one is set.
Request Body
| Parameter | Type | Description |
|---|---|---|
| platform | string | Platform ID from /api/relay-platforms required |
| name | string | Display name for this destination required |
| rtmp_url | string | RTMP ingest URL (must start with rtmp:// or rtmps://) required |
| stream_key | string | Stream key for the destination platform required |
| enabled | boolean | Whether to relay when live (default: true) |
Update a relay destination's name, URL, stream key, or enabled state (owner only). All fields are optional; only provided fields are updated.
Remove a relay destination (owner only). If the relay is currently active, it will be stopped.
VOD Storage
Manage remote SFTP storage for recorded VODs (MKV files). Each user configures their own storage server. VODs are automatically recorded as 5-minute MKV chunks during live broadcasts and continuously uploaded to SFTP. When a stream ends, it enters an Encoding state (is_live=2) while the final chunk is written and all remaining chunks are offloaded, ensuring no VOD data is lost.
Returns the user's SFTP storage configuration. Passwords and private keys are redacted.
Response
{
"storage": {
"host": "storage.example.com",
"port": 22,
"username": "user",
"auth_method": "password",
"remote_path": "/home/user/vods",
"has_password": true,
"has_key": false
}
}
Request Body
| Field | Type | Description |
|---|---|---|
| host | string | SFTP server hostname |
| port | integer | SSH port (default: 22) |
| username | string | SSH username |
| auth_method | string | "password" or "key" |
| password | string | SSH password (if auth_method=password) |
| private_key | string | SSH private key PEM (if auth_method=key) |
| remote_path | string | Path to VOD directory on remote server |
Removes the SFTP storage configuration. Does not delete any remote files.
Tests the SFTP connection with the provided credentials and reports how many MKV files were found.
Recursively lists MKV files in the user's remote storage directory. Files are organized by recording session: StreamName/YYYY-MM-DD_HH-MM-SS/chunk_NNN.mkv. Returns 404 if no storage is configured.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| sort | string | Sort by: name, size, modified (default: modified) |
| order | string | asc or desc (default: desc) |
Response
{
"files": [
{
"name": "StreamName/2026-02-08_04-24-49/chunk_000.mkv",
"size": 233436982,
"modified": 1770525031
}
],
"path": "/home/user/vods"
}
Streams the specified MKV file from remote storage. Only .mkv files are allowed. Supports subdirectory paths. Path traversal is blocked.
Downloads multiple VOD files as a single zip archive streamed from SFTP. Maximum 500 files per request. All paths must end in .mkv.
Request Body
{
"files": [
"StreamName/2026-02-08_04-24-49/chunk_000.mkv",
"StreamName/2026-02-08_04-24-49/chunk_001.mkv"
]
}
Response
Binary zip stream with Content-Disposition: attachment header.
Deletes the specified MKV file from remote storage. Only .mkv files can be deleted. Supports subdirectory paths.
User Management
Returns information about the authenticated user.
Response
{
"id": 1,
"username": "myuser",
"is_admin": false,
"scopes": "access:*",
"created_at": "2024-01-01T00:00:00Z",
"totp_enabled": true
}
Try It
Request Body
| Parameter | Type | Description |
|---|---|---|
| current_password | string | Current password required |
| new_password | string | New password (min 8 chars) required |
Request Body
| Parameter | Type | Description |
|---|---|---|
| status | string | online, away, busy, dnd, or offline required |
| status_message | string | Custom status message (max 100 chars) |
Request Body
| Parameter | Type | Description |
|---|---|---|
| nickname | string | Display name (2-32 chars, alphanumeric/spaces/dashes). Empty to clear. |
Request Body
| Parameter | Type | Description |
|---|---|---|
| color | string | Hex color (#RRGGBB) |
| emoji | string | Emoji character (null to clear) |
| initials | string | 1-2 character initials |
Toggle anonymous mode in chat. Messages sent while anonymous remain anonymous in history.
Request Body
| Parameter | Type | Description |
|---|---|---|
| anonymous | boolean | Enable or disable anonymous mode required |
SSH Keys
Generate and manage SSH key pairs for authentication. Private keys are generated on the server and returned only once - they are never stored.
Request Body
| Parameter | Type | Description |
|---|---|---|
| name | string | Key name required |
| key_type | string | "ed25519" (default) or "rsa" |
Response
{
"id": 1,
"name": "my-key",
"public_key": "ssh-ed25519 AAAA...",
"private_key": "-----BEGIN OPENSSH PRIVATE KEY-----\n...",
"fingerprint": "SHA256:..."
}
Returns all SSH keys for the authenticated user (public keys only).
Returns details for a specific SSH key (public key only).
Permanently delete an SSH key pair.
Returns all your public keys in authorized_keys format for adding to remote servers.
Two-Factor Authentication
Enable TOTP-based two-factor authentication for enhanced security.
Response
{
"enabled": false,
"backup_codes_remaining": 0
}
Generates a TOTP secret and provisioning URI for authenticator apps.
Response
{
"secret": "JBSWY3DPEHPK3PXP",
"provisioning_uri": "otpauth://totp/Portal:user?secret=..."
}
Verify a TOTP code to enable 2FA. Returns backup codes.
Request Body
| Parameter | Type | Description |
|---|---|---|
| code | string | 6-digit TOTP code required |
Disable two-factor authentication. Requires a valid TOTP code to confirm.
Request Body
| Parameter | Type | Description |
|---|---|---|
| code | string | 6-digit TOTP code required |
WebSocket Service Relay
Open Relay Portal uses WebSocket connections to relay traffic to backend services. This enables real-time bidirectional communication for terminals, VNC, and other interactive protocols.
Connection URL Format
wss://portal.example.com/ws/{plugin}/{service_id}?token=YOUR_TOKEN
Supported Plugins
| Plugin | Description | Data Format |
|---|---|---|
| terminal | Local PTY terminal | JSON messages |
| ssh | SSH over WebSocket | JSON messages |
| vnc | VNC relay (for noVNC) | Binary frames |
| spice | SPICE VM console | Binary frames |
| tcp_tunnel | Generic TCP tunnel | Binary frames |
| secure_tunnel | Multiplexed secure tunnel | Binary frames |
Terminal WebSocket Protocol
Terminal sessions use JSON messages for communication:
Client Messages
// Send input {"type": "input", "data": "ls -la\n"} // Resize terminal {"type": "resize", "cols": 120, "rows": 40} // Ping (keep-alive) {"type": "ping"}
Server Messages
// Terminal output {"type": "output", "data": "total 128\ndrwxr-xr-x..."} // Pong response {"type": "pong"} // Error {"type": "error", "message": "Connection failed"}
JavaScript Example
const ws = new WebSocket('wss://portal.example.com/ws/ssh/1?token=...'); ws.onopen = () => { // Send terminal size ws.send(JSON.stringify({ type: 'resize', cols: terminal.cols, rows: terminal.rows })); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'output') { terminal.write(msg.data); } }; // Send user input terminal.onData(data => { ws.send(JSON.stringify({type: 'input', data})); });
VNC/RDP WebSocket Protocol
VNC connections relay raw binary RFB protocol frames:
wss://portal.example.com/ws/vnc/{service_id}?token=YOUR_TOKEN
Use with noVNC client library:
import RFB from '@novnc/novnc/core/rfb'; const rfb = new RFB( document.getElementById('vnc-container'), 'wss://portal.example.com/ws/vnc/1?token=...' ); rfb.scaleViewport = true; rfb.resizeSession = true;
TCP Tunnel Protocol
Generic TCP tunneling for databases, Redis, and other TCP services:
wss://portal.example.com/ws/tunnel/{service_id}?token=YOUR_TOKEN
Data is sent as binary WebSocket frames, directly relayed to/from the TCP backend.
User Connection WebSocket
Connect to your personal connections via:
wss://portal.example.com/ws/user-connection/{connection_id}
Chat/Forum
Real-time multi-user chat with channels. Messages are encrypted at rest and automatically deleted after 7 days.
Display Name Priority
Chat display names follow this priority order:
- Anonymous — If anonymous mode is enabled, displayed as "Anonymous" with no avatar or nickname
- Nickname — If set via
PUT /api/me/nickname - Username — The account username (fallback)
Anonymous state is stored per-message, so messages sent while anonymous remain anonymous even after the user turns anonymous mode off.
Message Rate Limiting
Chat messages are rate limited to 5 messages per 5 seconds per user. If exceeded, the server responds with an error and the message is not sent.
REST API
Returns all available chat channels.
Response
{
"channels": [
{
"id": 1,
"name": "general",
"description": "General discussion",
"is_default": 1,
"is_stream_channel": false
}
]
}
Request Body
| Parameter | Type | Description |
|---|---|---|
| name | string | Channel name (lowercase, hyphens allowed) required |
| description | string | Channel description |
Upload an image to embed in a chat message. Returns a URL to include as image_url in the WS message payload.
Request
Multipart form data with field name image. Max size: 5MB. Accepted types: JPEG, PNG, GIF, WebP.
Response
{
"url": "/static/uploads/chat/abc123.jpg"
}
Fetches OpenGraph metadata (title, description, image) for a given URL. Used by the chat frontend to render link preview cards. Results are cached server-side for 1 hour.
Query Parameters
url | The URL to preview (required, must be http/https) |
Response
{
"url": "https://example.com/article",
"title": "Article Title",
"description": "A brief description...",
"image": "https://example.com/image.jpg",
"site_name": "Example"
}
Returns the full reply chain (thread) for a given message, walking backwards through reply_to references up to 20 messages deep. Messages are returned in chronological order (oldest first).
Response
{
"messages": [
{"id": 10, "username": "alice", "message": "Original message", "created_at": "..."},
{"id": 15, "username": "bob", "message": "Reply", "reply_to": 10, "created_at": "..."},
{"id": 20, "username": "alice", "message": "Reply to reply", "reply_to": 15, "created_at": "..."}
]
}
WebSocket API
Connect to the chat WebSocket for real-time messaging:
wss://portal.example.com/ws/chat
Client Messages
// Join a channel {"type": "join", "channel": "general"} // Send a message {"type": "message", "channel": "general", "message": "Hello!"} // Send a reply (reply_to is the parent message ID) {"type": "message", "channel": "general", "message": "Nice!", "reply_to": 42} // Send a message with an image (upload first via POST /api/chat/upload) {"type": "message", "channel": "general", "message": "", "image_url": "/static/uploads/chat/abc.jpg"} // Delete a message (admins/mods: any; users: own only) {"type": "delete", "message_id": 42} // Toggle emoji reaction (adds if not present, removes if present) {"type": "react", "message_id": 42, "emoji": "👍"} // Edit own message (within 5-minute window) {"type": "edit_message", "message_id": 42, "message": "Updated text"} // Pin/unpin message (moderator/admin only) {"type": "pin_message", "message_id": 42} {"type": "unpin_message", "message_id": 42} // Mark channel as read (for unread tracking) {"type": "mark_read", "message_id": 50} // Typing indicator {"type": "typing", "channel": "general"} // Block/unblock a user (hides their messages for you) {"type": "block_user", "user_id": 5} {"type": "unblock_user", "user_id": 5} // Create a poll (2-10 options, optional duration in minutes) {"type": "create_poll", "channel": "general", "question": "Favorite language?", "options": ["Python", "JavaScript", "Rust"], "allow_multiple": false, "anonymous_votes": false, "duration": 60} // Vote on a poll (option_index is 0-based) {"type": "vote_poll", "poll_id": 1, "option_index": 0} // Close a poll (creator or moderator+) {"type": "close_poll", "poll_id": 1}
Server Messages
// Channel info (after joining) {"type": "channel_info", "name": "general", "topic": "..."} // Message history {"type": "history", "messages": [...]} // New message (includes avatar, nickname, anonymous flag) {"type": "message", "id": 42, "user_id": 1, "username": "alice", "nickname": "Ali", "role": "user", "anonymous": false, "avatar": {"color": "#3b82f6", "emoji": "🚀"}, "message": "Hello!"} // Reply message (includes reply_to and reply_preview) {"type": "message", "id": 43, "user_id": 2, "username": "bob", "message": "Nice!", "reply_to": 42, "reply_preview": {"id": 42, "username": "alice", "message": "Hello!"}} // User joined/left {"type": "user_joined", "username": "bob", "role": "user", "anonymous": false} {"type": "user_left", "username": "bob"} // Online users list (with profile data) {"type": "users", "users": [{"user_id": 1, "username": "alice", "nickname": "Ali", "role": "user", "anonymous": false, "avatar": {...}}]} // Message deleted {"type": "message_deleted", "message_id": 42, "deleted_by": 1} // Channel renamed (admin action via REST) {"type": "channel_renamed", "old_name": "dev-talk", "new_name": "engineering"} // Channel deleted (admin action via REST) {"type": "channel_deleted", "channel": "old-channel"} // Reaction update (add or remove) {"type": "reaction_update", "message_id": 42, "emoji": "👍", "user_id": 1, "username": "alice", "action": "add"} // Message edited {"type": "message_edited", "message_id": 42, "new_text": "Updated message", "edited_at": "2026-02-10T12:00:00"} // Message pinned (mod/admin action) {"type": "message_pinned", "message_id": 42, "pinned_by": 1, "pinned_by_username": "alice"} // Message unpinned (mod/admin action) {"type": "message_unpinned", "message_id": 42} // Pinned messages list (sent on channel join) {"type": "pinned_messages", "messages": [{"id": 42, "username": "alice", "message": "Important info", "pinned_by": 1, "pinned_at": "..."}]} // User blocked/unblocked confirmation {"type": "user_blocked", "user_id": 5, "username": "bob"} {"type": "user_unblocked", "user_id": 5, "username": "bob"} // Poll created (includes poll data inline with the message) {"type": "poll_created", "message": {...}, "poll": {"id": 1, "question": "Favorite language?", "options": ["Python", "JavaScript", "Rust"], "votes": {}, "total_votes": 0}} // Poll updated (after a vote) {"type": "poll_updated", "poll_id": 1, "votes": {"0": 3, "1": 5, "2": 2}, "total_votes": 10} // Poll closed (by creator or moderator) {"type": "poll_closed", "poll_id": 1} // Rate limit error (5 messages per 5 seconds) {"type": "error", "message": "Slow down! You're sending messages too fast."}
DM Client Messages (via /ws/chat)
Direct message operations share the same WebSocket connection as channel chat.
// Open/load a DM conversation {"type": "dm_open", "conversation_id": 1} // Create a group DM {"type": "dm_create_group", "user_ids": [2, 3], "name": "Project Team"} // Send a DM {"type": "dm_message", "conversation_id": 1, "message": "Hello!"} // Typing indicator in DM {"type": "dm_typing", "conversation_id": 1} // Delete a DM message {"type": "dm_delete", "conversation_id": 1, "message_id": 42} // React to a DM message {"type": "dm_react", "conversation_id": 1, "message_id": 42, "emoji": "👍"} // Edit a DM message (within 5-minute window) {"type": "dm_edit", "conversation_id": 1, "message_id": 42, "message": "Updated text"} // Mark DM conversation as read {"type": "dm_mark_read", "conversation_id": 1} // Load DM history (pagination) {"type": "dm_history", "conversation_id": 1, "before_id": 100}
DM Server Messages
// Conversation opened (with participant details) {"type": "dm_conversation_opened", "conversation": {...}} // Conversations list (on connect or group creation) {"type": "dm_conversations_list", "conversations": [...]} // New DM message {"type": "dm_message", "conversation_id": 1, "id": 42, "user_id": 1, "username": "alice", "message": "Hello!", "created_at": "..."} // DM typing indicator {"type": "dm_typing", "conversation_id": 1, "user_id": 1, "username": "alice"} // DM message deleted {"type": "dm_message_deleted", "conversation_id": 1, "message_id": 42} // DM reaction update {"type": "dm_reaction_update", "conversation_id": 1, "message_id": 42, "emoji": "👍", "action": "add"} // DM message edited {"type": "dm_message_edited", "conversation_id": 1, "message_id": 42, "new_text": "Updated", "edited_at": "..."} // DM message history (paginated) {"type": "dm_history", "conversation_id": 1, "messages": [...], "has_more": true}
JavaScript Example
const ws = new WebSocket('wss://portal.example.com/ws/chat'); ws.onopen = () => { // Join the general channel ws.send(JSON.stringify({type: 'join', channel: 'general'})); }; ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'message') { console.log(`${data.username}: ${data.message}`); } }; // Send a message ws.send(JSON.stringify({ type: 'message', channel: 'general', message: 'Hello everyone!' }));
Voice Chat
Live voice chat using WebRTC P2P mesh (2-10 users per channel). The server acts purely as a signaling relay — no audio processing or storage. Audio is encrypted via WebRTC DTLS-SRTP. Voice signaling piggybacks on the existing /ws/chat WebSocket.
Returns STUN and optional TURN server configuration for WebRTC peer connections.
Response
{
"ice_servers": [
{"urls": "stun:stun.l.google.com:19302"},
{"urls": "turn:turn.example.com:3478", "username": "user", "credential": "pass"}
]
}
Voice Signaling (via /ws/chat)
Voice commands are sent as JSON messages on the existing chat WebSocket. Supported client-to-server message types:
| Type | Fields | Description |
|---|---|---|
| voice_join | room (optional) | Join voice in the current text channel, or pass room: "dm:{conversation_id}" for DM voice |
| voice_leave | — | Leave voice chat |
| voice_signal | target_user_id, signal | Forward WebRTC offer/answer/ICE candidate to a specific user |
| voice_mute | muted (bool) | Toggle microphone mute state |
| voice_deafen | deafened (bool) | Toggle deafen (mute all incoming audio) |
| voice_speaking | speaking (bool) | Voice activity indicator (rate-limited to 1/100ms) |
Voice Modes
Users can choose between two voice activation modes (stored in browser localStorage):
- VAD (Voice Activity Detection) — Automatically detects speech using AudioContext analysis with configurable dBFS threshold
- PTT (Push-to-Talk) — Transmit only while holding a configurable key (default: Space)
Direct Messages
Private messaging between users with support for 1:1 conversations and group DMs (up to 10 participants). All DM messages are encrypted at rest using AES-256 (Fernet), the same encryption used for channel messages.
REST API
Returns all DM conversations for the authenticated user, ordered by most recent activity. Includes the last message preview and unread count for each conversation.
Response
{
"conversations": [
{
"id": 1,
"type": "direct",
"name": null,
"participants": [
{"user_id": 1, "username": "alice"},
{"user_id": 2, "username": "bob"}
],
"last_message": {
"message": "Hey, are you around?",
"username": "alice",
"created_at": "2026-02-11T14:30:00"
},
"unread_count": 2,
"muted": false,
"created_at": "2026-02-11T10:00:00"
},
{
"id": 2,
"type": "group",
"name": "Project Team",
"participants": [
{"user_id": 1, "username": "alice"},
{"user_id": 2, "username": "bob"},
{"user_id": 3, "username": "charlie"}
],
"last_message": {
"message": "Meeting at 3pm",
"username": "charlie",
"created_at": "2026-02-11T13:00:00"
},
"unread_count": 0,
"muted": false,
"created_at": "2026-02-10T09:00:00"
}
]
}
Create a new DM conversation. For 1:1 conversations, provide user_id. For group DMs, provide user_ids array and an optional name. If a 1:1 conversation already exists with the given user, the existing conversation is returned.
Request Body (1:1)
| Parameter | Type | Description |
|---|---|---|
| user_id | integer | Target user ID required |
Request Body (Group)
| Parameter | Type | Description |
|---|---|---|
| user_ids | array | Array of user IDs to include (max 10 total participants) required |
| name | string | Group conversation name required |
Response
{
"id": 3,
"type": "direct",
"name": null,
"participants": [
{"user_id": 1, "username": "alice"},
{"user_id": 4, "username": "dave"}
],
"created_at": "2026-02-11T15:00:00"
}
Returns details for a specific DM conversation. Only accessible by conversation participants.
Response
{
"id": 1,
"type": "direct",
"name": null,
"participants": [
{"user_id": 1, "username": "alice", "nickname": "Ali", "role": "user", "avatar": {"color": "#3b82f6", "emoji": "🚀"}},
{"user_id": 2, "username": "bob", "nickname": null, "role": "admin", "avatar": {"color": "#ef4444", "emoji": "🔥"}}
],
"muted": false,
"created_at": "2026-02-11T10:00:00"
}
Returns messages for a DM conversation with pagination support. Messages are returned in reverse chronological order (newest first). Only accessible by conversation participants.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| limit | integer | Number of messages to return (default: 100) |
| before_id | integer | Return messages before this message ID (for pagination) |
Response
{
"messages": [
{
"id": 150,
"user_id": 1,
"username": "alice",
"nickname": "Ali",
"avatar": {"color": "#3b82f6", "emoji": "🚀"},
"message": "Hey, are you around?",
"created_at": "2026-02-11T14:30:00"
},
{
"id": 149,
"user_id": 2,
"username": "bob",
"nickname": null,
"avatar": {"color": "#ef4444", "emoji": "🔥"},
"message": "Sure, what's up?",
"created_at": "2026-02-11T14:29:00"
}
],
"has_more": true
}
Mute or unmute a DM conversation. Muted conversations do not generate notifications. Only accessible by conversation participants.
Request Body
| Parameter | Type | Description |
|---|---|---|
| muted | boolean | Set to true to mute, false to unmute required |
Response
{
"success": true,
"muted": true
}
Leave a group DM conversation. Only available for group conversations (not 1:1). The conversation remains for other participants. Only accessible by conversation participants.
Response
{
"success": true
}
Add users to an existing group DM conversation. Only available for group conversations. Total participants cannot exceed 10. Only accessible by existing conversation participants.
Request Body
| Parameter | Type | Description |
|---|---|---|
| user_ids | array | Array of user IDs to add (max 10 total participants) required |
Response
{
"success": true,
"participants": [
{"user_id": 1, "username": "alice"},
{"user_id": 2, "username": "bob"},
{"user_id": 3, "username": "charlie"},
{"user_id": 5, "username": "eve"}
]
}
Message Search
Full-text search across chat channel messages and DM conversations. Powered by SQLite FTS5 for fast, ranked results. Search respects access control — users can only find messages in channels they have access to and DM conversations they participate in.
Search across channel messages and DM conversations. Rate limited to 10 requests per minute per user.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| q | string | Search query (2-200 characters) required |
| scope | string | Search scope: all (default), channels, or dms |
| channel_id | integer | Limit search to a specific channel |
| conversation_id | integer | Limit search to a specific DM conversation |
| from | string | Filter by sender username |
| has | string | Filter by content type: image |
| before | string | Filter messages before this date (ISO 8601) |
| after | string | Filter messages after this date (ISO 8601) |
| limit | integer | Results per page (default: 25, max: 50) |
| offset | integer | Pagination offset (default: 0) |
Response
{
"results": [
{
"id": 42,
"source": "channel",
"channel_id": 1,
"channel_name": "general",
"user_id": 1,
"username": "alice",
"message": "Has anyone tried the new deployment script?",
"created_at": "2026-02-11T12:00:00"
},
{
"id": 88,
"source": "dm",
"conversation_id": 3,
"conversation_name": "bob",
"user_id": 2,
"username": "bob",
"message": "I deployed it yesterday, works great",
"created_at": "2026-02-11T12:05:00"
}
],
"total": 2,
"limit": 25,
"offset": 0
}
Notifications
Real-time notification system for stream events, service alerts, and security events. Notifications are delivered via polling and displayed in the navbar bell icon.
Returns notifications for the authenticated user with an unread count.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| unread | string | Set to 1 to return only unread notifications |
Response
{
"notifications": [
{
"id": 1,
"type": "stream_live",
"title": "Stream Live",
"message": "alice started streaming",
"is_read": false,
"created_at": "2026-02-08T12:00:00"
}
],
"unread_count": 3
}
Mark a single notification as read. Only affects notifications owned by the authenticated user.
Response
{
"success": true
}
Mark all notifications as read for the authenticated user.
Response
{
"success": true,
"count": 5
}
User Blocking
Block other users to hide their messages and prevent them from sending you DMs. Blocking is bidirectional for DMs — neither party can message the other while a block is active.
Returns the list of users you have blocked. Scoped to the authenticated user.
Response
{
"blocked_users": [
{"id": 5, "username": "bob", "blocked_at": "2026-02-13T10:00:00"}
]
}
Block a user by their ID. You cannot block yourself. Blocked users' messages are hidden in chat and DMs are prevented bidirectionally.
Response
{
"success": true
}
Remove a user from your block list.
Response
{
"success": true
}
WebSocket Integration
Blocking can also be managed via the /ws/chat WebSocket. Send block_user or unblock_user messages and receive confirmation events. Blocked users' messages are filtered client-side.
Polls
Create inline polls in chat channels. Polls support 2-10 options, optional multi-vote, anonymous voting, and time limits. All poll operations use the /ws/chat WebSocket.
Poll Constraints
| Field | Limit |
|---|---|
| Question length | 500 characters max |
| Option count | 2-10 options |
| Option length | 200 characters max per option |
| Duration | Optional, in minutes (poll auto-closes when expired) |
WebSocket Messages
Polls are created, voted on, and closed via the chat WebSocket. See the Chat WebSocket API section for the full message format. Key message types:
| Client Type | Description | Auth |
|---|---|---|
create_poll | Create a new poll in a channel | Any user (timeout/mute checked) |
vote_poll | Cast a vote (option_index is 0-based) | Any user |
close_poll | Close a poll early | Poll creator or Moderator+ |
| Server Type | Description |
|---|---|
poll_created | New poll broadcast with message and poll data |
poll_updated | Updated vote counts after a vote |
poll_closed | Poll has been closed |
Channel Permissions
Control channel visibility and send permissions. Channels can be public (visible to all) or private (members only), and open (everyone can send) or readonly (only moderators and channel moderators can send).
Channel Properties
| Field | Values | Description |
|---|---|---|
visibility | public / private | Who can see and join the channel |
mode | open / readonly | Who can send messages |
Chat User Search
Lightweight user search accessible to all authenticated users. Returns basic info only (id, username, nickname). Used for DM creation and channel member management.
Query Parameters
q | Search query (matches username or nickname, case-insensitive) |
Response
{
"users": [
{"id": 2, "username": "alice", "nickname": "Ali"},
{"id": 3, "username": "bob", "nickname": null}
]
}
Channel Members
List members of a channel. Visible to channel members and moderators+.
Response
{
"members": [
{"user_id": 2, "username": "alice", "role": "moderator", "added_at": "2026-02-13T10:00:00"},
{"user_id": 3, "username": "bob", "role": "member", "added_at": "2026-02-13T10:05:00"}
]
}
Add a user as a member of a private channel. Requires admin, channel creator, or channel moderator role.
Request Body
{
"user_id": 5
}
Response
{
"success": true
}
Change a channel member's role (member or moderator). Requires admin or channel creator.
Request Body
{
"role": "moderator"
}
Remove a user from a private channel. Requires admin, channel creator, or channel moderator role.
Response
{
"success": true
}
Webhooks
Webhooks let you receive real-time HTTP notifications when events happen in your portal (outgoing), or post messages into chat channels from external services (incoming). Outgoing webhooks sign payloads with HMAC-SHA256 for verification. Webhook URLs, secrets, and delivery log payloads are encrypted at rest. Admin+ required for management.
Create an outgoing or incoming webhook. Maximum 25 webhooks per user. The webhook secret is returned only once on creation.
Request Body
| Parameter | Type | Description |
|---|---|---|
| name | string | Webhook name (1-100 chars) required |
| type | string | outgoing or incoming required |
| url | string | Delivery URL (outgoing only, HTTPS required, SSRF-protected) required for outgoing |
| events | array | Events to subscribe to (outgoing only): message.created, message.deleted, stream.live, stream.offline, user.joined, user.connected, user.disconnected required for outgoing |
| channel_id | integer | Target channel for incoming webhook messages required for incoming |
Response
{
"id": 1,
"name": "My Webhook",
"type": "outgoing",
"url": "https://example.com/hook",
"secret": "abc123...",
"events": ["message.created"],
"enabled": true,
"token": null
}
Note: The secret field is only returned on creation. Store it securely for HMAC verification.
List your webhooks. Admins see all webhooks with owner usernames. Secrets are never returned in list responses.
{
"webhooks": [
{
"id": 1,
"name": "My Webhook",
"type": "outgoing",
"url": "https://example.com/hook",
"has_secret": true,
"events": ["message.created"],
"enabled": true,
"consecutive_failures": 0,
"created_at": "2026-02-19 12:00:00"
}
]
}
Get details for a specific webhook. Owner or admin required.
Update webhook name, URL, events, channel, or enabled status. URL changes are SSRF-validated. Owner or admin required.
Request Body
| Parameter | Type | Description |
|---|---|---|
| name | string | New name |
| url | string | New delivery URL (outgoing only) |
| events | array | Updated event list |
| channel_id | integer | New target channel (incoming only) |
| enabled | boolean | Enable/disable webhook |
Delete a webhook and all its delivery logs. Owner or admin required.
View delivery logs for a webhook. Includes request payload, response status, success/failure, attempt number, and duration. Logs are encrypted at rest and auto-cleaned after 7 days.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| limit | integer | Max results (default 50, max 100) |
| offset | integer | Pagination offset |
Send a webhook.test event to verify your webhook endpoint is working. Owner or admin required.
Generate a new HMAC signing secret. The new secret is returned only once. The old secret is immediately invalidated.
Post a message to a chat channel via an incoming webhook. No authentication required — the token in the URL acts as the credential. Rate limited to 10 messages per 60 seconds per token.
Request Body
| Parameter | Type | Description |
|---|---|---|
| content | string | Message content (1-4000 chars) required |
| username | string | Display name override (max 50 chars, defaults to webhook name) |
Outgoing Webhook Payload Format
Outgoing webhooks receive a signed JSON POST with this structure:
{
"event": "message.created",
"timestamp": "2026-02-19T12:00:00Z",
"data": {
"message_id": 123,
"channel_id": 1,
"channel_name": "general",
"user_id": 10,
"username": "alice",
"content": "Hello world"
}
}
Headers:
X-Portal-Signature: sha256=abc123...
X-Portal-Event: message.created
Content-Type: application/json
Verify the signature by computing HMAC-SHA256(secret, request_body) and comparing with the X-Portal-Signature header value.
Embedded Browser
Portal's embedded browser provides a tabbed browsing experience for HTTP/HTTPS connections (http_proxy plugin type). It includes navigation controls (back, forward, refresh), an address bar, tabbed browsing (Ctrl+T/W/Tab), and multi-site navigation for browser-mode connections. A default "Web Browser" connection with DuckDuckGo as homepage is created for all users. Connection IDs use opaque string tokens.
Opens the embedded browser page for browsing a remote web service through Portal. Requires authentication. The connection must use the http_proxy plugin type (includes http, https, http_proxy, and all web panel connection types such as Home Assistant, Grafana, Portainer, etc.). Connections with browser_mode: true support multi-site navigation to any website.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| connection | string | Connection ID (opaque token) required |
Notes
- This is the user-facing page for HTTP/HTTPS connections. When users click Connect on a web-type connection, they are directed here.
- Supports tabbed browsing (Ctrl+T new tab, Ctrl+W close, Ctrl+Tab switch), keyboard shortcuts (Alt+arrows, Ctrl+L, Ctrl+R), and middle-click to close tabs.
- Browser-mode connections can navigate to any website. The address bar accepts full URLs, bare hostnames (auto-prefixed with https://), and relative paths.
- All proxied traffic is routed internally through
/proxy/{connection_id}/{path}. Users should use/browser?connection={id}rather than accessing/proxy/directly. - The embedded browser rewrites links and intercepts navigation (forms, fetch, XHR, WebSocket, Location.assign/replace, history API) to keep all traffic within Portal's proxy layer.
Proxies HTTP requests to the backend service configured for the given connection. This endpoint is used internally by the embedded browser and should not be accessed directly by users. Use /browser?connection={id} instead for the full browsing experience with navigation controls.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| connection_id | string | Connection ID (opaque token) required |
| path | string | Path to proxy to the backend service |
Behavior
- Forwards all HTTP methods (GET, POST, PUT, DELETE, etc.) to the backend
- Proxies WebSocket upgrade requests for real-time features
- Enforces TLS for connections to HTTPS backends
- In browser mode, the path encodes the full target URL (e.g.,
/proxy/{id}/https://example.com/page) for multi-site navigation - Localhost and private IPs are blocked for browser-mode targets
- Portal session cookies and Authorization headers are stripped from upstream requests
- Requires authentication — only the connection owner can access their proxy routes
SFTP Browser
Browse and manage files on remote servers via your existing SSH/SFTP connections. Per-user access -- only connections you own are accessible. Ephemeral connections are opened per-request.
List remote directory contents.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| path | string | Remote directory path (default: /) |
Read remote text file for editing. Maximum file size: 5MB.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| path | string | Remote file path required |
Download a remote file as a streaming attachment.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| path | string | Remote file path required |
Upload a file to the remote server via multipart form data.
Form Fields
| Parameter | Type | Description |
|---|---|---|
| path | string | Destination directory path required |
| file | file | File to upload required |
Request Body
| Parameter | Type | Description |
|---|---|---|
| path | string | Remote file path required |
| content | string | File content required |
Request Body
| Parameter | Type | Description |
|---|---|---|
| path | string | Remote directory path to create required |
Request Body
| Parameter | Type | Description |
|---|---|---|
| old_path | string | Current remote path required |
| new_path | string | New remote path required |
Delete a remote file or empty directory.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| path | string | Remote path to delete required |
SFTP VOD Storage
Manage VOD files on your configured SFTP VOD storage server. These endpoints operate on your personal VOD storage connection (configured via the VOD Storage settings in the dashboard). Any authenticated user with VOD storage configured can use these endpoints.
List contents of a directory on the VOD SFTP storage.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| path | string | Remote directory path (default: /) |
Read a text file from VOD SFTP storage.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| path | string | Remote file path required |
Download a file from VOD SFTP storage as a streaming attachment.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| path | string | Remote file path required |
Request Body
| Parameter | Type | Description |
|---|---|---|
| path | string | Remote directory path to create required |
Upload a file to VOD SFTP storage via multipart form data.
Form Fields
| Parameter | Type | Description |
|---|---|---|
| path | string | Destination directory path required |
| file | file | File to upload required |
Request Body
| Parameter | Type | Description |
|---|---|---|
| path | string | Remote file path required |
| content | string | File content required |
Request Body
| Parameter | Type | Description |
|---|---|---|
| old_path | string | Current remote path required |
| new_path | string | New remote path required |
Delete a file or empty directory from VOD SFTP storage.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| path | string | Remote path to delete required |
System Health ADMIN
Detailed system resource monitoring including CPU, memory, disk, and Portal process metrics.
Activity Feed ADMIN
Recent activity log. Admins see all activity; regular users see only their own actions.
Returns recent activity entries. Admins see all activity; regular users see only their own.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| limit | integer | Max entries to return, 1-50 (default: 20) |
Response
{
"activities": [
{
"id": 1,
"user_id": 10,
"username": "john",
"action": "stream_started",
"details": "Started streaming",
"created_at": "2026-02-08T12:00:00"
}
]
}
Certificate Management ADMIN
Manage TLS certificates via the admin panel or API. Supports custom uploads, self-signed generation, and Let's Encrypt automation.
Server Settings ADMIN
Data Retention ADMIN
Configure automatic cleanup policies for chat messages, notifications, activity logs, and service logs. Protects against database bloat while preserving recent data.
Audit Log MODERATOR+
View a chronological record of all moderation actions performed on the server. Includes message deletions, user timeouts/mutes/bans, channel operations, role changes, and automod triggers. Available to moderators and above.
Auto-Moderation ADMIN
Configure automatic message filtering rules that run before messages are saved. Moderators and above bypass all automod rules. Triggers are logged to the audit log.
Rate Limits
API requests are rate limited to prevent abuse:
- Authentication endpoints: 10 requests per minute
- General API: 60 requests per minute
- WebSocket connections: 10 concurrent per user
- Chat messages: 5 messages per 5 seconds per user
- Outgoing webhooks: 30 deliveries per webhook per 60 seconds
- Incoming webhooks: 10 messages per token per 60 seconds
Rate limit headers are included in responses:
X-RateLimit-Limit: 60 X-RateLimit-Remaining: 58 X-RateLimit-Reset: 1704067200
Error Codes
| Code | Description |
|---|---|
| 400 | Bad Request - Invalid parameters |
| 401 | Unauthorized - Missing or invalid authentication |
| 403 | Forbidden - Insufficient permissions or blocked action |
| 404 | Not Found - Resource doesn't exist |
| 429 | Too Many Requests - Rate limit exceeded |
| 500 | Internal Server Error |