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
| Intent | Gates | Notes |
|---|---|---|
lifecycle | Installed, Removed, InstallsLoaded | On by default. |
interactions | Interaction | On by default. |
messages | MessageCreated, MessageEdited, MessageDeleted | No-read bots get messages without text. |
reactions | ReactionAdded, ReactionRemoved | |
members | MemberJoined, MemberLeft, MemberUpdated | Privileged |
channels | ChannelCreated, ChannelUpdated, ChannelDeleted | |
voice | VoiceState | |
presence | (none yet) | Privileged. Accepted in config; no event is delivered yet. |
dms | DmMessageCreated | Direct 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)
| Event | Fields | Meaning |
|---|---|---|
Connected | user_id | The socket is up. |
Disconnected | reason, retry_in_ms | The socket dropped; the runtime retries after that wait. |
WaitingForInstall | Connected, but the bot’s user id is unknown until its first install. Set bot_user_id to skip this. | |
Fatal | reason, message | The runtime stopped for good. Reasons: TokenRefused, TokenRevoked, WrongBot, StateDir. |
Error | message | A server refusal of an earlier action, a dropped-event notice, or a runtime fault. |
Lifecycle
| Event | Fields |
|---|---|
Installed | server_id, kind (Reader, NoRead, Unknown) |
Removed | server_id |
InstallsLoaded | installs: a list of BotInstallInfo (below) |
Messages, reactions, DMs
| Event | Fields |
|---|---|
MessageCreated, MessageEdited | message: BotMessage |
MessageDeleted | server_id, channel_id, timestamp_ms |
ReactionAdded, ReactionRemoved | server_id, channel_id, message_ts, user_id, emoji |
DmMessageCreated | message: BotDmMessage |
Members, channels, voice
| Event | Fields |
|---|---|
MemberJoined, MemberUpdated | server_id, member: BotMember |
MemberLeft | server_id, user_id |
ChannelCreated, ChannelUpdated | server_id, channel: BotChannel |
ChannelDeleted | server_id, channel_id |
VoiceState | server_id, channel_id, user_id, joined |
Interactions
Interaction carries interaction: a BotInteraction.
Records
| Record | Fields |
|---|---|
BotInstallInfo | server_id, kind, permissions (integer bits), role_id, installer_user_id, installed_at_ms |
BotMessage | server_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 |
BotDmMessage | dm_channel_id, timestamp_ms, author_id, author_name, text, from_self |
BotMember | user_id, username, display_name, nickname, role_ids, is_bot |
BotChannel | channel_id, name, kind (text, voice, app, …), topic |
BotInteraction | interaction_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) |
BotOptionValue | Text, 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.
| Action | Arguments | Notes |
|---|---|---|
send_message | channel_id, text | 5 per 5 s per channel. |
reply | channel_id, reply_to_ts, text | As above. |
edit_message | channel_id, timestamp_ms, text | Not counted against the send limit. |
delete_message | channel_id, timestamp_ms | Not counted. |
add_reaction, remove_reaction | channel_id, message_ts, emoji | Not counted. |
send_dm | dm_channel_id, text | 20 new recipients an hour; only people in a server you share. |
send_dm_to_user | user_id, text | As above. |
assign_role, remove_role | server_id, user_id, role_id | Within the bot’s role and hierarchy. |
kick | server_id, user_id | |
ban | server_id, user_id, reason | |
unban | server_id, user_id | |
timeout_member | server_id, user_id, duration_ms, reason | |
clear_timeout | server_id, user_id | |
refresh_installs, installs | none | Ask again for the installs (you get InstallsLoaded); read the last answer. |
reply_to_interaction | interaction_id, content (optional) | Ephemeral reply to the invoker, or a bare acknowledgement when content is empty. |
register_commands | server_id (optional), commands_json | Answers NotYetSupported arriving (#1436). |
shutdown | none | Closes 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.
| Error | Node code | Meaning |
|---|---|---|
NotConnected | NOT_CONNECTED | No socket right now; the runtime is reconnecting. |
NotReady | NOT_READY | Connected, 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_ARGUMENT | A bad id, config value or argument. |
Refused { message } | REFUSED | The runtime declined, for example an encrypted send it cannot make safely. |
NotYetSupported { message } | NOT_YET_SUPPORTED | Waiting on another feature (named in the message). |
Stopped | STOPPED | The 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.
| Key | Required | Default | Meaning |
|---|---|---|---|
token | yes | The bot token. Must start with hxb_ and hold no whitespace. | |
state_dir | yes | Where the bot keeps its device keys, encrypted cache and event positions. Relative paths are relative to the config file. One bot per directory. | |
endpoint | no | wss://us-east-1.hexis.chat/ws/websocket | The 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_id | no | learned at first install | The bot account’s UUID. |
intents | no | lifecycle, interactions | A 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
| Variable | Read by | Meaning |
|---|---|---|
HEXIS_BOT_LOG | the runtime | debug logs routing decisions (servers, channels, fetches, held rows, interactions) to stderr. |
HEXIS_BOT_TOKEN | Python echo (without a file), Node example | The bot token. |
HEXIS_BOT_USER_ID | Python echo, Node example | The bot’s user id. |
HEXIS_BOT_STATE_DIR | Python echo | State directory, default ./bot-state. |
HEXIS_BOT_STATE | Node example | State directory, default ./bot-state. |
HEXIS_BOT_ENDPOINT | Python echo; the Node smoke test | The socket URL. For the Node smoke test it names a development backend; the test never defaults to production. |
HEXIS_BOT_SMOKE_ENDPOINT | Python smoke test | The 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.
| Route | Does | Answers |
|---|---|---|
GET /api/v1/oauth/apps | List your apps. | {ok, apps: [App], known_scopes} |
POST /api/v1/oauth/apps | Create 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_id | Change name, description, homepage_url, redirect_uris or allowed_scopes. | {ok, app} |
DELETE /api/v1/oauth/apps/:public_id | Revoke the app and its token. | {ok: true} |
POST /api/v1/oauth/apps/:public_id/bot_token | Replace 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
| Status | Body | When |
|---|---|---|
| 401 | error: "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. |
| 403 | error: "email_required" | The account has no email set yet; the account setup gate refuses the route. |
| 403 | error: "totp_required" | Create, update or reset without a recent second factor. |
| 403 | error: "app_killed" | Token reset on an app an operator killed. |
| 404 | error: "not_found" | Not your app, or no such app. |
| 409 | error: "app_limit" or "app_creation_limit", with max | 10 active apps, or 10 new apps in a day. On reset, conflict. |
| 422 | error: "invalid", details | A field failed validation. A token reset can also answer 422 with another reason as the error string. |
| 429 | error: "rate_limited", retry_in (seconds), and a Retry-After header | More 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.
| Code | Where | Meaning | What the runtime does |
|---|---|---|---|
| 403 | Upgrade response | The 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-After | Upgrade response | Too 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 socket | Where 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 1008 | Open socket | Policy: 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 error | Open socket | A 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.