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.
Routes
Section titled “Routes”| 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. |
Shared behaviour
Section titled “Shared behaviour”- CORS. Every route allows requests from any origin, so browser clients on other domains can call them.
- Errors. Most failures return JSON with an
errorstring 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.
Status page
Section titled “Status page”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.
Server info
Section titled “Server info”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. |
Public user
Section titled “Public user”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. |
Public message
Section titled “Public message”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. |
Invites
Section titled “Invites”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"}.
Server images
Section titled “Server images”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.
Emoji and sticker images
Section titled “Emoji and sticker images”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.
Avatars
Section titled “Avatars”Only server accounts have avatars hosted by the server. For Rotur accounts these routes return 404.
Get an avatar
Section titled “Get an avatar”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.
Upload an avatar
Section titled “Upload an avatar”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. |
Attachments
Section titled “Attachments”Uploading attachments walks through the upload flow. This section lists the exact rules.
Upload an attachment
Section titled “Upload an attachment”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. |
Download an attachment
Section titled “Download an attachment”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. |
Webhooks
Section titled “Webhooks”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.
Look up a webhook
Section titled “Look up a webhook”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 a message
Section titled “Post a message”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. |
GitHub deliveries
Section titled “GitHub deliveries”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>.
