# API Reference

Source: https://docs.gryt.chat/docs/server/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](https://docs.gryt.chat/docs/server/identity).

## REST

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

### Public

| Method | Path | Returns |
|--------|------|---------|
| GET | `/health` | `{ status, service, serverName, timestamp }` |
| GET | `/info` | The join preview. See below |
| GET | `/icon` | The server icon. 404 when none is set |
| GET | `/metrics` | Prometheus, `text/plain` |

### What `/info` returns

```json
{
  "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.

<Callout title="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.
</Callout>

### Messages

| Method | Path | Auth | Notes |
|--------|------|------|-------|
| GET | `/api/messages/:conversationId` | Bearer | `?limit=` up to 200, `?before=` an ISO date |

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

### Uploads

| Method | Path | Auth | Notes |
|--------|------|------|-------|
| POST | `/api/uploads` | Bearer | multipart `file` |
| POST | `/api/uploads/avatar` | Bearer | multipart |
| DELETE | `/api/uploads/avatar` | Bearer | |
| GET | `/api/uploads/files/:fileId` | — | `?thumb=1` for the thumbnail, `?download=1` for an attachment header |

### Server

| Method | Path | Auth | Notes |
|--------|------|------|-------|
| POST | `/api/server/icon` | Bearer | Owner only, multipart `file` |
| DELETE | `/api/server/icon` | Bearer | Owner 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

| Method | Path | Auth | Returns |
|--------|------|------|---------|
| GET | `/api/members` | Bearer | `{ items: [{ serverUserId, nickname, lastSeen }] }` |

### Emojis

| Method | Path | Auth | Notes |
|--------|------|------|-------|
| GET | `/api/emojis` | — | `[{ name, file_id }]` |
| GET | `/api/emojis/all` | — | Including staged |
| GET | `/api/emojis/img/:name` | — | Image bytes, cached |
| POST | `/api/emojis` | Bearer | Admin or owner. `file` + `name`, or `files` + a `names` JSON array |
| PATCH | `/api/emojis/:name` | Bearer | Rename. Body `{ name }` |
| DELETE | `/api/emojis/:name` | Bearer | |
| GET | `/api/emojis/queue` | Bearer | The processing queue |
| POST | `/api/emojis/stage` | Bearer | Stage emojis before committing them |

### Importing emojis

From BetterTTV:

| Method | Path | Auth |
|--------|------|------|
| GET | `/api/emojis/bttv/user/:userId` | — |
| GET | `/api/emojis/bttv/emote/:emoteId` | — |
| GET | `/api/emojis/bttv/file/:emoteId` | — |
| POST | `/api/emojis/bttv/import` | Bearer, admin or owner, 200 per request |

From emoji.gg:

| Method | Path | Auth |
|--------|------|------|
| 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.

### Link previews and media

| Method | Path | Auth | Notes |
|--------|------|------|-------|
| GET | `/api/link-preview?url=` | Bearer | Open Graph metadata |
| GET | `/api/oembed?url=` | Bearer | X, SoundCloud, Spotify, TikTok |
| GET | `/api/media/metadata?url=` | Bearer | Dimensions and MIME type |

### Webhooks

| Method | Path | Auth | Notes |
|--------|------|------|-------|
| POST | `/api/webhooks/:webhookId/:token` | Token in the path | Post as the webhook |
| GET | `/api/webhooks` | Bearer | List |
| POST | `/api/webhooks` | Bearer | Create |
| GET | `/api/webhooks/:webhookId` | Bearer | Read one |
| PATCH | `/api/webhooks/:webhookId` | Bearer | Update |
| DELETE | `/api/webhooks/:webhookId` | Bearer | Delete |

## 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.

| Method | Path | Returns |
|--------|------|---------|
| GET | `/management/settings` | `joinPolicy`, `discoverable`, `lanOpen`, `profanityMode` |
| PATCH | `/management/settings` | The 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.

| Event | Direction | Payload |
|-------|-----------|---------|
| `server:info` | client → server | — |
| `server:join` | client → server | `{ nickname?, inviteCode? }` |
| `server:challenge` | server → client | A nonce to sign |
| `server:verify` | client → server | `{ certificate, assertion }` |
| `server:joined` | server → client | Success, with tokens |
| `server:setup_required` | server → client | Nothing 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

| Event | Direction |
|-------|-----------|
| `session:restore` | client → server |
| `token:refresh` | client → server |
| `token:refreshed`, `token:invalid`, `token:revoked`, `token:error` | server → client |

### Chat

| Event | Direction | Notes |
|-------|-----------|-------|
| `chat:send` | client → server | |
| `chat:fetch` | client → server | |
| `chat:edit`, `chat:delete`, `chat:react` | client → server | |
| `chat:report` | client → server | |
| `chat:typing`, `chat:stop_typing` | both | |
| `chat:new`, `chat:edited`, `chat:deleted`, `chat:reaction` | server → client | |
| `chat:history` | server → client | Answer to `chat:fetch` |
| `chat:purge_user` | server → client | Everything from one member is gone |
| `chat:error` | server → client | |

### Voice

| Event | Direction | Notes |
|-------|-----------|-------|
| `voice:room:request` | client → server | Asks for access to a voice channel |
| `voice:room:granted` | server → client | `{ room_id, join_token, sfu_url }` |
| `voice:channel:joined` | both | |
| `voice:state:update` | client → server | Mute, deafen |
| `voice:stream:set`, `voice:camera:state`, `voice:screen:state`, `voice:framing:set` | client → server | |
| `voice:peer:connected`, `voice:peer:disconnected` | client → server | |
| `voice:peer:joined`, `voice:peer:left` | server → client | |
| `voice:latency:report` | client → server | |
| `voice:latency:update` | server → client | Everybody's round-trip times |
| `voice:disconnect:user` | client → server | Moderation |
| `voice:kicked`, `voice:device:disconnect`, `voice:room:leave` | server → client | |
| `voice:state:restored` | server → client | After a reconnect |
| `voice:error`, `voice:room:error` | server → client | |

### Members, profile and presence

| Event | Direction |
|-------|-----------|
| `members:fetch` | client → server |
| `members:list`, `members:error` | server → client |
| `profile:update` | client → server |
| `profile:updated`, `profile:error` | server → client |
| `avatar:updated` | client → server |
| `presence:heartbeat` | client → server |
| `presence:stale` | server → client |

### Moderation

| Event | Direction |
|-------|-----------|
| `server:kick`, `server:ban`, `server:unban` | client → server |
| `server:mute`, `server:deafen` | client → server |
| `server:bans:list` | client → server |
| `server:kicked`, `server:muted`, `server:deafened` | server → client |
| `server:kick:success`, `server:ban:success`, `server:unban:success`, `server:mute:success`, `server:deafen:success` | server → client |
| `server:bans` | server → client |
| `reports:list`, `reports:resolve` | client → server |
| `reports:list`, `reports:resolved`, `report:submitted`, `report:already_reported` | server → client |

### Administration

| Event | Direction | Notes |
|-------|-----------|-------|
| `server:settings:get`, `server:settings:update` | client → server | The database-backed settings |
| `server:settings` | server → client | |
| `server:channels:list`, `:upsert`, `:delete`, `:reorder` | client → server | |
| `server:channels` | server → client | |
| `server:sidebar:list`, `:item:upsert`, `:item:delete`, `:reorder` | client → server | |
| `server:sidebar` | server → client | |
| `server:roles:list`, `server:roles:set` | client → server | |
| `server:roles`, `server:role:updated` | server → client | |
| `server:invites:create`, `:list`, `:revoke` | client → server | |
| `server:invites`, `server:invite:created`, `server:invite:revoked` | server → client | |
| `server:member:invite` | both | |
| `server:joinRequests:list`, `server:joinRequests:decide` | client → server | |
| `server:joinRequests`, `server:joinRequest:decided` | server → client | |
| `server:audit:list` | client → server | |
| `server:audit` | server → client | |
| `server:user:replace` | client → server | |
| `server:user:replace:success` | server → client | |
| `server:leave` | client → server | |
| `server:left` | server → client | |
| `server:details` | both | |
| `server:clients` | server → client | |
| `server:emojiQueue:get` | client → server | |
| `server:emojiQueue:state`, `server:emojis:updated` | server → client | |
| `server:version:check` | client → server | |
| `server:version:status` | server → client | |

### Diagnostics

| Event | Direction |
|-------|-----------|
| `diagnostics:ping` | client → server |
| `diagnostics:pong` | server → client |
