Skip to content

HTTP routes

Besides the WebSocket, an OriginChats server answers plain HTTP on the same host and port. Clients use HTTP for file uploads, images, and public lookups. Integrations use it to post messages through webhooks.

Signing in on the WebSocket doesn’t sign in HTTP requests. Routes that need an account take their own proof in the request body.

Method Path Purpose
GET / Status page for people who open the server in a browser.
GET /info Server info and counts as JSON.
GET /user/{username} A public user.
GET /messages/{channel}/{message} A public message.
GET /invite/{code} Redirect to an invite page.
GET /invite/{code}/check Check an invite code.
GET /server-assets/{name} Server icon or banner image.
POST /server-assets/upload Upload or clear the icon or banner.
GET /emojis/{name} Custom emoji image.
GET /stickers/{name} Custom sticker image.
GET /avatar/{username} Server account avatar.
POST /avatar/{username} Upload a server account avatar.
POST /attachments/upload Upload an attachment.
GET /attachments/{id} Download an attachment.
GET /webhooks, /webhooks/{path} Look up a webhook by token.
POST /webhooks, /webhooks/{path} Post a message through a webhook.
GET / with a WebSocket upgrade The WebSocket. See the protocol model.
  • CORS. Every route allows requests from any origin, so browser clients on other domains can call them.
  • Errors. Most failures return JSON with an error string and a matching HTTP status: {"error": "Attachment not found or expired"}. Emoji and sticker routes return plain text instead.
  • Body size. The server rejects request bodies larger than 32 MiB or twice the attachment max_size, whichever is larger.

GET / returns an HTML page with the server’s name and three counts:

  • Online now: accounts with at least one signed-in connection.
  • Members: members who haven’t left and aren’t banned.
  • Connections: open WebSockets, including ones that haven’t signed in.

The page also has link preview tags (og:title, og:image with the banner, and so on) and a link to a list of OriginChats clients.

GET /info returns the server’s public identity and a few counts:

{
"server": {
"name": "Example Chat",
"icon": "https://chat.example.com/server-assets/7f1c2d3e-4b5a-6978-8a9b-0c1d2e3f4a5b.png",
"banner": "",
"url": "https://chat.example.com",
"owner": { "name": "alice" }
},
"stats": {
"total_users": 128,
"connected_users": 9,
"total_channels": 12,
"total_roles": 5
}
}
Field Description
server The same object as the handshake’s server.
stats.total_users Members who haven’t left and aren’t banned.
stats.connected_users Accounts with at least one signed-in connection.
stats.total_channels Channels of every type, including separators.
stats.total_roles Roles on the server.

GET /user/{username} returns one user object. {username} can be a username in any case, with or without a leading @, or a user ID.

{
"username": "alice",
"nickname": null,
"roles": ["owner", "user"],
"color": "#9400D3",
"cracked": false,
"clients": [],
"devices": []
}

The response never includes status, and clients and devices are always empty. Accounts that left the server are still returned.

Status error When
404 User not found No account matches, or the account is banned.

GET /messages/{channel}/{message} returns one message from a channel that anyone with the default role can view. Use it for link previews and embeds outside the app.

{
"id": "b2d4f6a8-1c3e-4a5b-8d7f-0e9c1b2a3d4e",
"type": "message",
"user": "alice",
"content": "Release notes are up.",
"timestamp": 1788177600.412
}

Only messages directly in the channel work. Thread messages aren’t served here.

Status error When
400 Channel and message are required A path part is empty.
404 Message not found The channel doesn’t exist, the default role can’t view it, or the message doesn’t exist.
500 Unable to read message The stored message couldn’t be read.

These routes back shareable invite links. Access control covers invites in full.

GET /invite/{code} redirects with HTTP 302 to https://originchats.com/invite/{host}/{code}, where {host} comes from the server’s configured URL. It doesn’t check the code. It returns 404 with Invite not found when the code is empty or the server has no configured URL.

GET /invite/{code}/check tells you whether a code works, without using it up:

{
"valid": true,
"code": "4F9A1C7E2B3D",
"expires_at": 1788782400,
"server": {
"name": "Example Chat",
"icon": "",
"banner": "",
"url": "https://chat.example.com",
"owner": { "name": "alice" }
}
}

expires_at is Unix seconds, or null for an invite that doesn’t expire. When the code can’t be used, valid is false and reason is invite_expired or invite_invalid. Both answers use HTTP 200 and include server. An empty code returns HTTP 404 with {"valid": false, "reason": "invite_invalid"}.

GET /server-assets/{name} serves an uploaded icon or banner. The handshake’s server.icon and server.banner already point here, so you rarely build this URL yourself. A name with a path in it, or a file that doesn’t exist, returns 404 with Server asset not found.

Server image uploads covers POST /server-assets/upload.

GET /emojis/{name} and GET /stickers/{name} serve custom emoji and sticker images. {name} can be the stored file name or the numeric emoji or sticker ID.

Successful responses send Cache-Control: public, max-age=86400, immutable. A missing image returns HTTP 404 with the plain-text body Emoji not found or Sticker not found.

Only server accounts have avatars hosted by the server. For Rotur accounts these routes return 404.

GET /avatar/{username} returns the account’s avatar as PNG, with ETag and Cache-Control: public, no-cache. A request with a matching If-None-Match gets HTTP 304.

If the account has no avatar, the server redirects with HTTP 302 to the server icon. If there’s no icon either, or the name isn’t a server account, it returns 404 with Avatar not found.

POST /avatar/{username} sets a server account’s avatar. Send multipart form data:

Field Required Description
file Yes A PNG or JPEG image from 1 byte to 5 MB, at most 4096 pixels on each side.
local_upload_token Yes The local_upload_token from ready. It must belong to {username}.

The server crops the image to a centred square, resizes it to 512 by 512, and stores it as PNG. It returns:

{
"url": "https://chat.example.com/avatar/USR%3Alocal_alice",
"user": { "username": "USR:local_alice", "nickname": "alice", "roles": ["user"], "cracked": true }
}

user is your full user object, shortened here. Every signed-in connection then gets a user_update event with user and pfp.

Status error When
400 Missing required field: file No file part.
400 Avatar must be between 1 byte and 5 MB The file is empty or too big.
400 Avatar must be a valid PNG or JPEG image The image can’t be decoded.
400 Avatar dimensions cannot exceed 4096 pixels The image is too large.
400 Unable to crop avatar image, Unable to resize avatar image, Unable to encode avatar image Image processing failed.
401 Invalid local upload token The token is missing, wrong, or its connection closed.
401 Invalid server account session The token belongs to a different account.
403 Access denied The account is banned.
404 Server account not found {username} isn’t a server account.
415 Content-Type must be multipart/form-data The body isn’t multipart.
429 Avatar upload rate limit exceeded More than 5 uploads in one minute.
429 The timeout reason The account is in timeout. The body also has length, in milliseconds.
500 Unable to save avatar Storage failed.

Uploading attachments walks through the upload flow. This section lists the exact rules.

POST /attachments/upload takes multipart/form-data or application/json. It returns HTTP 201 with attachment, an attachment object, and permanent: false.

Field Required Description
file Yes Multipart: the file. JSON: base64, or a data:<mime_type>;base64, URI.
name No File name, up to 255 characters. Multipart defaults to the part’s file name.
mime_type No MIME type, checked against the handshake’s allowed_types. Multipart defaults to the part’s Content-Type.
expires_in_days No Days until expiry. Default 7, capped by the operator’s limit.
validator_key, validator For Rotur accounts The connection’s validator_key and a fresh Rotur validator for it.
local_upload_token For server accounts The local_upload_token from ready. When present, the validator fields are ignored.
Status error When
400 Invalid JSON body, Invalid data URI, Invalid base64 file data The JSON body is malformed.
400 Missing required field: file, File is empty, Invalid file data There’s no usable file.
400 Invalid file name The name is empty, over 255 characters, or has a line break.
400 expires_in_days must be positive expires_in_days is zero or negative.
401 validator_key and validator are required for authentication, Invalid credentials The Rotur proof is missing or rejected.
401 Invalid local upload token The server account token is missing, wrong, or expired.
403 Access denied The account is banned.
413 File too large (max <n> bytes), File too large after image processing (max <n> bytes) The file is over max_size.
413 Image dimensions are too large to process safely The image has too many pixels to resize.
413 Attachment storage limit reached The server’s total storage is full.
415 Content-Type must be application/json or multipart/form-data Wrong request type.
415 File type <mime> is not allowed The MIME type isn’t in allowed_types.
415 Unable to remove EXIF metadata from this image, EXIF orientation is unsupported for this image type, EXIF orientation is unsupported for animated images, Invalid JPEG or PNG data, Unable to decode image, Unable to apply image orientation, Unable to resize image, Unable to encode image The image couldn’t be processed.
429 Upload rate limit exceeded Too many uploads this minute.
429 Attachment limit reached The account holds its maximum number of uploads.
429 The timeout reason The account is in timeout. The body also has length, in milliseconds.
500 Attachment storage is unavailable, Failed to save attachment Storage failed.
502 Failed to validate credentials, Invalid authentication response The server couldn’t check the validator with Rotur.
503 Attachments are disabled Uploads are turned off.

GET /attachments/{id} returns the file with its stored MIME type and Cache-Control: public, max-age=86400, immutable.

Status error When
404 Attachment not found or expired No such upload, or it has expired.
404 Attachment file not found The record exists but the file is missing from storage.
503 Attachments are disabled Attachments are turned off.

A webhook lets an outside service post messages into one channel. Admins create them with webhook_create, which returns a secret token. Every webhook request passes that token in the query string. Anything after /webhooks/ in the path is ignored.

GET /webhooks?token=<token> returns the stored webhook, including its token:

{
"id": "3e5f7a9b-1c2d-4e6f-8a0b-2c4d6e8f0a1b",
"channel": "announcements",
"name": "Release bot",
"token": "<webhook token>",
"created_by": "USR:local_alice",
"created_at": 1788091200.5,
"avatar": null
}

created_by is the creator’s user ID.

Status error When
401 Webhook token required No token in the query string.
401 Invalid webhook token No webhook has that token.

POST /webhooks?token=<token> posts a message to the webhook’s channel. Send JSON with Content-Type: application/json:

{
"content": "Version 2.4 is out.",
"username": "Release bot",
"avatar_url": "https://example.com/release-bot.png",
"embeds": [
{
"title": "Changelog",
"url": "https://example.com/changelog/2.4",
"description": "Faster search and a new theme picker."
}
]
}
Field Type Description
content string Message text. text and message work as fallbacks, in that order.
embeds array Up to 10 embeds. Required when there’s no content.
username string Display name for this message. Defaults to the webhook’s name.
avatar_url string Avatar for this message. Defaults to the webhook’s avatar.
usercolor string Name colour for this message.

The server saves the message with user: "originChats" and a webhook object holding the display fields, then sends message_new to everyone who can view the channel. The request returns HTTP 204 with no body. The message also goes through automod.

Status error When
400 Invalid JSON body The body isn’t a JSON object.
400 <field> must be a string content, text, message, username, avatar_url, or usercolor isn’t a string.
400 No content provided There’s no text and no embeds.
400 An embed error, such as Embed 1: title cannot exceed 256 characters An embed breaks the embed rules.
400 Message blocked by automod Automod matched the message.
400 Unhandled event type: <event> A GitHub delivery for an event other than push.
401 Webhook token required, Invalid webhook token The token is missing or wrong.
404 Channel not found The webhook’s channel was deleted.
413 Request body too large (max 10MB) The body is over 10 MiB.
415 Content-Type must be application/json Wrong request type.
500 Unable to save webhook message Storage failed.

You can use a webhook URL as a GitHub webhook with content type application/json. When a request has an X-GitHub-Event header and its body has repository and ref, the server treats it as a GitHub delivery. It handles only push events, which it turns into an embed listing the commits. Other GitHub events return HTTP 400 with Unhandled event type: <event>.