Reference

Events, actions, errors, configuration and routes, as the runtime defines them. Names below are the Rust ones; the Python SDK uses upper-case nested names (BotEvent.MESSAGE_CREATED) and the Node SDK uses lower-case types (message, dm_message, member_joined, see types.ts in the SDK).

Ids and times

Ids (servers, channels, users, roles, DMs) are hyphenated lowercase UUID strings. 32 bare hex digits are accepted as input. A message is addressed by its channel id and timestamp_ms, the server’s timestamp for it. Times are milliseconds since the epoch.

Intents

IntentGatesNotes
lifecycleInstalled, Removed, InstallsLoadedOn by default.
interactionsInteractionOn by default.
messagesMessageCreated, MessageEdited, MessageDeletedNo-read bots get messages without text.
reactionsReactionAdded, ReactionRemoved
membersMemberJoined, MemberLeft, MemberUpdatedPrivileged
channelsChannelCreated, ChannelUpdated, ChannelDeleted
voiceVoiceState
presence(none yet)Privileged. Accepted in config; no event is delivered yet.
dmsDmMessageCreatedDirect messages to the bot.

Config names are lower case. Unknown names are an error. Connected, Disconnected, WaitingForInstall, Fatal and Error are delivered whatever the intents.

Events

Runtime (always delivered)

EventFieldsMeaning
Connecteduser_idThe socket is up.
Disconnectedreason, retry_in_msThe socket dropped; the runtime retries after that wait.
WaitingForInstallConnected, but the bot’s user id is unknown until its first install. Set bot_user_id to skip this.
Fatalreason, messageThe runtime stopped for good. Reasons: TokenRefused, TokenRevoked, WrongBot, StateDir.
ErrormessageA server refusal of an earlier action, a dropped-event notice, or a runtime fault.

Lifecycle

EventFields
Installedserver_id, kind (Reader, NoRead, Unknown)
Removedserver_id
InstallsLoadedinstalls: a list of BotInstallInfo (below)

Messages, reactions, DMs

EventFields
MessageCreated, MessageEditedmessage: BotMessage
MessageDeletedserver_id, channel_id, timestamp_ms
ReactionAdded, ReactionRemovedserver_id, channel_id, message_ts, user_id, emoji
DmMessageCreatedmessage: BotDmMessage

Members, channels, voice

EventFields
MemberJoined, MemberUpdatedserver_id, member: BotMember
MemberLeftserver_id, user_id
ChannelCreated, ChannelUpdatedserver_id, channel: BotChannel
ChannelDeletedserver_id, channel_id
VoiceStateserver_id, channel_id, user_id, joined

Interactions

Interaction carries interaction: a BotInteraction.

Records

RecordFields
BotInstallInfoserver_id, kind, permissions (integer bits), role_id, installer_user_id, installed_at_ms
BotMessageserver_id, channel_id, timestamp_ms, author_id, author_name, text (none when the bot cannot read it: a no-read install in an encrypted server, or keys that never arrived), reply_to_ts, edited_at_ms, mentioned_user_ids, mentions_me, from_self (sent by any device of this bot; skip these in an echo bot), attachment_count
BotDmMessagedm_channel_id, timestamp_ms, author_id, author_name, text, from_self
BotMemberuser_id, username, display_name, nickname, role_ids, is_bot
BotChannelchannel_id, name, kind (text, voice, app, …), topic
BotInteractioninteraction_id, server_id, channel_id, kind (Command, Click, Mention), message_ts (the message a click or mention is about, else 0), created_ms, invoker_user_id (the verified signer), invoker_device_id (empty on a plaintext server), sealed, command_name, options (list of name and BotOptionValue), custom_id, values (a select’s values), mention_text, payload (raw bytes)
BotOptionValueText, Integer, Number, Boolean, Id (a user, channel or role id), or None

Actions

Every action is one runtime call, answered at once: either a local refusal (nothing was sent) or “handed to the socket”. A refusal by the server comes back later as an Error event. In Python they are awaitable, in Node they return promises; names are snake_case in Python and camelCase in Node.

ActionArgumentsNotes
send_messagechannel_id, text5 per 5 s per channel.
replychannel_id, reply_to_ts, textAs above.
edit_messagechannel_id, timestamp_ms, textNot counted against the send limit.
delete_messagechannel_id, timestamp_msNot counted.
add_reaction, remove_reactionchannel_id, message_ts, emojiNot counted.
send_dmdm_channel_id, text20 new recipients an hour; only people in a server you share.
send_dm_to_useruser_id, textAs above.
assign_role, remove_roleserver_id, user_id, role_idWithin the bot’s role and hierarchy.
kickserver_id, user_id
banserver_id, user_id, reason
unbanserver_id, user_id
timeout_memberserver_id, user_id, duration_ms, reason
clear_timeoutserver_id, user_id
refresh_installs, installsnoneAsk again for the installs (you get InstallsLoaded); read the last answer.
reply_to_interactioninteraction_id, content (optional)Ephemeral reply to the invoker, or a bare acknowledgement when content is empty.
register_commandsserver_id (optional), commands_jsonAnswers NotYetSupported arriving (#1436).
shutdownnoneCloses the socket, saves state. Queued events can still be read.

Rust also has next_event(timeout_ms) (waits up to the timeout, None on timeout or when the runtime has stopped) and is_finished(). The Python and Node layers turn those into events() and run_forever() / bot.on(...). A bot socket may only call the requests the server allows bots (messaging, reactions, DMs, roles, channels, voice, the encryption calls, moderation, leaving a server, its own profile and devices); it can never join a server or create an invite.

Errors

An action that fails locally returns or raises one of these. Nothing was sent.

ErrorNode codeMeaning
NotConnectedNOT_CONNECTEDNo socket right now; the runtime is reconnecting.
NotReadyNOT_READYConnected, but the bot has no install yet.
RateLimited { retry_after_ms, limit }RATE_LIMITED (retryAfterMs, limit)Over a local limit; retry after retry_after_ms. limit names it: 5 messages per 5 s per channel or the DM recipient limit.
InvalidArgument { message }INVALID_ARGUMENTA bad id, config value or argument.
Refused { message }REFUSEDThe runtime declined, for example an encrypted send it cannot make safely.
NotYetSupported { message }NOT_YET_SUPPORTEDWaiting on another feature (named in the message).
StoppedSTOPPEDThe runtime ended: shut down or fatal.

Python raises BotError.<Variant> for these and BotFatalError from run() after a Fatal event. Node throws BotError with a code, and emits server refusals as a runtime_error event (not error, which throws in an event emitter with no listener).

Configuration

The TOML file given to hexis-bot run, python -m hexis_bot.echo and bot_config_from_file. Unknown keys are an error.

KeyRequiredDefaultMeaning
tokenyesThe bot token. Must start with hxb_ and hold no whitespace.
state_diryesWhere the bot keeps its device keys, encrypted cache and event positions. Relative paths are relative to the config file. One bot per directory.
endpointnowss://us-east-1.hexis.chat/ws/websocketThe socket URL. Use wss://. ws:// sends the hxb_ token in cleartext, so use it only for a local development backend. Must not contain token=.
bot_user_idnolearned at first installThe bot account’s UUID.
intentsnolifecycle, interactionsA list of intent names.

The Node constructor takes the same values as token, stateDir, endpoint, botUserId, intents. The echo bots default to the intents lifecycle, interactions, messages and dms.

Environment variables

VariableRead byMeaning
HEXIS_BOT_LOGthe runtimedebug logs routing decisions (servers, channels, fetches, held rows, interactions) to stderr.
HEXIS_BOT_TOKENPython echo (without a file), Node exampleThe bot token.
HEXIS_BOT_USER_IDPython echo, Node exampleThe bot’s user id.
HEXIS_BOT_STATE_DIRPython echoState directory, default ./bot-state.
HEXIS_BOT_STATENode exampleState directory, default ./bot-state.
HEXIS_BOT_ENDPOINTPython echo; the Node smoke testThe socket URL. For the Node smoke test it names a development backend; the test never defaults to production.
HEXIS_BOT_SMOKE_ENDPOINTPython smoke testThe development backend the Python smoke test connects to (for example ws://localhost:4000/ws/websocket). Never point a test at production.

These variables are conveniences of the examples, not part of the runtime’s API; your own bot reads its settings however it likes.

HTTP routes

App management lives on the regional gateway (https://us-east-1.hexis.chat) under /api/v1. These routes need a signed-in account session (the same cookie the account portal uses), not the bot token. Create, update and reset ask for a fresh second factor when the account has one.

RouteDoesAnswers
GET /api/v1/oauth/appsList your apps.{ok, apps: [App], known_scopes}
POST /api/v1/oauth/appsCreate an app and its bot. Body: name, description, homepage_url, redirect_uris (list), allowed_scopes (list), is_public_client.201 {ok, app: App + client_id, client_secret, bot_token, bot_user_id}. The secret and token are shown once.
PATCH /api/v1/oauth/apps/:public_idChange name, description, homepage_url, redirect_uris or allowed_scopes.{ok, app}
DELETE /api/v1/oauth/apps/:public_idRevoke the app and its token.{ok: true}
POST /api/v1/oauth/apps/:public_id/bot_tokenReplace the bot token, or mint the first one for an older app. Closes every socket on the old token.{ok, bot_token, bot_user_id, app}. Shown once.

The App object

is_public_client means an OAuth PKCE client (no client secret); it has nothing to do with a public bot. The fields are public_id, name, description, homepage_url, redirect_uris, allowed_scopes, is_public_client, approved, approved_at, created_at, has_bot_token, bot_user_id, bot_token_issued_at, suspended (an operator killed the bot). It never holds the token or its hash. client_id appears on the create answer. A privileged_intents field is arriving with the event gateway arriving.

Route errors

StatusBodyWhen
401error: "not_authenticated"No signed-in web session (or, on create, update and reset, no session to check the second factor against).
400{ok: false, error: "invalid_scope"}An unknown scope.
403error: "email_required"The account has no email set yet; the account setup gate refuses the route.
403error: "totp_required"Create, update or reset without a recent second factor.
403error: "app_killed"Token reset on an app an operator killed.
404error: "not_found"Not your app, or no such app.
409error: "app_limit" or "app_creation_limit", with max10 active apps, or 10 new apps in a day. On reset, conflict.
422error: "invalid", detailsA field failed validation. A token reset can also answer 422 with another reason as the error string.
429error: "rate_limited", retry_in (seconds), and a Retry-After headerMore than 20 create attempts in an hour.

Status and close codes on the bot socket

The bot socket is a WebSocket on the endpoint above. The token goes in the x-api-key header of the upgrade request.

CodeWhereMeaningWhat the runtime does
403Upgrade responseThe token is unknown, reset, revoked, or the app was killed or its owner is suspended; or the runtime is below the server’s version floor.Stops with Fatal (TokenRefused; TokenRevoked right after a 1008).
429 + Retry-AfterUpgrade responseToo many connects from this address. Not a dead token.Waits what Retry-After says (at least 1 s, at most 10 min), then retries.
close 1000 (normal)Open socketWhere a token reset, revoke, kill or suspension ends a live socket.Reports Disconnected, backs off briefly and reconnects. The upgrade then meets a 403 and the runtime stops with Fatal(TokenRefused).
close 1008Open socketPolicy: the token died while the socket was still connecting.Retries once at once to learn which it is. A 403 then ends in Fatal(TokenRevoked).
other close or errorOpen socketA lost connection.Reconnects with backoff (1 s doubling to 60 s).

What you see for a dead token, then, is almost always a Disconnected event, a short wait, and Fatal with TokenRefused. For a suspended owner the token works again once the suspension ends; start the bot again.

Request-level refusals on an open socket: a call a bot may not make is refused as not accessible; a rate-limited send is refused with a “Rate limit: wait Ns.” message; a direct message to someone who shares no server with the bot is refused with “Bots can only message people in a server they share.” Both show up as Error events.