Strafe.chat Developers Open Strafe

REST API

All paths are relative to this instance's API base: https://your-instance.example/api. Authenticate with Authorization: Bot <token> (bots) or Authorization: Bearer <access token> (OAuth2, scoped endpoints only). Send and expect JSON; ids are strings.

Who can call what. Every endpoint below accepts a bot token unless marked session only. OAuth2 tokens reach only the endpoints marked with a scope. A bot is checked against its permissions in the space or room like any member.

Instance#

GET / — no auth#

What this instance is and what it has switched on. Clients read it before anyone signs in.

json
{ "strafe": { "version": "…" }, "federation": { "enabled": true, "domain": "your-instance.example" }, "features": { "captcha": { "enabled": true, "provider": "altcha" }, "email": { "enabled": true, "verification_required": false }, "invite_only": { "enabled": false }, "voice": { "enabled": true }, "gifs": { "enabled": true, "providers": ["giphy"] }, "activities": { "enabled": true } }, "services": { "cdn": { "enabled": true, "url": "…" }, "voice": { "enabled": true, "url": "…" } } }

features.gifs.providers lists the GIF providers (giphy, heypster) this instance has an API key for, and enabled is true when there is at least one; see GIFs. The keys themselves never leave the server.

features.activities.enabled is true when the instance takes activities ("Playing …") over the gateway's op 9 and shows them on presences; see Gateway › Activity. Clients send none to an instance without it.

Users#

GET /users/@me — scope: identify or email#

The authenticated account. email is present for sessions and bots (a bot's is synthetic) and for OAuth2 tokens with email.

json
{ "id": "…", "email": "", "username": "Docs_Bot", "display_name": "Docs Bot", "bio": "", "about_me": "", "pronouns": "they/them", "birthday": "09-01", "birthday_opt_in": true, "avatar": "", "banner": "", "accent_color": "", "public_flags": 0, "bot": true, "presence": { "status": "online" } }

presence is the presence object, as only you see it. Its activities reach sessions and bots only: an OAuth2 access token gets the status and custom status but never the activity, since no scope asks for it.

pronouns is free text (max 40 characters) and is public, like the bio - it rides along on every user object (message authors, mention_users, space members, relationships). birthday is "MM-DD" with no year, derived from the date of birth given at registration, and birthday_opt_in says whether the account wants it announced in spaces. The owner's own birthday is returned whether or not they opted in so their settings can show it; on other people's user objects both birthday and is_birthday appear only after they opt in.

The presence object#

json
{ "status": "online", "custom_status": "brb", "activities": [ { "type": "playing", "name": "Factory Builder", "started_at": 1761000000000 } ] }
Field
statusonline, idle, dnd or offline. Your own presence can also be invisible; everyone else sees an invisible account as offline
custom_statusThe account's custom status, absent when there is none
activitiesWhat the account is doing right now, absent when nothing: at most one entry, type "playing", the game's name (1-128 characters; show it as plain text) and started_at (unix ms). Set over the gateway with op 9 and kept only in the instance's short-lived cache while the reporting connection is open - never in its database. It appears on your own presence, on space member objects and on friends' entries - never while the account appears offline or invisible to you, and not on every user object: a message author or a PM's recipients carry none, so keep the last value you saw rather than clearing it from those. Sessions and bot tokens see it; an OAuth2 access token never does (GET /users/@me gives it the status without activities)

PATCH /users/@me#

Update the account's profile: display_name, bio, about_me, pronouns (free text, max 40 characters; "" clears it), birthday_opt_in (true lets spaces celebrate the account's birthday - the server keeps the announcement index itself), accent_color (hex), presence ({"status": "online" | "idle" | "dnd" | "invisible", "custom_status": "…"} - activities is not set here but over the gateway). Avatar and banner are uploaded with POST /users/@me/avatar and /users/@me/banner (multipart file). The date of birth itself cannot be changed over the API.

GET /users/@me/spaces — scope: spaces#

The account's spaces with owner and its space-wide permissions there. See OAuth2 › Listing the person's spaces.

Spaces#

GET /spaces#

Every space the account is in, each with roles. Same shape as READY.spaces.

GET /spaces/:id#

One space (member only) with roles.

json
{
  "id": "2102226824774811648", "name": "Unread Test Space 2", "name_acronym": "UTS", "description": "", "icon": "", "banner": "",
  "owner_id": "2102223172425220096", "everyone_role_id": "2102226824779005952",
  "system_room_id": "2102443061534523392", "system_room_flags": 0, "default_message_notifications": 0,
  "afk_room_id": null, "afk_timeout": 0, "widget_enabled": false, "features": [],
  "roles": [ … ], "created_at": "…", "updated_at": "…"
}

PATCH /spaces/:id — Manage Space#

name, description, system_room_id, system_room_flags, default_message_notifications (0 all messages, 1 only mentions), afk_room_id, afk_timeout (60, 300, 900, 1800 or 3600), widget_enabled, widget_room_id, birthday_channel_id, birthday_message. Room ids as strings; "" clears one. birthday_channel_id must be a text room in the space (otherwise 400) and is where the daily birthday greetings go; birthday_message is an optional template, max 500 characters, with {user} marking where the member is named - empty uses the default greeting.

GET /spaces/:id/rooms#

The space's rooms with overrides and, for the caller, read state. Same shape as READY.space_rooms[id].

POST /spaces/:id/rooms — Manage Rooms#

{"name", "type": 3 | 4 | 5, "parent_id"?, "e2ee_enabled"?, "user_limit"?, "bitrate"?}. Type 3 text, 4 voice, 5 section. Returns the room (201). A space holds at most 500 rooms and 250 roles (400 beyond that).

PATCH /spaces/:id/rooms/:roomId — Manage Rooms#

name, topic, slowmode_seconds, e2ee_enabled (text rooms; turning encryption on is one-way for existing history), user_limit, bitrate, position, permissions_synced. You must be able to view the room; permissions_synced rewrites the room's overrides, so it needs Manage Roles as well, and only for overrides you could have written yourself.

DELETE /spaces/:id/rooms/:roomId — Manage Rooms#

GET /spaces/:id/audit-log — Manage Space#

?limit= (≤100) &before=<entry id> &action=<type>. Returns {"entries": [{id, action_type, user_id, target_id, changes, reason, created_at}], "users": {…}}. Bot installs are action_type: "bot_add" with the bot as target.

POST /spaces/:id/leave#

Leave the space (the owner cannot; they transfer or delete it).

Members#

The member object#

json
{
  "id": "2102223172425220096", "username": "turtle", "display_name": "Turtle", "avatar": "", "banner": "", "bio": "", "about_me": "", "pronouns": "",
  "bot": false, "public_flags": 0, "presence": { "status": "online" },
  "joined_at": "2026-10-13T12:00:00Z", "roles": ["2102226824779005952"],
  "nick": "Shell", "space_avatar": "https://cdn.your-instance.example/v1/spaces/members/…/avatar-….png",
  "space_banner": null, "space_about_me": null, "space_bio": null, "space_pronouns": null
}

presence is the member's presence, including what they are playing. The user fields (display_name, avatar, bio, about_me, pronouns, …) are always the member's global profile. The six space-profile keys are their profile in this space only, and every member object carries all six, each a string or null: nick (max 32 characters), space_avatar and space_banner (image URLs, set only through the upload endpoints below), space_about_me and space_bio (max 190 each), space_pronouns (max 40). Where one is set it replaces the global value inside this space - show nick instead of the display name, space_avatar instead of avatar, and so on. A space profile is visible to the space's members only, and it is deleted with the membership: leaving, a kick, a ban or the space's deletion removes it.

GET /spaces/:id/members#

Every member as a member object.

GET /spaces/:id/members/:userId#

One member as a member object; @me for yourself. You must be a member yourself (403), and 404 member not found means they are not one.

PATCH /spaces/:id/members/:userId — Change Nickname for your own, Manage Nicknames for others'#

Set part of a space profile: nick, about_me, bio, pronouns, avatar, banner. A key that is absent is left alone; null or "" clears it; values are trimmed. Returns the updated member object and sends SPACE_MEMBER_UPDATE. A request that changes nothing returns 200 with no event.

  • Yourself (@me): nick needs Change Nickname (on for @everyone by default) or Manage Nicknames; the other fields need only membership. avatar and banner accept only a clear (null or "") - set an image with the upload endpoints below.
  • Another member, with Manage Nicknames and a highest role above theirs: nick (set or clear), and null (or "") for any of avatar, banner, about_me, bio and pronouns, which resets it to their global profile. A non-empty value for those five is 403 only the member can change their own space avatar, banner, pronouns, about me or bio (code: "cannot_edit_member_profile"). Only the owner can change the owner's nickname or profile, Administrators included: anyone else gets 403 only the space owner can change the owner's nickname when the request touches nick, and 403 only the space owner can change the owner's space profile when it only resets the other fields.

A moderator's nickname change is logged as member_nick_update (changes.nick with the old and new value) and a reset as member_profile_reset (changes.fields, the fields that were cleared). Changes to your own profile are never logged. In a space hosted on another instance the change is made there, where the space profile lives.

Errors: 400 no fields to update, 400 for a value over its limit or a nickname that is not on one line, 400 avatar and banner must be uploaded, or cleared with null, 403 missing permission to change your nickname in this space, 403 only the space owner can change the owner's nickname, 403 only the space owner can change the owner's space profile, 403 only the member can change their own space avatar, banner, pronouns, about me or bio (code: "cannot_edit_member_profile"), 403 cannot manage a role or member at or above your own highest role, 404 member not found, 409 the space profile changed while saving; try again (another write to the same image won the race; send it again).

POST /spaces/:id/members/@me/avatar, POST /spaces/:id/members/@me/banner#

Upload your own space avatar or banner: multipart file, png, jpg, gif or webp, within the same size limits as the global ones. Only membership is needed. Returns the member object with space_avatar / space_banner set; the image it replaces is deleted. To remove one, PATCH it to null.

Errors: 400 missing file field, 400 invalid file size, 400 unsupported image type (use jpg, png, gif, or webp), 403 when you are not a member, 502 could not store image, 503 file uploads are not configured (the instance has no object storage set up).

DELETE /spaces/:id/members/:userId — Kick Members#

Kick. A kicked bot's managed role is removed with it.

PUT /spaces/:id/members/:userId/roles — Manage Roles#

{"role_ids": ["…"]} replaces the member's roles (@everyone is implied); 204. As on Discord, only the roles decide, never the member's rank: you can change anyone's roles - your own, the space owner's, a member ranked above you - as long as every role you add or remove is below your own highest (the owner has no limit). Roles at or above it that the member already holds are kept whatever the list says, so leaving one out does not remove it. A bot's managed role can neither be given to anyone else nor taken from the bot.

Errors: 400 unknown role id, 400 this role belongs to a bot and cannot be deleted or assigned, 403 missing permission, 403 cannot manage a role or member at or above your own highest role (a role you add is at or above yours), 403 not a member of this space.

PUT /spaces/:id/members/:userId/roles/:roleId, DELETE /spaces/:id/members/:userId/roles/:roleId — Manage Roles#

Give a member one role, or take one away - Discord's Add / Remove Guild Member Role. 204, also when they already have the role (or, for DELETE, do not), in which case nothing is written and no event is sent. Each is a single atomic change to the member's role set, so two bots giving out different roles to the same person at the same moment cannot undo each other; prefer these to the full-list PUT whenever you change one role. (That holds on every instance once the space's origin is up to date; in a space mirrored from an older origin, the change reaches the origin as a full-list write.) The same rules apply: the role must be below your own highest role, whatever the member's rank (the owner included), and a managed role is never assignable. Unlike the full-list PUT, which quietly keeps a role at or above yours, DELETE refuses to take one away (403). A change sends SPACE_MEMBER_UPDATE and is written to the audit log as member_roles_update.

Errors: 400 when :roleId is @everyone (every member always has it), 400 this role belongs to a bot and cannot be deleted or assigned, 403 missing permission, 403 cannot manage a role or member at or above your own highest role, 403 not a member of this space (the user is not in the space), 404 role not found.

PUT /spaces/:id/members/:userId — Create Invite; scope spaces.join on the token in the body#

Add a user who authorized your application with spaces.join: {"access_token": "…"}. 201 added, 204 already a member. See OAuth2 › spaces.join.

Bans — Ban Members#

POST /spaces/:id/bans/:userId {"reason"?} bans (and removes) the user - someone who has already left or been kicked can be banned too, so a live invite does not let them back in; DELETE /spaces/:id/bans/:userId lifts it; GET /spaces/:id/bans lists the bans (user_id, reason, banned_by, created_at) together with the profiles of the users involved. The owner cannot be kicked or banned, and you cannot act on someone whose highest role is at or above yours.

Roles#

The role object#

json
{ "id": "2105144670576185344", "name": "Docs Bot", "permissions": 8768, "position": 1, "color": 0, "hoist": false, "mentionable": false, "bot_id": "2105141069376131072", "created_at": "…", "updated_at": "…" }

position orders roles: @everyone is always 0, custom roles hold unique positions from 1 up, and higher outranks lower (see Permissions › Hierarchy). bot_id is present on a managed role created by a bot install: the bot's user id.

GET /spaces/:id/roles#

Every role of the space, @everyone included. Any member may list them.

POST /spaces/:id/roles — Manage Roles#

{"name", "permissions"?, "color"?, "hoist"?, "mentionable"?}. You cannot grant permission bits you do not hold (403). The new role goes in at position 1, just above @everyone - as on Discord, and so that whoever created the role (a moderator, a bot) can manage it and give it out straight away. Only the unbroken run of roles directly above it moves up one to make room; roles above a gap in the positions keep theirs. Returns the role (201) with its position. Members get SPACE_ROLE_CREATE, and when roles moved up they arrive in one SPACE_UPDATE carrying roles. Read the new positions from the response and that event rather than working them out.

PATCH /spaces/:id/roles — Manage Roles#

Move several roles at once - Discord's Modify Guild Role Positions. The body is a list of {"id", "position"}:

json
[{ "id": "2105144670576185344", "position": 5 }, { "id": "2105144670576185999", "position": 4 }]

Each listed role is placed at its position and every other role keeps its order around them: a listed role that lands on a taken position takes it, and the role there (and any above it in the way) moves up one. Two listed roles asking for the same position: the newer one goes lower. Positions stay unique. To reorder a block of roles without touching anything else, give them the positions they already hold, in the new order - the example swaps two roles that sit at 4 and 5.

1 to 250 entries, each id once, each an existing custom role of the space, each position from 1 to 2147483397; managed roles can be moved, @everyone cannot. Except for the owner, every listed role must be below your own highest role and so must every position you ask for - otherwise nothing is written. Returns 200 with every role of the space (the GET shape). Members get one SPACE_UPDATE carrying the full roles list. Each listed role whose position changed is written to the audit log (role_update with changes.position); roles pushed up along the way are not.

Errors: 400 for a malformed list - invalid JSON (not a list of {"id", "position"}), invalid role id in list (an id that is not a snowflake), role positions must list from 1 to 250 roles, each once (empty, more than 250 entries, a repeated id) - 400 role position must be from 1 to 2147483397, 400 unknown role id (a listed id that is not a role of the space), 400 cannot move @everyone, 403 missing permission, 403 cannot manage a role or member at or above your own highest role.

PATCH /spaces/:id/roles/:roleId — Manage Roles#

Any of the fields above plus position. @everyone can only have permissions changed; a role at or above your own highest is off limits. position (1 to 2147483397) swaps the role with the role holding that position, or, when no role holds it, puts the role there as sent; except for the owner it must be below your own highest role. When the move swapped another role, the space's roles arrive in one SPACE_UPDATE carrying roles. To reorder several roles, use PATCH /spaces/:id/roles instead: one request, all or nothing.

DELETE /spaces/:id/roles/:roleId — Manage Roles#

Not @everyone, not a managed role, and except for the owner only a role below your own highest.

Errors for the role endpoints: 400 role name must be 1-100 characters and not @everyone, 400 cannot rename or delete the @everyone role, 400 cannot move @everyone, 400 role position must be from 1 to 2147483397, 400 this role belongs to a bot and cannot be deleted or assigned, 400 a space can have at most 250 roles, 403 missing permission, 403 cannot grant permissions you do not have, 403 cannot manage a role or member at or above your own highest role, 404 role not found.

Room overrides — Manage Roles#

  • GET /spaces/:id/rooms/:roomId/overrides → [{role_id, allow, deny}]; PUT …/overrides/:roleId {"allow", "deny"}; DELETE …/overrides/:roleId.
  • GET /spaces/:id/rooms/:roomId/overrides/users → [{user_id, allow, deny}]; PUT …/overrides/users/:userId; DELETE …/overrides/users/:userId.

Only room-scoped bits are accepted (see Permissions › Room overrides). Listing needs View on the room. Deleting an override is bound by the same rule as writing one - removing a deny grants what the matching allow would - so you can only delete overrides whose bits you hold in that room (403 otherwise).

Invites#

POST /spaces/:id/invites — Create Invite#

{"max_age_seconds"?: 0…2592000, "max_uses"?: 0…1000} (0 = unlimited / never expires). Returns:

json
{ "code": "k3Dx9qLm", "space_id": "…", "inviter_id": "…", "max_uses": 0, "current_uses": 0, "expires_at": "…", "created_at": "…" }

The invite link is <web>/invite/<code>.

GET /spaces/:id/invites — Manage Space, DELETE /spaces/:id/invites/:code — Manage Space#

GET /spaces/invites/:code — no auth#

Public preview {space: {id, name, icon, …}, inviter_display_name}. POST /spaces/invites/:code/join joins (the caller must be a person or a bot allowed to join; bots are normally added through install links instead).

Rooms#

GET /rooms#

The account's private messages and group PMs.

GET /rooms/:id#

Any room the account can see, space rooms included - a space room is listed only when the account may view it. Space rooms carry space_id; every room carries e2ee_enabled.

POST /rooms#

Open a PM: {"recipient_id": "…"} (or recipient_handle: "name"), or a group: {"name", "recipient_ids": [...]}. Returns the room. PMs are end-to-end encrypted by default, so a bot can open one but cannot read it until the person turns encryption off for that PM.

POST /rooms/:id/typing#

Broadcast a typing indicator (rate limited to one per 5 s per room).

POST /rooms/:id/ack#

{"message_id": "…"} marks the room read up to that message (clears the caller's unread count and mention badge).

Threads#

A thread is a room of type 6 inside a text channel (Discord's model): started from a message - then it shares the message's id and the message carries thread_id - or on its own, public or private, with its own member list, an archive state that times out on its own, and a lock. Threads inherit the channel's permission overrides; four bits of their own apply (see Permissions): Create Public Threads, Create Private Threads, Send Messages In Threads (Send Messages does not grant sending in a thread) and Manage Threads. Active threads the caller can see come in READY.space_rooms and GET /spaces/:id/rooms with a thread block: {"archived", "archived_at", "locked", "private", "invitable", "auto_archive_minutes", "owner_id", "starter_message_id", "last_active_at", "message_count", "member_count", "joined"}. Messages in a thread use the ordinary /rooms/:thread_id/messages endpoints.

POST /rooms/:id/messages/:msg_id/threads — Create Public Threads#

{"name", "auto_archive_minutes"} (60, 1440, 4320 or 10080; default 1440). 201 with the thread. The channel gets a thread_created system message.

POST /rooms/:id/threads — Create Public Threads, or Create Private Threads with private#

{"name", "auto_archive_minutes", "private", "invitable"}. A private thread starts with only its creator in it.

GET /rooms/:id/thread, PATCH /rooms/:id/thread, DELETE /rooms/:id/thread#

PATCH takes any of {"name", "archived", "locked", "invitable", "auto_archive_minutes", "slowmode_seconds"}. Name, archive state, auto-archive and invitable: Manage Threads or the thread's creator (unarchiving an unlocked thread: any member too). Lock and slowmode: Manage Threads. DELETE: Manage Threads. An archived thread only takes archived: false (a locked change may ride along); any other edit must unarchive in the same request or gets 400. A message sent into an archived, unlocked thread unarchives it; a locked thread takes nothing from members without Manage Threads.

Thread members#

GET /rooms/:id/thread-members lists [{"user_id", "joined_at", "user"}]. PUT / DELETE /rooms/:id/thread-members/@me join and leave; PUT / DELETE /rooms/:id/thread-members/:userId add (any member of a public or invitable thread, else Manage Threads) and remove (Manage Threads or the creator). Posting in a thread, or being mentioned in one, joins it. Members are who gets unread and mention counts for the thread.

GET /spaces/:id/threads/active, GET /rooms/:id/threads/archived#

Every active thread in the space the caller can see, and a channel's archived threads (?before=<RFC3339 archive time>&limit=, newest first; private ones only for their members and Manage Threads holders). Both answer {"threads": [...]}, the archived list with has_more.

Messages#

The message object#

json
{
  "id": "2105141783225700352",
  "room_id": "2102443061534523392",
  "sender_id": "2105141069376131072",
  "sender_device_id": "0",
  "plaintext": "pong from the bot",
  "ciphertext": "",
  "reply_to_id": "2105141774585434112",
  "mentions": ["2102223172425220096"],
  "mention_roles": [],
  "mention_everyone": false,
  "attachments": [ { "id": "…", "filename": "chart.png", "url": "https://…/cdn/…", "content_type": "image/png", "size": 48213, "width": 1280, "height": 720 } ],
  "reactions": [ { "emoji": "👋", "count": 1, "me": false } ],
  "pinned": true,
  "pinned_at": "2026-10-06T01:12:00.000Z",
  "pinned_by": "2102223172425220096",
  "created_at": "2026-09-30T03:44:47.421Z",
  "updated_at": "2026-09-30T03:44:47.421Z"
}

plaintext is present in plain rooms; encrypted rooms carry ciphertext instead. reactions is included when fetching messages, not in gateway events. pinned, pinned_at and pinned_by are present while the message is pinned in its room. Deleted messages keep their id with a deleted_at. System notices have sender_id: "0", a system_type (space_member_join, space_member_leave, space_birthday, …) and a system_payload. space_birthday is posted once a day in the space's birthday_channel_id for each opted-in member whose birthday is that day (UTC); its payload is {"user_id": "…", "message": "…"}, with message being the space's birthday_message (possibly empty, meaning use the default greeting).

GET /rooms/:id/messages#

?limit= (default 50, ≤100) &before=<message id> for older history. Newest first. Needs View Room and Read Message History in the room.

GET /rooms/:id/messages/:msg_id#

POST /rooms/:id/messages — Send Messages#

See Bots › Sending messages. 201 with the message.

PATCH /rooms/:id/messages/:msg_id#

{"plaintext": "…"} - own messages only.

DELETE /rooms/:id/messages/:msg_id — own, or Manage Messages#

?q=<text> &has=link|file|image|video|audio &limit=. Plain rooms only. GET /spaces/:id/messages/search searches every plain room of a space the caller can read.

Reactions — Add Reactions#

PUT / DELETE /rooms/:id/messages/:msg_id/reactions/:emoji, GET …/reactions/:emoji (who reacted). :emoji is a URL-encoded unicode emoji or custom:<id>. Custom emoji from other spaces need Use External Emojis.

Pins — Manage Messages in a space room; anyone in a PM or group#

PUT / DELETE /rooms/:id/messages/:msg_id/pin pin or unpin a message for everyone in the room (204, idempotent). A room holds at most 50 pins. Pinning posts a message_pinned system message whose system_payload names actor_id and message_id; every change fires ROOM_PINS_UPDATE. Deleting a message drops its pin.

GET /rooms/:id/pins — the room's pinned messages, newest pin first: {"items": [{"pinned_at", "pinned_by", "message": {…}}], "has_more": false}. Needs View Room and Read Message History.

POST /rooms/:id/attachments — Send Messages#

multipart/form-data with file and optional width/height. Returns the attachment record; reference its id in a message within the same room. Size limit per the instance's configuration (25 MB on a default deployment).

GIFs#

The instance proxies GIF search, so the provider's API key stays on the server and the search request comes from the instance, never from the person searching: not their address, cookies or account. The GIFs themselves (url, preview_url, still_url) are served by the provider's CDN, so whatever loads them is seen by the provider. Trending pages are cached for two minutes; searches are passed straight through and not cached.

Parameter
providergiphy (the default) or heypster - one of features.gifs.providers on GET /
qSearch only, and required there. Trimmed, and cut to 50 characters
limit1-50, default 24
offset0-4999, default 0

Results are rated up to PG-13.

json
{ "results": [{ "id": "…", "title": "…", "url": "https://…/giphy.gif", "width": 480, "height": 270, "preview_url": "https://…/200w.gif", "preview_width": 200, "preview_height": 113, "still_url": "https://…/200w_s.gif" }], "next_offset": 24, "total_count": 1000 }

url is the rendition to send or embed, preview_url a smaller animated one for a picker grid, and still_url (absent when the provider has none) a single frame to show while it loads. Every URL is https://; an item without a usable preview and full rendition is left out, so a page can hold fewer than limit. Pass next_offset as offset for the next page, and stop once it reaches total_count.

StatusBodyWhen
400{"error": "unknown gif provider", "code": "gif_provider_unknown"}provider is neither giphy nor heypster
400{"error": "query is required"}q is empty on /gifs/search
404{"error": "gif provider not configured", "code": "gif_not_configured"}The instance has no key for that provider
502{"error": "gif provider unavailable", "code": "gif_upstream"}The provider failed or answered with an error
503{"error": "gif provider unavailable", "code": "gif_upstream"}The provider is rate limiting the instance; try again later

Applications (session only)#

Managed from the client's Settings → Developers; listed for completeness. Only a person's session may call them, never a bot or an OAuth2 token.

Method and path
GET /applicationsYour applications
POST /applications {"name"}Create; returns the app with client_secret (once)
GET /applications/:id, PATCH (name, description, icon, redirect_uris, bot_public), DELETE
GET /applications/:id/publicPublic profile of any application (id, name, description, icon, has_bot, bot_public, bot) - how a client shows Add to Space on a bot's profile
POST /applications/:id/secretNew client secret (once)
POST /applications/:id/botAdd the bot; returns it with its token (once)
PATCH /applications/:id/bot (display_name, about_me, bio, accent_color), POST /applications/:id/bot/avatar, POST /applications/:id/bot/banner (multipart file)The bot's profile, edited by the application's owner
POST /applications/:id/bot/tokenNew bot token (once); disconnects the bot
GET /oauth2/@me/grants, DELETE /oauth2/@me/grants/:app_idThe applications you authorized, and revoking one

OAuth2 endpoints (/oauth2/token, /oauth2/token/revoke, /oauth2/@me) are on the OAuth2 page.

Key backup (session only)#

The end-to-end encryption key backup (m.megolm_backup.v1.curve25519-aes-sha2, the same shape as Matrix's /room_keys). The server stores room keys encrypted to the backup's public key and the private key wrapped under the user's recovery code; it can read neither. Only a person's session may call these.

Method and path
POST /devices/backup/version {"algorithm", "auth_data", "wrapped_private_key", "salt", "password"}Start a new version; returns {"version"}. password is the account password, checked again: 400 password is required without it, 403 invalid password when wrong. The version this one replaces is kept, keys included, for 7 days so a replacement the user did not make can be undone; anything older is removed. Fires KEY_BACKUP_UPDATE
GET /devices/backup/versionThe current version's public parameters: {"exists", "version", "algorithm", "auth_data", "etag", "count", "created_at"}, plus "previous": {"version", "created_at"} while a superseded version is still retained (absent otherwise)
GET /devices/backup/recovery[?version=N]The wrapped private key and salt for a restore. Without version, the current one; with it, the current or the retained previous version only - anything else is 404
DELETE /devices/backup/version?version=N {"password"}Discard a version and every key in it, immediately; takes the password the same way as POST. Deleting the current version makes the retained previous one current again. Fires KEY_BACKUP_UPDATE
PUT /devices/backup/keys?version=NUpload room keys to the current version; a superseded one answers 404, which is how a device that missed KEY_BACKUP_UPDATE learns the version moved
GET /devices/backup/keys?version=N[&after=<cursor>]One page of a version's keys - the current or the retained previous one

Errors#

json
{ "error": "missing permission to send messages in this channel" }
StatusWhen
400Invalid JSON, a missing or invalid field, an id that is not a snowflake; OAuth2 invalid_* errors
401No credential, or an invalid / expired / reset one
403Not a member, missing permission, role hierarchy, banned, or an OAuth2 token without the needed scope (insufficient_scope)
404No such space / room / message / role, or one you cannot see
409A conflict (an application that already has a bot, the application limit)
429Rate limited or slowmode; see below
500Something broke server-side. Retry with backoff
502, 503A service the instance relays to (a GIF provider) failed or is limiting the instance. Retry later

Rate limits#

WhatLimit
POST /oauth2/token, POST /oauth2/token/revoke20 per minute per IP
Creating applications, secrets and bot tokens20 per minute
Typing indicatorsOne broadcast per 5 s per room (extra calls succeed silently)
GET /gifs/search, GET /gifs/trending60 per minute per account, each
Gateway op 2 framesBurst of 20 per connection, refilling one every 250 ms
Messages in a room with slowmodeOne per slowmode_seconds per member; 429 with retry_after (seconds) and a Retry-After header. Members with Manage Messages are exempt
Space member profilesPer account: 60 per minute reading one member, 20 per minute PATCH, 10 per minute image uploads
Member roles (PUT …/members/:userId/roles, PUT / DELETE …/members/:userId/roles/:roleId)120 per minute per account
Sign-in, registration, key backupSeparate tight limits (not relevant to bots)

Other endpoints are not individually limited today; be a good citizen anyway - batch what you can, cache what does not change, and back off on 429 and 5xx.