Skip to content

API Reference

All API endpoints require authentication via a Bearer token, unless noted otherwise.

Terminal window
Authorization: Bearer <your-jwt-token>

Default: http://localhost:3000

Health check endpoint. No authentication required.

Response: 200 OK

OK

Development mode login. Only available when auth.authMode is "dev".

Request:

{
"email": "user@example.com"
}

Response: 200 OK

{
"user": {
"id": "abc123",
"userName": "user",
"email": "user@example.com",
"isAdmin": false
},
"token": "eyJhbG..."
}

Request a magic link. Sends an email with a login link. Only available when auth.authMode is "magic-link".

Request:

{
"email": "user@example.com"
}

Response: 200 OK

{
"success": true,
"message": "If a matching account was found, a magic link has been sent."
}

Verify a magic link token and create a session. Only available when auth.authMode is "magic-link". The token is passed via the Authorization: Bearer <token> header.

Request:

{
"email": "user@example.com"
}

Response: 200 OK

{
"accessToken": "eyJhbG...",
"refreshToken": "eyJhbG..."
}

Sign in with email and password. Only available when auth.authMode is "password".

Request:

{
"email": "user@example.com",
"password": "your-password"
}

Response: 200 OK

{
"accessToken": "eyJhbG...",
"refreshToken": "eyJhbG...",
"exp": 900,
"tokenType": "Bearer"
}

Errors:

  • 401 Unauthorized - Invalid credentials

Request a password reset email. Only available when auth.authMode is "password". Always returns success to prevent email enumeration.

Request:

{
"email": "user@example.com"
}

Response: 200 OK

{
"success": true,
"message": "If a matching account was found, a password reset link has been sent."
}

Set a password after clicking an invite or password reset link. Only available when auth.authMode is "password". The token can be passed in the request body or via the Authorization: Bearer <token> header.

Request:

{
"email": "user@example.com",
"token": "invite-link-token",
"password": "your-new-password"
}

Response: 200 OK

{
"accessToken": "eyJhbG...",
"refreshToken": "eyJhbG...",
"exp": 900,
"tokenType": "Bearer"
}

Errors:

  • 400 Bad Request - Invalid token or password too short (minimum 8 characters)

Self-register with email and password. Only available when auth.authMode is "password" and isInviteRequired is false.

Request:

{
"email": "user@example.com",
"password": "your-password"
}

Response: 200 OK

{
"accessToken": "eyJhbG...",
"refreshToken": "eyJhbG...",
"exp": 900,
"tokenType": "Bearer"
}

Errors:

  • 400 Bad Request - User already exists or password too short

List available OAuth providers. Only present when OAuth is configured. No authentication required.

Response: 200 OK

{
"providers": [
{
"id": "google",
"name": "Google",
"loginUrl": "/auth/oauth/google"
}
],
"count": 1
}

Initiate an OAuth flow. Redirects the browser to the provider’s authorization page. No authentication required.

Response: 302 Found (redirect to OAuth provider)

Errors:

  • 429 Too Many Requests - Rate limit exceeded

OAuth callback handler. Called by the provider after user authentication. Validates the CSRF state, exchanges the authorization code for tokens, and redirects the user back to the application with tokens as query parameters. No authentication required.

Query parameters (set by the OAuth provider):

  • code - Authorization code
  • state - CSRF state token

Response: 302 Found (redirect to application URL with access_token, refresh_token, and expires_in query parameters)

Errors (redirected as query parameter oauth_error):

  • Missing or invalid state/code
  • User not found and invites are required
  • Rate limit exceeded

Refresh an expired access token.

Request:

{
"refreshToken": "eyJhbG..."
}

Response: 200 OK

{
"accessToken": "eyJhbG...",
"refreshToken": "eyJhbG..."
}

End the current session.

Request:

{
"refreshToken": "eyJhbG..."
}

Response: 200 OK

Get the current authenticated user.

Response: 200 OK

{
"user": {
"id": "abc123",
"userName": "user",
"email": "user@example.com",
"isAdmin": false
}
}

List active sessions for the current user.

Response: 200 OK

Revoke sessions for the current user.

Response: 200 OK

List all users.

Response: 200 OK

{
"users": [
{
"id": "user456",
"userName": "user",
"email": "user@example.com",
"isAdmin": false,
"createdAt": 1706234567890
}
]
}

Add a new user. Requires admin.

Request:

{
"email": "newuser@example.com",
"role": "user",
"password": "optional-initial-password"
}

Set role to "admin" to create an admin user (requires the requesting user to be an admin).

The password field is optional. When provided in password auth mode, the user’s password is set immediately. When omitted and email is configured, an invite email with a setup link is sent. When omitted and email is not configured, the user is created without a password.

Response: 200 OK

{
"success": true,
"userId": "user789"
}

Reset a user’s password. Requires admin.

Request:

{
"password": "new-password-min-8-chars"
}

Response: 200 OK

{
"success": true
}

Errors:

  • 400 Bad Request - Password too short (minimum 8 characters) or user not found
  • 403 Forbidden - Non-admin user

Remove a user. Requires admin. Cannot remove the last admin.

Response: 200 OK

{
"success": true
}

Self-remove the current user from the server. Admins can only exit if there is at least one other admin.

Response: 200 OK

{
"success": true,
"message": "You have exited the server"
}

Update the current user’s display name.

Request:

{
"userName": "New Display Name"
}

Response: 200 OK

{
"user": {
"id": "abc123",
"userName": "New Display Name",
"email": "user@example.com",
"isAdmin": false
}
}

List all channels.

Response: 200 OK

{
"channels": [
{
"id": "abc123",
"name": "general",
"createdAt": 1706234567890
},
{
"id": "def456",
"name": "random",
"createdAt": 1706234600000
}
]
}

Create a new channel. Requires admin.

Request:

{
"name": "new-channel"
}

Response: 200 OK

{
"channel": {
"id": "ghi789",
"name": "new-channel",
"createdAt": 1706234700000
}
}

Update a channel. Requires admin.

Request:

{
"name": "renamed-channel"
}

Response: 200 OK

{
"channel": {
"id": "abc123",
"name": "renamed-channel",
"updatedAt": 1706234800000
}
}

Delete a channel. Requires admin. Deleting a channel also deletes all its messages and webhooks.

Response: 200 OK

{
"success": true
}

Get messages in a channel.

Query parameters:

  • limit (optional): Maximum messages to return (default: 50)
  • before (optional): Get messages before this message ID

Response: 200 OK

{
"messages": [
{
"id": "msg123",
"channelId": "abc123",
"author": {
"id": "user456",
"userName": "user"
},
"content": "Hello, world!",
"createdAt": 1706234567890,
"updatedAt": null,
"reactions": {
"👍": ["user456", "user789"]
}
}
]
}

Send a message to a channel.

Request:

{
"content": "Hello, world!"
}

Response: 200 OK

{
"message": {
"id": "msg123",
"channelId": "abc123",
"author": {
"id": "user456",
"userName": "user"
},
"content": "Hello, world!",
"createdAt": 1706234567890
}
}

Edit a message. Users can only edit their own messages.

Request:

{
"content": "Updated message"
}

Response: 200 OK

{
"message": {
"id": "msg123",
"content": "Updated message",
"updatedAt": 1706234800000
}
}

Delete a message. Users can delete their own messages. Admins can delete any message.

Response: 200 OK

{
"success": true
}

Add a reaction to a message.

Request:

{
"reaction": "👍"
}

Response: 200 OK

{
"message": {
"id": "msg123",
"reactions": {
"👍": ["user456"]
}
}
}

Remove a reaction from a message.

Request:

{
"reaction": "👍"
}

Response: 200 OK

{
"message": {
"id": "msg123",
"reactions": {}
}
}

Upload an image. Images are sent as base64-encoded JSON (not multipart/form-data).

Request:

{
"filename": "photo.jpg",
"image": "<base64-encoded-image-data>",
"thumbnail": "<base64-encoded-thumbnail>"
}
  • image: Base64-encoded image data (max 10MB, formats: JPG, PNG, WebP, SVG)
  • thumbnail (optional): Base64-encoded thumbnail for previews

Response: 200 OK

{
"success": true,
"filename": "1706234567890-abc123def456.jpg"
}

GET /channels/:channelId/messages/image/:filename

Section titled “GET /channels/:channelId/messages/image/:filename”

Download an image.

Query parameters:

  • size (optional): Set to "thumb" to get the thumbnail version

Response: 200 OK

Returns the image data with appropriate Content-Type header.

Threaded replies allow sub-conversations on any channel message. Thread replies are regular messages linked to a parent message.

Get all replies in a thread.

Query parameters:

  • limit (optional): Maximum replies to return (default: 50)
  • before (optional): Get replies before this message ID

Response: 200 OK

{
"replies": [
{
"id": "reply123",
"threadId": "msg123",
"channelId": "abc123",
"author": {
"id": "user456",
"userName": "user"
},
"content": "Great point!",
"createdAt": 1706234567890
}
]
}

Create a reply in a thread. The parent message must be a top-level channel message (not itself a reply).

Request:

{
"content": "Great point!"
}

Response: 200 OK

{
"reply": {
"id": "reply123",
"threadId": "msg123",
"channelId": "abc123",
"author": {
"id": "user456",
"userName": "user"
},
"content": "Great point!",
"createdAt": 1706234567890
},
"threadMeta": {
"replyCount": 1,
"lastReplyAt": 1706234567890,
"lastReplyBy": { "id": "user456", "userName": "user" },
"participants": ["user456"]
}
}

Update a thread reply. Users can only edit their own replies.

Request:

{
"content": "Updated reply"
}

Response: 200 OK

{
"message": {
"id": "reply123",
"content": "Updated reply",
"updatedAt": 1706234800000
}
}

DELETE /messages/:messageId/thread/:replyId

Section titled “DELETE /messages/:messageId/thread/:replyId”

Delete a thread reply. Users can delete their own replies. Admins can delete any reply. The parent message’s threadMeta is recalculated automatically.

Response: 200 OK

{
"success": true
}

Upload an image for a thread reply. Same base64 JSON format as channel image uploads.

Request:

{
"filename": "photo.jpg",
"image": "<base64-encoded-image-data>",
"thumbnail": "<base64-encoded-thumbnail>"
}

Response: 200 OK

{
"success": true,
"filename": "1706234567890-abc123def456.jpg"
}

List all conversations for the current user.

Response: 200 OK

{
"conversations": [
{
"id": "conv123",
"participants": ["user456", "user789"],
"lastMessageAt": 1706234567890,
"otherUser": {
"id": "user789",
"userName": "other-user"
}
}
]
}

Create or get a conversation with another user.

Request:

{
"targetUserId": "user789"
}

Response: 200 OK

{
"conversation": {
"id": "conv123",
"participants": ["user456", "user789"],
"otherUser": {
"id": "user789",
"userName": "other-user"
}
},
"isNew": true
}

GET /conversations/:conversationId/messages

Section titled “GET /conversations/:conversationId/messages”

Get messages in a conversation. Only participants can access.

Query parameters:

  • limit (optional): Maximum messages to return (default: 50)
  • before (optional): Get messages before this message ID

Response: 200 OK

{
"messages": [
{
"id": "msg123",
"channelId": "conv123",
"author": {
"id": "user456",
"userName": "user"
},
"content": "Hey!",
"createdAt": 1706234567890
}
]
}

POST /conversations/:conversationId/messages

Section titled “POST /conversations/:conversationId/messages”

Send a direct message.

Request:

{
"content": "Hey!"
}

Response: 200 OK

{
"message": {
"id": "msg123",
"channelId": "conv123",
"author": {
"id": "user456",
"userName": "user"
},
"content": "Hey!",
"createdAt": 1706234567890
}
}

PUT /conversations/:conversationId/messages/:messageId

Section titled “PUT /conversations/:conversationId/messages/:messageId”

Edit a direct message. Users can only edit their own messages.

Request:

{
"content": "Updated message"
}

Response: 200 OK

{
"message": {
"id": "msg123",
"content": "Updated message",
"updatedAt": 1706234800000
}
}

DELETE /conversations/:conversationId/messages/:messageId

Section titled “DELETE /conversations/:conversationId/messages/:messageId”

Delete a direct message.

Response: 200 OK

{
"success": true
}

POST /conversations/:conversationId/messages/image

Section titled “POST /conversations/:conversationId/messages/image”

Upload an image in a conversation. Same base64 JSON format as channel image uploads.

Response: 200 OK

{
"success": true,
"filename": "1706234567890-abc123def456.jpg"
}

GET /conversations/:conversationId/messages/image/:filename

Section titled “GET /conversations/:conversationId/messages/image/:filename”

Download an image from a conversation. Only participants can access.

Query parameters:

  • size (optional): Set to "thumb" to get the thumbnail version

Response: 200 OK

Returns the image data with appropriate Content-Type header.

Get server settings.

Response: 200 OK

{
"name": "My Team Chat"
}

Update server settings. Requires admin.

Request:

{
"name": "New Server Name"
}

Response: 200 OK

{
"name": "New Server Name"
}

Webhooks allow external services to post messages to channels programmatically. Each webhook is scoped to a single channel and uses a static Bearer token for authentication.

List all webhooks. Requires admin. Tokens are stripped from the response.

Response: 200 OK

{
"webhooks": [
{
"id": "wh123",
"name": "GitHub Bot",
"channelId": "abc123",
"createdAt": 1706234567890,
"createdBy": "user456"
}
]
}

Create a new webhook. Requires admin. The token is returned only once at creation.

Request:

{
"name": "GitHub Bot",
"channelId": "abc123"
}

Response: 200 OK

{
"webhook": {
"id": "wh123",
"name": "GitHub Bot",
"channelId": "abc123",
"token": "a1b2c3d4e5f6...",
"createdAt": 1706234567890,
"createdBy": "user456"
}
}

Delete a webhook. Requires admin.

Response: 200 OK

{
"success": true
}

Post a message to a channel via webhook. Uses webhook token authentication instead of JWT.

Headers:

Authorization: Bearer <webhook-token>

Request:

{
"content": "Build #42 passed successfully!"
}

Response: 200 OK

{
"message": {
"id": "msg123",
"channelId": "abc123",
"content": "Build #42 passed successfully!",
"author": {
"id": "webhook:wh123",
"userName": "GitHub Bot",
"isBot": true
},
"createdAt": 1706234567890
}
}

Webhook messages appear in the channel with a “BOT” badge next to the author name.

Server-Sent Events endpoint for real-time updates. Authentication can be provided via query parameter (?token=<jwt>) or Authorization header.

Connection:

GET /events?token=<your-jwt-token>
Accept: text/event-stream

On connection, you receive a CONNECTED event:

data: {"type":"CONNECTED","payload":{"message":"SSE connection established","timestamp":"...","userId":"abc123","connectionId":"..."}}

Events:

All events are sent as data: lines (no event: header). Each event has a type and payload:

data: {"type":"NEW_MESSAGE","payload":{"id":"msg123","channelId":"abc123","author":{"id":"user456","userName":"user"},"content":"Hello!","createdAt":1706234567890}}
data: {"type":"UPDATE_MESSAGE","payload":{"id":"msg123","content":"Updated","updatedAt":1706234600000}}
data: {"type":"DELETE_MESSAGE","payload":{"id":"msg123","channelId":"abc123"}}
data: {"type":"NEW_CHANNEL","payload":{"id":"ch123","name":"general","createdAt":1706234567890}}
data: {"type":"UPDATE_CHANNEL","payload":{"id":"ch123","name":"renamed"}}
data: {"type":"DELETE_CHANNEL","payload":{"id":"ch123","name":"old-channel"}}
data: {"type":"NEW_REACTION","payload":{"messageId":"msg123","userId":"user456","reaction":"👍"}}
data: {"type":"DELETE_REACTION","payload":{"messageId":"msg123","userId":"user456","reaction":"👍"}}
data: {"type":"NEW_USER","payload":{"id":"user789","userName":"newuser","email":"new@example.com"}}
data: {"type":"REMOVE_USER","payload":{"id":"user789","email":"removed@example.com"}}
data: {"type":"USER_EXIT","payload":{"id":"user789","userName":"exited-user"}}
data: {"type":"UPDATE_USER","payload":{"id":"user789","userName":"new-display-name"}}
data: {"type":"NEW_CONVERSATION","payload":{"id":"conv123","participants":["user456","user789"],"createdAt":1706234567890}}
data: {"type":"NEW_DM_MESSAGE","payload":{"id":"msg123","channelId":"conv123","author":{"id":"user456","userName":"user"},"content":"Hey!","participants":["user456","user789"]}}
data: {"type":"UPDATE_DM_MESSAGE","payload":{"id":"msg123","content":"Updated","participants":["user456","user789"]}}
data: {"type":"DELETE_DM_MESSAGE","payload":{"id":"msg123","conversationId":"conv123","participants":["user456","user789"]}}
data: {"type":"NEW_THREAD_REPLY","payload":{"parentMessageId":"msg123","channelId":"abc123","reply":{...},"threadMeta":{...}}}
data: {"type":"UPDATE_THREAD_REPLY","payload":{...}}
data: {"type":"DELETE_THREAD_REPLY","payload":{"id":"reply123","threadId":"msg123","channelId":"abc123","threadMeta":{...}}}
data: {"type":"NEW_WEBHOOK","payload":{"id":"wh123","name":"GitHub Bot","channelId":"abc123"}}
data: {"type":"DELETE_WEBHOOK","payload":{"id":"wh123","channelId":"abc123"}}
data: {"type":"UPDATE_SERVER_SETTINGS","payload":{"name":"New Server Name"}}

DM events (NEW_DM_MESSAGE, UPDATE_DM_MESSAGE, DELETE_DM_MESSAGE) are only sent to the conversation participants.

All errors return a JSON object with an error field:

{
"error": "Error message here"
}

Common status codes:

  • 400 Bad Request - Invalid request body or parameters
  • 401 Unauthorized - Missing or invalid authentication
  • 403 Forbidden - Insufficient permissions
  • 404 Not Found - Resource not found
  • 429 Too Many Requests - Rate limit exceeded
  • 500 Internal Server Error - Server error