Remedia

WebSocket RPC protocol

Authentication, version 1 JSON messages, commands, errors, and lifecycle events.

Connect to ws://<host>:<port>/v1/ws or wss://.../v1/ws. The server accepts standard WebSocket URLs. The @remedia/client convenience schemes remedia:// and remedias:// map to those URLs and default to /v1/ws. Authenticate and identify the bot in the WebSocket HTTP upgrade:

Authorization: Basic base64(username:password)
X-Remedia-Bot-Id: 100000000000000001

The bot header must contain a positive decimal Discord snowflake that fits u64. The server binds it to the client session created or resumed on that socket. Reconnecting to resume a session must use the same bot ID. This value is supplied at runtime, not configured on the server, so separate bots can use the same server account. Credentials in query parameters are not supported. Authentication and bot identity validation happen before allocating client, player, or voice resources. Invalid Basic credentials receive HTTP 401. A missing or malformed bot header receives HTTP 400.

Connect by hand

@remedia/client does all of this for you. A hand-written client opens a WebSocket with the handshake headers above, then creates a client session. Install ws for this standalone example (npm install ws) and set the password and bot user ID in its environment:

import { WebSocket } from 'ws';

const username = process.env.REMEDIA_RPC_USERNAME ?? 'media-bot';
const password = process.env.REMEDIA_RPC_PASSWORD;
const botUserId = process.env.DISCORD_BOT_USER_ID;
if (!password || !botUserId) throw new Error('Set REMEDIA_RPC_PASSWORD and DISCORD_BOT_USER_ID');

const socket = new WebSocket('ws://127.0.0.1:8787/v1/ws', {
  headers: {
    Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`,
    'X-Remedia-Bot-Id': botUserId,
  },
});

socket.on('open', () => {
  socket.send(
    JSON.stringify({ version: 1, id: 'create-client-1', command: 'client.create', params: {} }),
  );
});

The response carries an opaque sessionId and resumeCredential. The client session owns its guild sessions (guild.create), and each guild session owns one player and an optional Discord voice transport. After an unexpected disconnect, client.resume reclaims the session within the configured resumption window. Events are not replayed while detached, and a client that falls behind receives client.events_lost. To play audio, send Gateway voice credentials to voice.connect, then player.play:

{
  "version": 1,
  "id": "play-1",
  "command": "player.play",
  "params": {
    "guildId": "200000000000000001",
    "source": { "type": "url", "url": "https://media.example.com/audio.ogg" }
  }
}

Playback without a voice transport uses a null output for validation and does not produce audible sound. Raw FFmpeg graphs (player.filter.set) are trusted configuration. Do not expose them to untrusted users.

Request and response

Every request is one text WebSocket frame containing a strict JSON object. Unknown top-level and command parameter fields are rejected.

{ "version": 1, "id": "request-1", "command": "node.info", "params": {} }

Responses retain the request ID:

{
  "version": 1,
  "id": "request-1",
  "ok": true,
  "result": { "nodeId": "...", "version": "0.1.0", "protocolVersion": 1 }
}
{
  "version": 1,
  "id": "request-2",
  "ok": false,
  "error": { "code": "invalid_state", "message": "..." }
}

The server accepts protocol version 1, requires a 1–128 byte request ID and command name, and requires params to be an object. Binary frames are rejected. WebSocket frame and message sizes, in-flight requests, per-connection request rate, and request duration are bounded by configuration.

Error codes

CodeMeaning
invalid_requestMalformed JSON/protocol message, unknown command, invalid parameters, or denied media source
authentication_failedHandshake authentication failed (HTTP 401)
unknown_sessionSession or guild is missing or not owned by this connection
invalid_stateOperation conflicts with the current session/player state
resource_exhaustedConfigured connection, session, player, request, or event capacity was reached
request_timeoutOperation did not produce a response before its configured deadline. Started cleanup/native work may continue safely
unsupportedRequested media operation, such as seeking, is unsupported for this source
playback_failedMedia open, decoder, resampler, filter, encoder, or playback worker failed
cancelledA pending plugin operation was cancelled by stop, replacement, staging changes, plugin.cancel, disconnect, session close, or shutdown
stale_generationA staging request or result belongs to a source generation that is no longer current
plugin_*Plugin-host failures: see Playing audio through plugins and forwarding
invalid_bot_user_idMissing or malformed bot identity during the HTTP upgrade (HTTP 400)
media_failedDisposable media probing failed to open or inspect the source
transport_failedDiscord voice connect, disconnect, or resource cleanup failed

Messages are safe summaries and do not include raw media URLs, passwords, or voice tokens. Inspect server diagnostics for operational context.

Lifecycle events

Events are separate text frames and do not have request IDs:

{
  "version": 1,
  "event": "player.completed",
  "sessionId": "...",
  "sequence": 12,
  "data": { "guildId": "200000000000000001", "generation": 3 }
}

Implemented event names include guild.created, guild.destroyed, player.started, player.paused, player.resumed, player.seeked, player.stopped, player.filter_changed, player.volume_changed, player.completed, player.failed, voice.state_changed, voice.failed, and client.events_lost.

A successful player.play response and every player.state snapshot include the same monotonically increasing source generation as that playback's player.started, player.completed, and player.failed events. Consumers can bind waiters to a generation so a later source cannot complete an older waiter. The source generation is independent of transport replacement generations.

Events are bounded and are not replayed while the client is detached. A slow consumer can receive client.events_lost with a dropped-event count. client.resume, guild.list, guild.get, and player.state provide authoritative snapshots after reconnect.

Session resumption

client.create returns an opaque sessionId and high-entropy resumeCredential. Store the credential securely. On unexpected socket loss, the server keeps resources for sessionResumptionTimeoutSeconds. The same authenticated username may call client.resume with both values while detached and before expiry. The credential stays stable until the session closes. This allows a client to retry if a resume response was lost. A concurrent socket cannot control an already attached session.

client.close releases resources immediately. Expiry and graceful shutdown do the same. Active sessions are in-memory only and are lost when the server process restarts.

Server build identity

node.info and node.stats report version from the release metadata embedded at build time (including prerelease suffixes), and commit with the Git SHA or unknown for a source archive without supplied metadata. These match the CLI build identity.

Detailed references

On this page