Docs / Developers / Référence / The ctx object
GeneratedSDK 0.10.181 methods

The ctx object

Every method on ctx, grouped by namespace, with its signature, what it does and the capability it needs.

How ctx is organised
Context — the object your event handlers receive.

Every handler gets a Context that exposes:
  - ctx.log(message)                print to plugin audit log
  - ctx.kv.get/set/delete           per-server key-value storage
  - ctx.kv.list/get_many/set_many   batch KV operations
  - ctx.kv.increment/decrement      atomic counters
  - ctx.kv.list_values              get key-value pairs by prefix
  - ctx.discord.send_message        send Discord messages (with embeds)
  - ctx.discord.edit_message        edit a message by ID
  - ctx.discord.delete_message      delete a message by ID
  - ctx.discord.add_reaction        add a reaction emoji to a message
  - ctx.discord.get_member          look up a server member
  - ctx.discord.get_channel         look up a channel
  - ctx.discord.list_roles          list all roles in the server
  - ctx.discord.list_members        paginated member listing
  - ctx.discord.search_members      search members by name
  - ctx.discord.get_messages        fetch channel message history
  - ctx.discord.create_channel       create a channel (voice/text/category)
  - ctx.discord.edit_channel         edit a channel's properties
  - ctx.discord.delete_channel       delete a channel
  - ctx.discord.timeout_member       timeout a member
  - ctx.discord.ban_member           ban a member
  - ctx.discord.kick_member          kick a member
  - ctx.discord.add_role             add a role to a member
  - ctx.discord.remove_role          remove a role from a member
  - ctx.discord.*_bulk               bulk operations (add_role_bulk, etc.)
  - ctx.http.get/post/request        make HTTP requests (approved domains only)
  - ctx.interaction.respond           respond to a slash command or button
  - ctx.interaction.defer             acknowledge with "thinking..."
  - ctx.interaction.followup          send follow-up messages
  - ctx.interaction.send_modal        show a modal dialog
  - ctx.metrics.record                record a data point
  - ctx.metrics.query                 query aggregated metrics
  - ctx.metrics.total                 get a single aggregate total
  - ctx.sql.execute                   run DDL/DML statements
  - ctx.sql.query                     run SELECT queries
  - ctx.sql.query_one                 get single row
  - ctx.sql.scalar                    get single value
  - ctx.server_id                     the Discord server ID this install is for
  - ctx.plugin_id                     your plugin's ID

ctx

The context object passed to every event handler. Attributes: server_id Discord server (guild) ID as a string plugin_id Your plugin's ID string version Installed version string kv Key-value storage API discord Discord actions API http HTTP proxy API ws Persistent WebSocket API (requires proxy:websocket) interaction Interaction response API (slash commands, buttons, modals) metrics Time-series metrics API (available to all plugins) sql Sandboxed SQL API (requires storage:sql capability) ephemeral Fast rate counters, cooldowns, dedup (no capability required)

ctx.request_id property
Stable correlation ID for the current event. Returns the Discord interaction ID inside an interaction_create handler, or an empty string outside an event. Pass this through ctx.log(..., request_id=ctx.request_id) to correlate log lines across handler entry, RPC calls, and downstream work.
ctx.log(message: str, *, level: str = info, tags: Optional[List[str]] = None, **extra) -> None
Write to the plugin audit log. Visible to server admins in the dashboard. Args: message: Log message (max 4000 chars). level: "info", "warning", or "error". tags: Optional list of tags for filtering (e.g., ["moderation", "ban"]). **extra: Additional key-value context (e.g., user_id="123", reason="spam").
ctx.has_capability(cap: str) -> bool
Check if this install has a specific capability approved.

ctx.kv

11 methods

Per-server key-value store.

About this namespace
ctx.kv — key-value storage (requires storage:kv capability).
ctx.kv.get(key: str) -> Any storage:kv
Get a value by key. Returns the stored value, or None if not set.
ctx.kv.set(key: str, value: Any, *, ttl_seconds: int = 0) -> None storage:kv
Store a value. Value must be JSON-serialisable. Args: key: Storage key. value: JSON-serialisable value. ttl_seconds: Auto-expire after this many seconds (0 = no expiry).
ctx.kv.delete(key: str) -> None storage:kv
Delete a key (no-op if it doesn't exist).
ctx.kv.increment(key: str, amount: int = 1, *, path: str = ) -> Any storage:kv
Atomic increment in a single RPC round-trip. Much faster than get() + modify + set() for counters. Args: key: Storage key. path: Dot-separated path into a JSON object (e.g. "total"). Empty string = treat value as a plain integer. amount: How much to add (default 1). Returns: The new value after increment.
ctx.kv.list(prefix: str = , limit: int = 100, *, start_after: str = ) -> List[str] storage:kv
List stored key names, optionally filtered by prefix. The host caps this at **100 keys per call** — a larger limit is silently clamped, not an error. To walk more than 100 keys, page with start_after: pass the last key you received to get the next page. Keys come back in ascending order. Args: prefix: Only return keys starting with this string. limit: Max keys to return (1-100, default 100). start_after: Resume after this key (exclusive). Empty = from the start. Example:: cursor = "" while True: page = ctx.kv.list(prefix="user:", start_after=cursor) if not page: break for key in page: ... cursor = page[-1]
ctx.kv.get_many(keys: List[str]) -> Dict[str, Any] storage:kv
Batch get up to 50 keys. Returns {key: value}; missing keys omitted.
ctx.kv.exists(key: str) -> bool storage:kv
Check if a key exists without loading its value.
ctx.kv.count(prefix: str = ) -> int storage:kv
Count keys matching a prefix. Useful for pagination.
ctx.kv.set_many(entries: Dict[str, Any]) -> None storage:kv
Batch set up to 25 key-value pairs. All values must be JSON-serialisable.
ctx.kv.decrement(key: str, amount: int = 1) -> int storage:kv
Atomically decrement a numeric value. Creates with `-amount` if key doesn't exist. Returns the new value after decrementing. Requires capability: storage:kv
ctx.kv.list_values(prefix: str = , limit: int = 100) -> Dict[str, Any] storage:kv
List stored key-value pairs matching a prefix. Returns {key: value} dict. Up to 100 results. Avoids the N+1 problem of list() + get() calls. Requires capability: storage:kv

ctx.secrets

3 methods

Encrypted secrets the server owner enters for your plugin.

About this namespace
ctx.secrets — encrypted per-plugin secrets (requires storage:secrets capability).

Plugins use this to read sensitive values the dev configured in the dev
portal (API keys, signing secrets, etc.) without committing them to git.
Values are encrypted at rest with AES-GCM via the platform's master key.

Resolution order:
  1. ctx.secrets.get("FOO") first checks for a per-server override at
     the current server (set by the plugin itself via ctx.secrets.set
     or by an admin in a future per-server UI).
  2. Falls back to the dev-level default set by the plugin author in the
     dev portal Settings → Plugin secrets page.
  3. Returns None if neither is set.

Per-server values are scoped to ctx.server_id. They never bleed
across servers, never leak into KV, never appear in plugin stdout or
logs.
ctx.secrets.get(key: str) -> Optional[str] storage:secrets
Read a secret. Returns None if not set. Args: key: 1-64 chars, letters/digits/underscore/hyphen/dot only. Returns: Plaintext secret, or None if not set at either scope.
ctx.secrets.set(key: str, value: str) -> None storage:secrets
Store a per-server secret. Per-server values override the dev-level default for the current ctx.server_id only. To clear a value, call ctx.secrets.delete. Args: key: 1-64 chars (alphanumeric + _.-). value: 1-4096 chars (UTF-8).
ctx.secrets.delete(key: str) -> None storage:secrets
Remove a per-server secret. No-op if not set. Note: this only deletes the per-server override. The dev-level default (set in the dashboard) is unaffected.

ctx.sql

4 methods

Sandboxed Postgres, one schema per plugin and server.

About this namespace
ctx.sql — sandboxed SQL (requires storage:sql capability, staff-reviewed).

Each plugin gets an isolated Postgres schema. You can create tables,
insert data, and run queries — but cannot access platform tables.
ctx.sql.execute(sql: str, params: Optional[list] = None) -> int storage:sql
Execute DDL/DML (CREATE TABLE, INSERT, UPDATE, DELETE). Args: sql: SQL statement with %s placeholders for params. params: List of parameter values (optional). Returns: Number of rows affected. Example:: ctx.sql.execute( "INSERT INTO user_stats (user_id, messages) VALUES (%s, 1) " "ON CONFLICT (user_id) DO UPDATE SET messages = user_stats.messages + 1", ["123456"] )
ctx.sql.query(sql: str, params: Optional[list] = None, *, limit: int = 1000) -> "QueryResult" storage:sql
Run a SELECT query. Returns list of dicts, max 1000 rows. The return value is a plain list of row dicts that also carries a .truncated flag, True when the host clipped the result at limit. Check it before treating a result as complete — clipping is otherwise invisible. Args: sql: SELECT statement with %s placeholders. params: List of parameter values (optional). limit: Max rows to return (1-1000, default 1000). Example:: rows = ctx.sql.query( "SELECT user_id, messages FROM user_stats ORDER BY messages DESC LIMIT 10" ) for row in rows: print(row["user_id"], row["messages"]) if rows.truncated: ctx.log("warn", "result clipped — narrow the query or page with OFFSET")
ctx.sql.query_one(sql: str, params: Optional[list] = None) -> Optional[Dict[str, Any]] storage:sql
Run a SELECT and return the first row only (or None). Example:: count = ctx.sql.query_one("SELECT COUNT(*) AS cnt FROM user_stats") print(count["cnt"]) # 150
ctx.sql.scalar(sql: str, params: Optional[list] = None) -> Any storage:sql
Run a SELECT and return the first column of the first row. Convenience wrapper around query_one() for single-value queries. Example:: total = ctx.sql.scalar("SELECT COUNT(*) FROM user_stats") print(total) # 150

ctx.discord

37 methods

Discord actions, each gated by a capability.

About this namespace
ctx.discord — Discord actions (require specific capabilities).
ctx.discord.send_message(*, channel_id: str, content: str = , embeds: Optional[List[Dict[str, Any]]] = None, components: Optional[list] = None, files: Optional[List[Dict[str, str]]] = None, allowed_mentions: Optional[Dict[str, Any]] = None) -> Dict[str, Any] discord:send_message
Send a message. Returns dict with 'message_id' and 'channel_id'. Requires capability: discord:send_message At least one of content, embeds, components, or files must be provided. Components are ActionRow objects or raw dicts matching Discord's format. Args: files: List of file dicts, each with: - filename: Display name (e.g. "chart.png") - data_b64: Base64-encoded file content Max 3 files, 8 MB total. allowed_mentions: Discord allowed_mentions object controlling which mentions in content actually ping. Omit and NOTHING pings (the host defaults to {"parse": []}). The host sanitizes whatever you pass and always strips everyone/here, so those can never be triggered from a plugin. Example:: import base64 with open("chart.png", "rb") as f: data = base64.b64encode(f.read()).decode() ctx.discord.send_message( channel_id="123", content="Here's the chart:", files=[{"filename": "chart.png", "data_b64": data}], )
ctx.discord.edit_message(*, channel_id: str, message_id: str, content: Optional[str] = None, embeds: Optional[List[Dict[str, Any]]] = None, components: Optional[list] = None, allowed_mentions: Optional[Dict[str, Any]] = None) -> Dict[str, Any] discord:edit_message
Edit an existing message. Only bot-owned messages can be edited. Requires capability: discord:edit_message Pass None to leave a field unchanged. Pass an empty list to clear that field: - content="" — clear text - embeds=[] — clear all embeds - components=[] — clear all buttons / select menus allowed_mentions follows the same policy as send_message: omit it and nothing pings, and the host always strips everyone/here.
ctx.discord.delete_message(*, channel_id: str, message_id: str) -> None discord:delete_message
Delete a message. Requires capability: discord:delete_message
ctx.discord.bulk_delete_messages(*, channel_id: str, message_ids: List[str]) -> None discord:delete_message
Bulk delete 2-100 messages (must be < 14 days old). Requires capability: discord:delete_message
ctx.discord.add_reaction(*, channel_id: str, message_id: str, emoji: str) -> None discord:add_reaction
Add a reaction. emoji is a unicode char or 'name:id' for custom. Requires capability: discord:add_reaction
ctx.discord.get_member(*, user_id: str) -> "Member" discord:read
Look up a server member. Requires capability: discord:read Returns: user_id, username, display_name, nick, avatar, roles, joined_at, bot.
ctx.discord.get_channel(*, channel_id: str) -> "Channel" discord:read
Look up a channel. Requires capability: discord:read Returns: id, name, type, topic, parent_id, position, nsfw.
ctx.discord.list_roles() -> "List[Role]" discord:read
List all server roles. Requires capability: discord:read Returns list of: id, name, color, position, managed, mentionable.
ctx.discord.list_members(*, role_id: Optional[str] = None, limit: int = 100, after: Optional[str] = None) -> "List[Member]" discord:read
Paginated member listing. Requires capability: discord:read Args: role_id: Filter to members with this role (optional). limit: Max members to return (1-100, default 100). after: User ID to paginate after (cursor-based pagination). Returns list of: user_id, username, display_name, nick, avatar, roles, joined_at, bot.
ctx.discord.search_members(query: str, *, limit: int = 25) -> "List[Member]" discord:read
Search members by username or nickname. Requires capability: discord:read Args: query: Search string (matches username and nickname). limit: Max results (1-25, default 25). Returns list of: user_id, username, display_name, nick, roles, joined_at, bot.
ctx.discord.get_messages(*, channel_id: str, limit: int = 50, before: Optional[str] = None, after: Optional[str] = None) -> "List[Message]" discord:read
Fetch message history from a channel. Requires capability: discord:read Args: channel_id: The channel to fetch messages from. limit: Max messages (1-50, default 50). before: Fetch messages before this message ID. after: Fetch messages after this message ID. Returns list of: id, channel_id, author_id, author_username, author_bot, content, timestamp, edited_timestamp, attachments, embeds, pinned.
ctx.discord.iter_messages(*, channel_id: str, batch_size: int = 50, before: Optional[str] = None, after: Optional[str] = None) -> "Iterator[Message]" discord:read
Walk a channel's full message history, paging automatically. get_messages returns at most 50 messages per call; this generator keeps fetching pages and yields one message at a time so you can audit or export an entire channel without managing cursors yourself. Requires capability: discord:read. Direction: * default / before — walk newest → oldest (optionally starting before a given message id). * after — walk oldest → newest, starting after a given message id. Args: channel_id: The channel to walk. batch_size: Messages per underlying fetch (1-50, default 50). before: Start before this message id (newest→oldest walk). after: Start after this message id (oldest→newest walk). Yields: One message dict at a time (see get_messages for the shape).
ctx.discord.create_channel(*, name: str, channel_type: int = 0, category_id: Optional[str] = None, topic: Optional[str] = None, user_limit: Optional[int] = None) -> Dict[str, Any] discord:manage_channels
Create a channel. Requires capability: discord:manage_channels Args: name: Channel name (max 100 chars). channel_type: 0=text, 2=voice, 4=category, 13=stage, 15=forum. category_id: Parent category ID (optional). topic: Channel topic (text channels only, max 1024 chars). user_limit: Max users (voice channels only, 0-99). Returns: dict with id, name, type of the created channel.
ctx.discord.delete_channel(*, channel_id: str) -> None discord:manage_channels
Delete a channel. Requires capability: discord:manage_channels
ctx.discord.edit_channel(*, channel_id: str, name: Optional[str] = None, topic: Optional[str] = None, user_limit: Optional[int] = None) -> Dict[str, Any] discord:manage_channels
Edit a channel's properties. Requires capability: discord:manage_channels Pass None to leave a property unchanged.
ctx.discord.timeout_member(*, user_id: str, duration_seconds: int, reason: str = ) -> None discord:moderate_members
Timeout a member. Requires capability: discord:moderate_members Args: user_id: The member's Discord user ID. duration_seconds: How long to timeout (max 2419200 = 28 days). reason: Audit log reason (max 512 chars).
ctx.discord.ban_member(*, user_id: str, reason: str = , delete_message_seconds: int = 0) -> None discord:ban_members
Ban a member. Requires capability: discord:ban_members Args: user_id: The member's Discord user ID. reason: Audit log reason (max 512 chars). delete_message_seconds: How far back to delete messages (0-604800 = 7 days).
ctx.discord.unban_member(*, user_id: str) -> None discord:ban_members
Unban a user. Requires capability: discord:ban_members
ctx.discord.kick_member(*, user_id: str, reason: str = ) -> None discord:kick_members
Kick a member. Requires capability: discord:kick_members
ctx.discord.add_role(*, user_id: str, role_id: str, reason: str = ) -> None discord:manage_roles
Add a role to a member. Requires capability: discord:manage_roles
ctx.discord.remove_role(*, user_id: str, role_id: str, reason: str = ) -> None discord:manage_roles
Remove a role from a member. Requires capability: discord:manage_roles
ctx.discord.add_role_bulk(*, user_ids: List[str], role_id: str, reason: str = ) -> Dict[str, Any] discord:manage_roles
Add a role to multiple members. Max 25 users per call. Requires capability: discord:manage_roles Returns: {"success": int, "failed": int, "errors": [...]}
ctx.discord.remove_role_bulk(*, user_ids: List[str], role_id: str, reason: str = ) -> Dict[str, Any] discord:manage_roles
Remove a role from multiple members. Max 25 users per call. Requires capability: discord:manage_roles Returns: {"success": int, "failed": int, "errors": [...]}
ctx.discord.timeout_bulk(*, user_ids: List[str], duration_seconds: int, reason: str = ) -> Dict[str, Any] discord:moderate_members
Timeout multiple members. Max 25 users per call. Requires capability: discord:moderate_members Returns: {"success": int, "failed": int, "errors": [...]}
ctx.discord.kick_bulk(*, user_ids: List[str], reason: str = ) -> Dict[str, Any] discord:kick_members
Kick multiple members. Max 25 users per call. Requires capability: discord:kick_members Returns: {"success": int, "failed": int, "errors": [...]}
ctx.discord.set_channel_permissions(*, channel_id: str, target_id: str, allow: str = 0, deny: str = 0, target_type: int = 0) -> None discord:manage_channels
Set permission overwrites on a channel. Requires capability: discord:manage_channels Args: channel_id: The channel to modify. target_id: Role ID (target_type=0) or user ID (target_type=1). allow: Permission bitfield to allow (as string). deny: Permission bitfield to deny (as string). target_type: 0=role, 1=member. Note: Dangerous permissions (ADMINISTRATOR, MANAGE_GUILD, MANAGE_ROLES, MANAGE_WEBHOOKS, KICK/BAN_MEMBERS) are automatically stripped.
ctx.discord.delete_channel_permission(*, channel_id: str, target_id: str) -> None discord:manage_channels
Remove a permission overwrite from a channel. Requires capability: discord:manage_channels
ctx.discord.create_thread(*, channel_id: str, name: str, thread_type: int = 11, auto_archive_duration: int = 1440) -> Dict[str, Any] discord:manage_channels
Create a thread in a channel. Requires capability: discord:manage_channels Args: channel_id: Parent channel. name: Thread name (max 100 chars). thread_type: 11=public, 12=private. auto_archive_duration: Minutes until auto-archive (60, 1440, 4320, 10080). Returns: dict with id, name, type, archived.
ctx.discord.edit_thread(*, thread_id: str, archived: Optional[bool] = None, locked: Optional[bool] = None, name: Optional[str] = None, auto_archive_duration: Optional[int] = None) -> None discord:manage_channels
Edit a thread. Requires capability: discord:manage_channels Pass None to leave a property unchanged.
ctx.discord.pin_message(*, channel_id: str, message_id: str) -> None discord:send_message
Pin a message. Requires capability: discord:send_message
ctx.discord.unpin_message(*, channel_id: str, message_id: str) -> None discord:send_message
Unpin a message. Requires capability: discord:send_message
ctx.discord.get_guild() -> "Guild" discord:read
Get server info. Requires capability: discord:read Returns: id, name, icon, member_count, premium_tier, features, owner_id, description.
ctx.discord.list_channels() -> "List[Channel]" discord:read
List all channels in the server. Requires capability: discord:read Returns list of: id, name, type, parent_id, position. Channel types: 0=text, 2=voice, 4=category, 5=announcement, 13=stage, 15=forum.
ctx.discord.set_nickname(*, user_id: str, nickname: Optional[str] = None) -> None discord:moderate_members
Set a member's nickname. Pass None to reset. Requires capability: discord:moderate_members
ctx.discord.create_webhook(*, channel_id: str, name: str) -> Dict[str, Any] discord:manage_webhooks
Create a webhook. Requires capability: discord:manage_webhooks Returns: id, token, channel_id, name. Store the token — you need it for execute_webhook.
ctx.discord.execute_webhook(*, webhook_id: str, webhook_token: str, content: str = , embeds: Optional[List[Dict[str, Any]]] = None, username: Optional[str] = None, avatar_url: Optional[str] = None) -> Dict[str, Any] discord:manage_webhooks
Send a message via webhook. Requires capability: discord:manage_webhooks Args: webhook_id: Webhook ID from create_webhook(). webhook_token: Webhook token from create_webhook(). content: Message text. embeds: List of embed dicts. username: Override the webhook's display name. avatar_url: Override the webhook's avatar. Returns: dict with message_id.
ctx.discord.delete_webhook(*, webhook_id: str) -> None discord:manage_webhooks
Delete a webhook. Requires capability: discord:manage_webhooks

ctx.interaction

4 methods

Reply to the slash command, button or modal being handled.

About this namespace
ctx.interaction — respond to slash commands, buttons, selects, and modals.

Requires capability: interaction:respond

These methods only work inside an interaction_create event handler.
The interaction_id and token are automatically extracted from the
current event context.
ctx.interaction.respond(*, content: str = , embeds: Optional[List[Dict[str, Any]]] = None, components: Optional[list] = None, ephemeral: bool = False, allowed_mentions: Optional[Dict[str, Any]] = None, update_message: bool = False) -> None interaction:respond
Send an immediate response to the interaction. Must be called within 3 seconds of receiving the interaction. Can only be called once per interaction — use defer() + followup() for slow operations. Args: content: Message text (max 2000 chars). embeds: List of embed dicts (max 10). components: List of ActionRow objects or dicts. ephemeral: If True, only the interacting user sees the response. Ignored when update_message=True (the message being updated keeps its visibility). allowed_mentions: Discord allowed_mentions object controlling which mentions actually ping. Common shapes: {"parse": []} — suppress all pings {"parse": ["users"]} — only user mentions ping {"users": ["123", "456"]} — only these user IDs ping update_message: If True, EDIT the message the component is attached to (Discord UPDATE_MESSAGE) instead of sending a new message. Only valid inside a component (button / select menu) interaction handler — the platform rejects it for slash commands and modal submits. The fields you pass REPLACE the message's current content/embeds/components. Use this to update game boards, dashboards, paginated lists, etc. in place.
ctx.interaction.defer(*, ephemeral: bool = False) -> None interaction:respond
Acknowledge the interaction and show a "thinking..." indicator. You have 15 minutes after deferring to send a followup(). Use this when your handler needs more than 3 seconds to process.
ctx.interaction.followup(*, content: str = , embeds: Optional[List[Dict[str, Any]]] = None, components: Optional[list] = None, ephemeral: bool = False, allowed_mentions: Optional[Dict[str, Any]] = None) -> Dict[str, Any] interaction:respond
Send a follow-up message after defer(). Can be called multiple times. Each creates a new message. Returns a dict with the created message's identifiers — at minimum {"message_id": str, "channel_id": str} — so you can edit or delete it later. Returns an empty dict if the platform could not capture the response (e.g. transient parse failure); the message was still sent in that case. See respond() for allowed_mentions semantics.
ctx.interaction.send_modal(*, title: str, custom_id: str, fields: Optional[list] = None) -> None interaction:respond
Show a modal dialog to the user. Only works on slash command and component interactions (not on another modal submit). The user's response arrives as a new interaction_create event with interaction_type=5 (MODAL_SUBMIT). Args: title: Modal title (max 45 chars). custom_id: ID to match in your on_modal_submit handler (max 100 chars). fields: List of TextInput objects or ActionRow(TextInput(...)) objects.

ctx.http

5 methods

Outbound HTTP through the platform proxy.

About this namespace
ctx.http — outbound HTTP requests (requires proxy:http capability).
ctx.http.request(method: str, url: str, *, headers: Optional[Dict[str, str]] = None, body: Optional[str] = None, params: Optional[Dict[str, Any]] = None, secret_auth: Optional[str] = None, auth: Optional[Dict[str, Any]] = None) -> Dict[str, Any] proxy:http
Make an HTTP request. Only approved domains are reachable. params is a dict of query-string parameters; values may be strings or lists of strings (lists are encoded as repeated keys, e.g. {"k": ["a", "b"]} becomes k=a&k=b). The encoded query string is appended to url with the correct separator (? or &). secret_auth is the NAME of a stored secret (requires the storage:secrets capability and the secret must be bound to this URL's domain). The platform resolves it and sets the Authorization: Bearer <value> header for you — you never see the value, and you cannot set Authorization yourself (it is stripped). For a non-bearer scheme pass auth={"scheme": "basic"|"token", "secret": "KEY_NAME"}. Returns: status (int), headers (dict), body_bytes (str), truncated (bool).
ctx.http.get(url: str, *, headers: Optional[Dict[str, str]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any] proxy:http
GET url through the platform proxy. Shorthand for request("GET", ...). Returns the same dict as request: status, headers, body_bytes and truncated. Requires capability: proxy:http.
ctx.http.post(url: str, *, body: str, headers: Optional[Dict[str, str]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any] proxy:http
POST body to url through the platform proxy. Shorthand for request("POST", ...). body is sent as-is; set headers={"Content-Type": ...} to match it. Returns the same dict as request: status, headers, body_bytes and truncated. Requires capability: proxy:http.
ctx.http.allow_host(host: str) -> Dict[str, Any]
Authorize THIS server for HTTP requests to an admin-supplied host. Your proxy_domains_requested is fixed when you publish, so it can never name a destination only the installing server knows — their own game-server panel, their self-hosted API. Ask for the address in a setup command and pass it here: the platform verifies the caller is a server admin (Manage Server) and that the host is public, then remembers it for this server only. Must be called from a slash-command handler invoked by an admin, or from a dashboard handler where the platform verifies the viewer is a manager. host may be an IP, ip:port, hostname, or a URL (only the host part is used). Matched exactly, so subdomains are NOT included. Idempotent.
ctx.http.revoke_host(host: str) -> Dict[str, Any]
Revoke a previously approved HTTP host for this server (admin only).

ctx.ws

5 methods

Long-lived WebSocket connections through the platform broker.

About this namespace
ctx.ws — persistent WebSocket connections (requires proxy:websocket).

The platform's broker holds the actual socket; your plugin sends and
receives frames through it. Connections are identified by a short name
you choose. Inbound frames arrive as events — register handlers with
@plugin.on_ws_message(name) / on_ws_open / on_ws_close.

Pool-safe: ensure is idempotent, so call it whenever you need the
connection (e.g. on the first event, or in on_ready) — repeat calls for
the same name are a no-op.
ctx.ws.ensure(name: str, url: str, *, secret_auth: Optional[str] = None, auth: Optional[Dict[str, Any]] = None, subscribe: Optional[List[Any]] = None, binary: bool = False) -> Dict[str, Any] proxy:websocket
Idempotently open (or confirm) a managed WebSocket to url. url must be wss:// (or ws://) and the exact host must be in your proxy_domains_requested. secret_auth is a stored secret NAME bound to that host (the platform injects Authorization: Bearer <value> on connect; you never see it). subscribe is a list of frames sent right after every (re)connect — the place to (re)subscribe. Set binary=True for binary protocols (e.g. protobuf); inbound binary frames are delivered base64-encoded. Returns {"conn_id", "name", "state"}. There is no socket object — the platform owns it.
ctx.ws.send(name: str, data: Any) -> Dict[str, Any] proxy:websocket
Send one frame on the named connection. str -> text frame; bytes -> binary frame (sent as base64 over the wire to the broker).
ctx.ws.close(name: str) -> Dict[str, Any] proxy:websocket
Close the named connection (idempotent).
ctx.ws.allow_host(host: str) -> Dict[str, Any] proxy:websocket
Authorize this server to open WebSocket connections to host. For connecting to a destination the SERVER ADMIN supplies at setup time (e.g. their own game server's IP) — which a static manifest allowlist can't express. MUST be called from inside a slash-command handler run by a server admin (Manage Server): the platform verifies the invoking member is an admin and that host is a public address, then remembers it for THIS server only. After approval, ctx.ws.ensure(name, "wss://<host>:...") to that host succeeds. host may be an IP, ip:port, hostname, or a wss:// URL (only the host part is used). Raises if the caller isn't an admin or the host isn't public. Idempotent.
ctx.ws.revoke_host(host: str) -> Dict[str, Any] proxy:websocket
Revoke a previously approved host for this server (admin only).

ctx.ephemeral

6 methodsNo capability needed

Short-lived counters, cooldowns, flags and de-duplication.

About this namespace
ctx.ephemeral — fast, short-lived state for rate limiting, cooldowns, and dedup.

Redis-backed with automatic in-process fallback. All keys are scoped to
your plugin + server. TTL max is 24 hours — this is NOT persistent storage
(use ctx.kv for that).

No capability required — available to all plugins.
ctx.ephemeral.counter(key: str, window_seconds: int = 60) -> int
Increment a sliding-window counter. Returns the current count within the window. Use for rate limiting: if ctx.ephemeral.counter("spam:" + user_id, 60) > 5: ... Args: key: Counter name (max 256 chars). window_seconds: Sliding window size (1-86400, default 60).
ctx.ephemeral.cooldown_set(key: str, ttl_seconds: int = 60) -> None
Start a cooldown. Check with cooldown_check(). Args: key: Cooldown name (max 256 chars). ttl_seconds: How long the cooldown lasts (1-86400).
ctx.ephemeral.cooldown_check(key: str) -> Dict[str, Any]
Check if a cooldown is active. Returns {"active": bool, "remaining_seconds": float}.
ctx.ephemeral.dedup(key: str, ttl_seconds: int = 3600) -> bool
Check if this is the first time seeing this key. Returns True if new, False if seen before. Use for deduplication: if ctx.ephemeral.dedup(f"welcome:{user_id}"): send_welcome() Args: key: Dedup key (max 256 chars). ttl_seconds: How long to remember (1-86400, default 3600).
ctx.ephemeral.flag_set(key: str, ttl_seconds: int = 3600) -> None
Set a boolean flag with TTL. Check with flag_check(). Args: key: Flag name (max 256 chars). ttl_seconds: How long the flag stays set (1-86400).
ctx.ephemeral.flag_check(key: str) -> bool
Check if a flag is set. Returns True if active, False if expired or unset.

ctx.metrics

3 methodsNo capability needed

Numeric series your dashboard can chart.

About this namespace
ctx.metrics — time-series metrics storage (available to all plugins).

Record numeric data points with tags, then query aggregated results.
Platform handles storage, rollups, and retention (90 days).
ctx.metrics.record(metric: str, value: float = 1.0, tags: Optional[Dict[str, str]] = None) -> None
Record a data point. Args: metric: Metric name — 1-64 chars, must start with a letter or underscore, may contain only letters, digits, and underscores (no dots). value: Numeric value (default 1.0). tags: Optional key-value tags for grouping (max 10 tags). Example:: ctx.metrics.record("messages", 1, tags={"channel_id": "123"}) ctx.metrics.record("voice_minutes", 5.2, tags={"user_id": "456"})
ctx.metrics.query(metric: str, *, period: str = 7d, group_by: Optional[str] = None, aggregate: str = sum) -> Dict[str, Any]
Query aggregated metrics. Args: metric: Metric name to query. period: Time window — "1h", "24h", "7d", "30d", "90d". group_by: Optional tag key to group by (e.g., "channel_id"). aggregate: Aggregation function — "sum", "count", "avg", "min", "max". Returns: {"labels": ["2026-03-17", ...], "series": [{"name": "...", "data": [...]}], "total": float}
ctx.metrics.total(metric: str, *, period: str = 30d) -> float
Get a single aggregate total for a metric. Args: metric: Metric name. period: Time window — "1h", "24h", "7d", "30d", "90d". Returns: The total (sum) as a float.

Return shapes

Dicts the Discord methods return. Fields may be absent when Discord did not send them.

Channel

Returned by ctx.discord.get_channel / list_channels.

ChampTypeSignification
idstr
namestr
typeint0=text, 2=voice, 4=category, 13=stage, 15=forum
topicOptional[str]
parent_idOptional[str]category snowflake, if any
positionint
nsfwbool

Guild

Returned by ctx.discord.get_guild.

ChampTypeSignification
idstr
namestr
iconOptional[str]
member_countint
premium_tierint
featuresList[str]
owner_idstr
descriptionOptional[str]

Member

Returned by ctx.discord.get_member / list_members / search_members.

ChampTypeSignification
user_idstr
usernamestr
display_namestr
nickOptional[str]
avatarOptional[str]
rolesList[str]role snowflakes the member has
joined_atOptional[str]ISO-8601 timestamp
botbool

Message

Returned by ctx.discord.get_messages.

ChampTypeSignification
idstr
channel_idstr
author_idstr
author_usernamestr
author_botbool
contentstr
timestampOptional[str]ISO-8601 timestamp
edited_timestampOptional[str]
attachmentsintnumber of file attachments (count, not a list)
embedsintnumber of embeds (count, not a list)
pinnedbool

Role

Returned by ctx.discord.list_roles.

ChampTypeSignification
idstr
namestr
colorint
positionint
managedboolTrue for integration/bot-managed roles
mentionablebool
permissionsstrstring-encoded integer permission bitfield

Message components

Builders for buttons, select menus and text inputs. Each has to_dict() and can be passed straight to send_message or send_modal.

Button(label: 'str', custom_id: 'str' = '', *, style: 'str' = 'primary', emoji: 'Optional[str]' = None, url: 'Optional[str]' = None, disabled: 'bool' = False)
A clickable button component. Args: label: Button text (max 80 chars). custom_id: Developer-defined ID for handling clicks (max 100 chars). Not needed for link buttons. style: One of "primary", "secondary", "success", "danger", "link". emoji: Optional emoji string (e.g., "🎮" or a custom emoji dict). url: URL for link-style buttons (required if style="link"). disabled: Whether the button is greyed out.
SelectOption(label: 'str', value: 'str', *, description: 'Optional[str]' = None, emoji: 'Optional[str]' = None, default: 'bool' = False)
A single option within a SelectMenu. Args: label: Display text (max 100 chars). value: Developer-defined value returned on select (max 100 chars). description: Optional secondary text (max 100 chars). emoji: Optional emoji string. default: Whether this option is pre-selected.
SelectMenu(custom_id: 'str', options: 'Optional[List[SelectOption]]' = None, *, placeholder: 'str' = '', min_values: 'int' = 1, max_values: 'int' = 1, disabled: 'bool' = False)
A dropdown select menu component. Args: custom_id: Developer-defined ID for handling selections (max 100 chars). options: List of SelectOption objects (max 25). placeholder: Greyed-out text when nothing is selected (max 150 chars). min_values: Minimum selections required (default 1). max_values: Maximum selections allowed (default 1). disabled: Whether the menu is greyed out.
TextInput(label: 'str', custom_id: 'str', *, style: 'str' = 'short', placeholder: 'str' = '', value: 'str' = '', required: 'bool' = True, min_length: 'Optional[int]' = None, max_length: 'Optional[int]' = None)
A text input field for modal dialogs. Args: label: Input label shown to the user (max 45 chars). custom_id: Developer-defined ID (max 100 chars). style: "short" (single line) or "paragraph" (multi-line). placeholder: Greyed-out placeholder text (max 100 chars). value: Pre-filled value (max 4000 chars). required: Whether the field must be filled. min_length: Minimum input length (0-4000). max_length: Maximum input length (1-4000).
ActionRow(*children)
A container row for up to 5 components. An ActionRow can contain either: - Up to 5 Button components, OR - 1 SelectMenu component, OR - 1 TextInput component (in modals only) Args: *children: Component objects (Button, SelectMenu, or TextInput).
Button stylesdanger, link, primary, secondary, success
Text input stylesparagraph, short
YourBot docs Reference tables are generated from the code that is running. Ask in Discord Suggest a correction