Gryt

API Reference

WebSocket events, REST endpoints, and data structures

API reference for the Gryt signaling server. Authentication uses Keycloak OIDC via auth.gryt.chat and a challenge-response flow with identity certificates from id.gryt.chat. There are no custom auth REST endpoints on the server itself.

REST Endpoints

All REST endpoints are served by the signaling server (default port 5000). Endpoints marked Bearer require an Authorization: Bearer <accessToken> header.

Public

MethodPathDescription
GET/healthHealth check. Returns { status, service, serverName, timestamp }
GET/metricsPrometheus metrics (text/plain)
GET/infoPublic server preview: { name, description, members, version, lanOpen }
GET/iconServer icon image (404 if none set)
GET/.well-known/jwks.jsonJWKS for built-in identity verification

Messages

MethodPathAuthDescription
GET/api/messages/:conversationIdBearerFetch messages. Query: ?limit= (max 200), ?before= (ISO date)

Messages are created via the WebSocket chat:send event, not via REST.

Uploads

MethodPathAuthDescription
POST/api/uploadsBearerUpload a file (multipart file)
POST/api/uploads/avatarBearerUpload avatar image
DELETE/api/uploads/avatarBearerRemove avatar
GET/api/uploads/files/:fileIdNoDownload file. ?thumb=1 for thumbnail, ?download=1 for attachment header

Server

MethodPathAuthDescription
POST/api/server/iconBearerUpload server icon (owner only, multipart file)
DELETE/api/server/iconBearerRemove server icon (owner only)

Members

MethodPathAuthDescription
GET/api/membersBearerList members: { items: [{ serverUserId, nickname, lastSeen }] }

Emojis

MethodPathAuthDescription
GET/api/emojisNoList all emojis: [{ name, file_id }]
GET/api/emojis/img/:nameNoEmoji image bytes (cached)
POST/api/emojisBearerUpload emoji (admin/owner). Multipart file + name, or files + names JSON for batch
PATCH/api/emojis/:nameBearerRename emoji (admin/owner). Body: { name }
DELETE/api/emojis/:nameBearerDelete emoji (admin/owner)

BetterTTV Import

MethodPathAuthDescription
GET/api/emojis/bttv/user/:userIdNoFetch channel/shared emotes for a BTTV user
GET/api/emojis/bttv/emote/:emoteIdNoBTTV emote metadata
GET/api/emojis/bttv/file/:emoteIdNoBTTV emote image bytes
POST/api/emojis/bttv/importBearerImport BTTV emotes as server emojis (admin/owner, max 200 per request)
MethodPathAuthDescription
GET/api/link-preview?url=BearerFetch Open Graph metadata for a URL
GET/api/oembed?url=BearerFetch oEmbed data (X/Twitter, SoundCloud, Spotify, TikTok)
GET/api/media/metadata?url=BearerFetch media dimensions and MIME type

WebSocket Events (Socket.IO)

Connection and Join Flow

Client → Server

EventPayloadDescription
server:infoRequest public server preview
server:join{ nickname?, inviteCode? }Start join flow; server responds with server:challenge
server:verify{ certificate, assertion }Complete join with signed identity assertion
server:leaveLeave the server
server:detailsRequest full server state
token:refresh{ refreshToken? } or { accessToken? }Refresh access token

Server → Client

EventPayloadDescription
server:info{ name, description, members, version, lanOpen }Public preview
server:challenge{ nonce, serverHost }Identity challenge (response to server:join)
server:joined{ accessToken, refreshToken, nickname, avatarFileId, isOwner, setupRequired }Join success
server:error{ error, message?, retryAfterMs?, canReapply? }Error (e.g. invite_required, invalid_invite, rate_limited)
server:detailsFull server stateChannels, sidebar, clients, SFU hosts, STUN config
server:left{ message }Leave confirmation
token:refreshed{ accessToken }New access token
token:error{ error, message? }Token error
token:revoked{ reason, message }Session revoked

Voice

Client → Server

EventPayloadDescription
voice:room:requestroomIdRequest SFU room credentials
voice:stream:setstreamIDSet active stream (empty string to clear)
voice:state:update{ isMuted, isDeafened, isAFK }Update voice state flags
voice:channel:joinedbooleanNotify channel join/leave
voice:camera:state{ enabled, streamId? }Update camera state
voice:screen:state{ enabled, videoStreamId?, audioStreamId? }Update screen share state
voice:peer:connectedstreamIdNotify peer stream connected
voice:peer:disconnectedstreamIdNotify peer stream disconnected
voice:latency:report{ estimatedOneWayMs?, networkRttMs?, jitterMs?, codec?, bitrateKbps? }Report latency metrics

Server → Client

EventPayloadDescription
voice:room:granted{ room_id, join_token, sfu_url, sfu_urls?, timestamp }SFU room credentials
voice:room:errorstring or { error, message?, retryAfterMs? }Voice join error
voice:peer:joined{ clientId, nickname, channelId }Peer joined voice
voice:peer:left{ clientId, nickname, channelId }Peer left voice
voice:kicked{ reason }Force disconnected from voice
voice:device:disconnect{ type, message }Device switch notification
voice:latency:update{ clientId, latency }Peer latency update

Chat

Client → Server

EventPayloadDescription
chat:send{ conversationId, accessToken, text?, attachments?, replyToMessageId?, nonce? }Send a message
chat:fetch{ conversationId, limit?, before? }Fetch message history
chat:react{ conversationId, messageId, reactionSrc, accessToken }Toggle reaction
chat:edit{ conversationId, messageId, text, accessToken }Edit a message
chat:delete{ conversationId, messageId, accessToken }Delete a message

Server → Client

EventPayloadDescription
chat:newMessageRecordNew message broadcast
chat:history{ conversation_id, items, hasMore, before? }Message history response
chat:reactionMessageRecordReaction update broadcast
chat:editedMessageRecordEdited message broadcast
chat:deleted{ conversation_id, message_id }Deletion broadcast
chat:error{ error, retryAfterMs?, message }Chat error (including rate limits)

Admin Events

Admin events require accessToken in the payload and appropriate role (admin or owner).

Event (Client → Server)Description
server:settings:updateUpdate server settings (owner). Fields: displayName, description, avatarMaxBytes, uploadMaxBytes, emojiMaxBytes, profanityMode, profanityCensorStyle, systemChannelId, lanOpen
server:invites:createCreate invite code
server:invites:revokeRevoke invite code
server:kickKick a user
server:banBan a user
server:unbanUnban a user
server:roles:setSet user role (owner)
server:channels:upsertCreate or update a channel
server:channels:deleteDelete a channel
server:channels:reorderReorder channels
server:sidebar:item:upsertCreate or update sidebar item
server:sidebar:item:deleteDelete sidebar item
server:sidebar:reorderReorder sidebar
voice:disconnect:userForce disconnect a user from voice

SFU WebSocket Protocol

The SFU uses raw WebSocket (not Socket.IO). All messages use a JSON envelope:

{ "event": "<event_name>", "data": "<json_string>" }

Client → SFU

EventDataDescription
client_join{ room_id, server_id, server_password, user_token, user_id }Join a voice room
answerSDP answer stringWebRTC answer
candidateRTCIceCandidateInit JSONICE candidate
renegotiateRequest renegotiation (e.g. to add tracks)
keep_alive{ timestamp }Keep-alive ping (every 15s)

SFU → Client

EventDataDescription
room_joinedJoin success
room_errorError stringJoin or room error
offerSDP offer stringWebRTC offer
candidateRTCIceCandidateInit JSONICE candidate

SFU HTTP Endpoints

PathMethodDescription
/healthGETHealth check: { status, service, version, timestamp }
/metricsGETPrometheus metrics

Data Types

MessageRecord

interface MessageRecord {
  conversation_id: string;
  message_id: string;
  sender_server_id: string;
  text: string;
  created_at: string;
  edited_at?: string;
  attachments?: string[];
  reactions?: Reaction[];
  reply_to_message_id?: string;
  sender_nickname?: string;
  sender_avatar_file_id?: string;
  enriched_attachments?: EnrichedAttachment[];
}

Channel

interface ServerChannelRecord {
  channel_id: string;
  name: string;
  type: "text" | "voice";
  position: number;
  description?: string;
  require_push_to_talk?: boolean;
  disable_rnnoise?: boolean;
  max_bitrate?: number;
  esports_mode?: boolean;
  text_in_voice?: boolean;
}

Server Roles

Roles in order of permission: owner > admin > mod > member.

Error Format

Errors are emitted as string-based codes, not numeric. Common error values:

  • invite_required — server is invite-only
  • invalid_invite — invite code is wrong or expired
  • rate_limited — too many requests (includes retryAfterMs)
  • auth_required — missing or invalid access token
  • token_invalid — access token is expired or malformed
  • not_owner / not_admin — insufficient permissions

On this page