Gryt

API Reference

Every REST endpoint and Socket.IO event the server answers

Two surfaces. REST handles uploads, downloads and a few things that want plain HTTP caching. Everything interactive runs over Socket.IO: chat, presence, voice signalling, moderation, settings.

There are no authentication endpoints here. Identity comes from an account issuer or from a keypair on the device, and joining is a signed challenge over the socket. See Identity.

REST

Default port 5000. Endpoints marked Bearer want Authorization: Bearer <accessToken>.

Public

MethodPathReturns
GET/health{ status, service, serverName, timestamp }
GET/infoThe join preview. See below
GET/iconThe server icon. 404 when none is set
GET/metricsPrometheus, text/plain

What /info returns

{
  "serverId": "...",
  "name": "...",
  "description": "...",
  "members": "12",
  "lanOpen": false,
  "identityTiers": ["account"],
  "joinPolicy": "invite"
}

identityTiers and joinPolicy are deliberately unauthenticated. The point is that a client can say "you do not need an account for this one" before anybody tries, and neither field tells a caller anything they could not learn by attempting the join and reading the refusal.

Two things depend on who is asking. A server whose discoverable setting is off answers 404 to anybody who is not already a member, so it disappears rather than advertising itself. And version is only included for members, because a precise build number lets anyone on the network scan for hosts running a version with a known vulnerability, and this endpoint has to stay open for the add-server flow.

No JWKS endpoint

The server does not publish /.well-known/jwks.json. It fetches one, from the identity issuer it is configured to trust, and caches it. Nothing here serves keys.

Messages

MethodPathAuthNotes
GET/api/messages/:conversationIdBearer?limit= up to 200, ?before= an ISO date

Sending is chat:send over the socket. There is no POST here.

Uploads

MethodPathAuthNotes
POST/api/uploadsBearermultipart file
POST/api/uploads/avatarBearermultipart
DELETE/api/uploads/avatarBearer
GET/api/uploads/files/:fileId?thumb=1 for the thumbnail, ?download=1 for an attachment header

Server

MethodPathAuthNotes
POST/api/server/iconBearerOwner only, multipart file
DELETE/api/server/iconBearerOwner only

GRYT_SERVER_ICON_MAX_MB caps the upload, 25 MB by default. PNG, JPEG, WebP, GIF, AVIF and SVG are accepted; SVG is sanitised before it is stored.

Members

MethodPathAuthReturns
GET/api/membersBearer{ items: [{ serverUserId, nickname, lastSeen }] }

Emojis

MethodPathAuthNotes
GET/api/emojis[{ name, file_id }]
GET/api/emojis/allIncluding staged
GET/api/emojis/img/:nameImage bytes, cached
POST/api/emojisBearerAdmin or owner. file + name, or files + a names JSON array
PATCH/api/emojis/:nameBearerRename. Body { name }
DELETE/api/emojis/:nameBearer
GET/api/emojis/queueBearerThe processing queue
POST/api/emojis/stageBearerStage emojis before committing them

Importing emojis

From BetterTTV:

MethodPathAuth
GET/api/emojis/bttv/user/:userId
GET/api/emojis/bttv/emote/:emoteId
GET/api/emojis/bttv/file/:emoteId
POST/api/emojis/bttv/importBearer, admin or owner, 200 per request

From emoji.gg:

MethodPathAuth
GET/api/emojis/emojigg/user/:username
GET/api/emojis/emojigg/pack/:slug
GET/api/emojis/emojigg/emoji/:slug
GET/api/emojis/emojigg/file

The file routes fetch the image server-side and return the bytes, so the browser is not the thing talking to BetterTTV or emoji.gg.

MethodPathAuthNotes
GET/api/link-preview?url=BearerOpen Graph metadata
GET/api/oembed?url=BearerX, SoundCloud, Spotify, TikTok
GET/api/media/metadata?url=BearerDimensions and MIME type

Webhooks

MethodPathAuthNotes
POST/api/webhooks/:webhookId/:tokenToken in the pathPost as the webhook
GET/api/webhooksBearerList
POST/api/webhooksBearerCreate
GET/api/webhooks/:webhookIdBearerRead one
PATCH/api/webhooks/:webhookIdBearerUpdate
DELETE/api/webhooks/:webhookIdBearerDelete

The management API

A separate listener on GRYT_ADMIN_PORT (5099), started only when GRYT_ADMIN_TOKEN is set, published to loopback only. It exists so somebody administering the machine can change database-backed settings without being the server's owner.

MethodPathReturns
GET/management/settingsjoinPolicy, discoverable, lanOpen, profanityMode
PATCH/management/settingsThe same, updated. Send only the keys you are changing
GET/management/health{ ok: true }

Authorization: Bearer <GRYT_ADMIN_TOKEN> on all three. Requires server 1.5.0. A PATCH goes through the same apply path as a change made from a client, so it withdraws mDNS advertising, drops caches and writes an audit entry rather than only updating a row.

Socket.IO

Joining

The handshake is three messages, and it is a signature exchange rather than a password.

EventDirectionPayload
server:infoclient → server
server:joinclient → server{ nickname?, inviteCode? }
server:challengeserver → clientA nonce to sign
server:verifyclient → server{ certificate, assertion }
server:joinedserver → clientSuccess, with tokens
server:setup_requiredserver → clientNothing has claimed ownership yet

The server also proves itself, over server:identify, as a connection-level exchange rather than part of the join. A client reconnecting with a saved token never joins at all, so a proof carried on join would leave the common path handing a bearer token to a server nobody authenticated.

Refusals come back on server:error with a reason: invite_required, invalid_invite, invite_rate_limited, identity_tier_refused, identity_verification_failed, challenge_expired, nonce_mismatch, account_already_member, approval_pending, auth_required, auth_disabled, auth_misconfigured, join_refused, join_failed, invalid_nickname.

Sessions and tokens

EventDirection
session:restoreclient → server
token:refreshclient → server
token:refreshed, token:invalid, token:revoked, token:errorserver → client

Chat

EventDirectionNotes
chat:sendclient → server
chat:fetchclient → server
chat:edit, chat:delete, chat:reactclient → server
chat:reportclient → server
chat:typing, chat:stop_typingboth
chat:new, chat:edited, chat:deleted, chat:reactionserver → client
chat:historyserver → clientAnswer to chat:fetch
chat:purge_userserver → clientEverything from one member is gone
chat:errorserver → client

Voice

EventDirectionNotes
voice:room:requestclient → serverAsks for access to a voice channel
voice:room:grantedserver → client{ room_id, join_token, sfu_url }
voice:channel:joinedboth
voice:state:updateclient → serverMute, deafen
voice:stream:set, voice:camera:state, voice:screen:state, voice:framing:setclient → server
voice:peer:connected, voice:peer:disconnectedclient → server
voice:peer:joined, voice:peer:leftserver → client
voice:latency:reportclient → server
voice:latency:updateserver → clientEverybody's round-trip times
voice:disconnect:userclient → serverModeration
voice:kicked, voice:device:disconnect, voice:room:leaveserver → client
voice:state:restoredserver → clientAfter a reconnect
voice:error, voice:room:errorserver → client

Members, profile and presence

EventDirection
members:fetchclient → server
members:list, members:errorserver → client
profile:updateclient → server
profile:updated, profile:errorserver → client
avatar:updatedclient → server
presence:heartbeatclient → server
presence:staleserver → client

Moderation

EventDirection
server:kick, server:ban, server:unbanclient → server
server:mute, server:deafenclient → server
server:bans:listclient → server
server:kicked, server:muted, server:deafenedserver → client
server:kick:success, server:ban:success, server:unban:success, server:mute:success, server:deafen:successserver → client
server:bansserver → client
reports:list, reports:resolveclient → server
reports:list, reports:resolved, report:submitted, report:already_reportedserver → client

Administration

EventDirectionNotes
server:settings:get, server:settings:updateclient → serverThe database-backed settings
server:settingsserver → client
server:channels:list, :upsert, :delete, :reorderclient → server
server:channelsserver → client
server:sidebar:list, :item:upsert, :item:delete, :reorderclient → server
server:sidebarserver → client
server:roles:list, server:roles:setclient → server
server:roles, server:role:updatedserver → client
server:invites:create, :list, :revokeclient → server
server:invites, server:invite:created, server:invite:revokedserver → client
server:member:inviteboth
server:joinRequests:list, server:joinRequests:decideclient → server
server:joinRequests, server:joinRequest:decidedserver → client
server:audit:listclient → server
server:auditserver → client
server:user:replaceclient → server
server:user:replace:successserver → client
server:leaveclient → server
server:leftserver → client
server:detailsboth
server:clientsserver → client
server:emojiQueue:getclient → server
server:emojiQueue:state, server:emojis:updatedserver → client
server:version:checkclient → server
server:version:statusserver → client

Diagnostics

EventDirection
diagnostics:pingclient → server
diagnostics:pongserver → client

On this page