Sblurp

Uploads and files

These endpoints are plain HTTP, not Connect. Paths are relative to the server's base URL.

Endpoint Auth What
POST /upload Bearer Upload one file.
GET /files/{id}/{name}?exp=...&sig=... signed URL Download a file.
GET /emoji Bearer List custom emoji.
GET /emoji/{id} none One emoji image.
POST /emoji Bearer, moderator or higher Add emoji, zips too.
GET /emoji.zip Bearer Export every emoji.
GET /invite/{code}, GET /i/{code} none Redirect to the join screen.
GET /healthz none Answers ok.

Upload a file

Send multipart/form-data with one part named file, and the same Bearer token as for calls. Files are up to 25 MB.

curl https://chat.example.com/upload \
  -H 'Authorization: Bearer ...' \
  -F [email protected]

The reply is the Attachment as protojson. size is a string, because it is an int64:

{"id":"01a1...","name":"cat.png","contentType":"image/png","size":"14211",
 "url":"/files/01a1.../cat.png?exp=1792000800&sig=...","width":640,"height":480}

Then send the id in SendMessage.attachment_ids. A message takes up to 10, and only your own unused uploads. An upload not sent in a message within a day is deleted.

Errors are {"error": "..."} with status 400, 401, 403 or 413.

Images for settings

Banners, community icons, channel headers and per-community profile pictures are uploads too. Upload the image, then pass its id:

Image Call
Community banner UpdateServerSettings{banner_id}
Community icon UpdateServerSettings{icon_id}
Channel header UpdateChannel{header_image_id}
Profile picture in this community UpdateProfile{avatar_url} with the upload's url

The upload must be your own and unattached, PNG, JPEG, GIF, WebP or AVIF, and at most 8 MB. "" removes the image.

Download a file

Use Attachment.url exactly as the server returns it. It is signed: HMAC-SHA256 over the id and the expiry, with the key in <data>/file-key.

  • The server signs URLs only for members who may see the file, and for the uploader of an unattached upload.
  • No header is needed, so <img src> works.
  • A URL is valid for one to two hours. exp is rounded up to the hour, so the URL stays the same for an hour and the browser can cache it. The response is Cache-Control: private, immutable with a max-age that ends when the URL expires, so no shared cache keeps a file.
  • A missing, wrong or expired signature returns 403. Get fresh URLs by listing the messages again.
  • Removing a member does not revoke URLs they already hold.

The server sniffs the content type, AVIF and HEIC too. width and height are set for PNG, JPEG, GIF, WebP, BMP, AVIF and HEIC. Images, audio and video are served inline. Everything else is a download, with nosniff and a sandbox CSP.

Custom emoji

Messages and reactions use a custom emoji as :name:. Names are 1 to 62 of a-z, 0-9, - and _, unique on the server. Unknown names stay plain text.

List

GET /emoji returns ListEmojiResponse as protojson, sorted by name. It is gzipped when asked, with a weak ETag and Cache-Control: private, no-cache. Send If-None-Match to get 304 when nothing changed. A browser fetch with cache: "no-cache" does this by itself.

After any change every member gets the emoji_changed event. Fetch the list again then.

Images

GET /emoji/{id} is the url from the list. Ids are never reused, so the image is public, immutable and cached for a year. Anyone with the URL can load it.

Add

POST /emoji takes multipart/form-data with any number of file parts, 64 MB in total. Moderator or higher.

  • Each part is a PNG, GIF (animated is fine) or WebP of at most 256 KB and 512x512, or a .zip of them.
  • Folders inside a zip are fine. __MACOSX and dotfiles are skipped.
  • The name comes from the file name: lowercased, and other characters become _.
  • A taken name is skipped, never replaced.

The reply is UploadEmojiResponse, with one result per image: added, exists, or rejected with a reason.

Export and delete

GET /emoji.zip returns every emoji as name.ext. It uploads as is to another server. Delete one with the DeleteEmoji call, moderator or higher.

GET /invite/{code} and GET /i/{code} redirect a browser to the web client's join screen: {web}/#/join?url={base}&invite={code}. The web client is set with -web-url.

Tools and agents can read every call and type from schema.json.