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
ctx.log(message: str, *, level: str = info, tags: Optional[List[str]] = None, **extra) -> None
ctx.has_capability(cap: str) -> bool
ctx.kv
11 methodsPer-server key-value store.
About this namespace
ctx.kv — key-value storage (requires storage:kv capability).
ctx.kv.get(key: str) -> Any
storage:kv
ctx.kv.set(key: str, value: Any, *, ttl_seconds: int = 0) -> None
storage:kv
ctx.kv.delete(key: str) -> None
storage:kv
ctx.kv.increment(key: str, amount: int = 1, *, path: str = ) -> Any
storage:kv
ctx.kv.list(prefix: str = , limit: int = 100, *, start_after: str = ) -> List[str]
storage:kv
ctx.kv.get_many(keys: List[str]) -> Dict[str, Any]
storage:kv
ctx.kv.exists(key: str) -> bool
storage:kv
ctx.kv.count(prefix: str = ) -> int
storage:kv
ctx.kv.set_many(entries: Dict[str, Any]) -> None
storage:kv
ctx.kv.decrement(key: str, amount: int = 1) -> int
storage:kv
ctx.kv.list_values(prefix: str = , limit: int = 100) -> Dict[str, Any]
storage:kv
ctx.secrets
3 methodsEncrypted 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
ctx.secrets.set(key: str, value: str) -> None
storage:secrets
ctx.secrets.delete(key: str) -> None
storage:secrets
ctx.sql
4 methodsSandboxed 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
ctx.sql.query(sql: str, params: Optional[list] = None, *, limit: int = 1000) -> "QueryResult"
storage:sql
ctx.sql.query_one(sql: str, params: Optional[list] = None) -> Optional[Dict[str, Any]]
storage:sql
ctx.sql.scalar(sql: str, params: Optional[list] = None) -> Any
storage:sql
ctx.discord
37 methodsDiscord 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
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
ctx.discord.delete_message(*, channel_id: str, message_id: str) -> None
discord:delete_message
ctx.discord.bulk_delete_messages(*, channel_id: str, message_ids: List[str]) -> None
discord:delete_message
ctx.discord.add_reaction(*, channel_id: str, message_id: str, emoji: str) -> None
discord:add_reaction
ctx.discord.get_member(*, user_id: str) -> "Member"
discord:read
ctx.discord.get_channel(*, channel_id: str) -> "Channel"
discord:read
ctx.discord.list_roles() -> "List[Role]"
discord:read
ctx.discord.list_members(*, role_id: Optional[str] = None, limit: int = 100, after: Optional[str] = None) -> "List[Member]"
discord:read
ctx.discord.search_members(query: str, *, limit: int = 25) -> "List[Member]"
discord:read
ctx.discord.get_messages(*, channel_id: str, limit: int = 50, before: Optional[str] = None, after: Optional[str] = None) -> "List[Message]"
discord:read
ctx.discord.iter_messages(*, channel_id: str, batch_size: int = 50, before: Optional[str] = None, after: Optional[str] = None) -> "Iterator[Message]"
discord:read
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
ctx.discord.delete_channel(*, channel_id: str) -> None
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
ctx.discord.timeout_member(*, user_id: str, duration_seconds: int, reason: str = ) -> None
discord:moderate_members
ctx.discord.ban_member(*, user_id: str, reason: str = , delete_message_seconds: int = 0) -> None
discord:ban_members
ctx.discord.unban_member(*, user_id: str) -> None
discord:ban_members
ctx.discord.kick_member(*, user_id: str, reason: str = ) -> None
discord:kick_members
ctx.discord.add_role(*, user_id: str, role_id: str, reason: str = ) -> None
discord:manage_roles
ctx.discord.remove_role(*, user_id: str, role_id: str, reason: str = ) -> None
discord:manage_roles
ctx.discord.add_role_bulk(*, user_ids: List[str], role_id: str, reason: str = ) -> Dict[str, Any]
discord:manage_roles
ctx.discord.remove_role_bulk(*, user_ids: List[str], role_id: str, reason: str = ) -> Dict[str, Any]
discord:manage_roles
ctx.discord.timeout_bulk(*, user_ids: List[str], duration_seconds: int, reason: str = ) -> Dict[str, Any]
discord:moderate_members
ctx.discord.kick_bulk(*, user_ids: List[str], reason: str = ) -> Dict[str, Any]
discord:kick_members
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
ctx.discord.delete_channel_permission(*, channel_id: str, target_id: str) -> None
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
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
ctx.discord.pin_message(*, channel_id: str, message_id: str) -> None
discord:send_message
ctx.discord.unpin_message(*, channel_id: str, message_id: str) -> None
discord:send_message
ctx.discord.get_guild() -> "Guild"
discord:read
ctx.discord.list_channels() -> "List[Channel]"
discord:read
ctx.discord.set_nickname(*, user_id: str, nickname: Optional[str] = None) -> None
discord:moderate_members
ctx.discord.create_webhook(*, channel_id: str, name: str) -> Dict[str, Any]
discord:manage_webhooks
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
ctx.discord.delete_webhook(*, webhook_id: str) -> None
discord:manage_webhooks
ctx.interaction
4 methodsReply 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
ctx.interaction.defer(*, ephemeral: bool = False) -> None
interaction:respond
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
ctx.interaction.send_modal(*, title: str, custom_id: str, fields: Optional[list] = None) -> None
interaction:respond
ctx.http
5 methodsOutbound 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
ctx.http.get(url: str, *, headers: Optional[Dict[str, str]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]
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
ctx.http.allow_host(host: str) -> Dict[str, Any]
ctx.http.revoke_host(host: str) -> Dict[str, Any]
ctx.ws
5 methodsLong-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
ctx.ws.send(name: str, data: Any) -> Dict[str, Any]
proxy:websocket
ctx.ws.close(name: str) -> Dict[str, Any]
proxy:websocket
ctx.ws.allow_host(host: str) -> Dict[str, Any]
proxy:websocket
ctx.ws.revoke_host(host: str) -> Dict[str, Any]
proxy:websocket
ctx.ephemeral
6 methodsNo capability neededShort-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
ctx.ephemeral.cooldown_set(key: str, ttl_seconds: int = 60) -> None
ctx.ephemeral.cooldown_check(key: str) -> Dict[str, Any]
ctx.ephemeral.dedup(key: str, ttl_seconds: int = 3600) -> bool
ctx.ephemeral.flag_set(key: str, ttl_seconds: int = 3600) -> None
ctx.ephemeral.flag_check(key: str) -> bool
ctx.metrics
3 methodsNo capability neededNumeric 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
ctx.metrics.query(metric: str, *, period: str = 7d, group_by: Optional[str] = None, aggregate: str = sum) -> Dict[str, Any]
ctx.metrics.total(metric: str, *, period: str = 30d) -> 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.
| Campo | Tipo | Significado |
|---|---|---|
id | str | |
name | str | |
type | int | 0=text, 2=voice, 4=category, 13=stage, 15=forum |
topic | Optional[str] | |
parent_id | Optional[str] | category snowflake, if any |
position | int | |
nsfw | bool |
Guild
Returned by ctx.discord.get_guild.
| Campo | Tipo | Significado |
|---|---|---|
id | str | |
name | str | |
icon | Optional[str] | |
member_count | int | |
premium_tier | int | |
features | List[str] | |
owner_id | str | |
description | Optional[str] |
Member
Returned by ctx.discord.get_member / list_members / search_members.
| Campo | Tipo | Significado |
|---|---|---|
user_id | str | |
username | str | |
display_name | str | |
nick | Optional[str] | |
avatar | Optional[str] | |
roles | List[str] | role snowflakes the member has |
joined_at | Optional[str] | ISO-8601 timestamp |
bot | bool |
Message
Returned by ctx.discord.get_messages.
| Campo | Tipo | Significado |
|---|---|---|
id | str | |
channel_id | str | |
author_id | str | |
author_username | str | |
author_bot | bool | |
content | str | |
timestamp | Optional[str] | ISO-8601 timestamp |
edited_timestamp | Optional[str] | |
attachments | int | number of file attachments (count, not a list) |
embeds | int | number of embeds (count, not a list) |
pinned | bool |
Role
Returned by ctx.discord.list_roles.
| Campo | Tipo | Significado |
|---|---|---|
id | str | |
name | str | |
color | int | |
position | int | |
managed | bool | True for integration/bot-managed roles |
mentionable | bool | |
permissions | str | string-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)
SelectOption(label: 'str', value: 'str', *, description: 'Optional[str]' = None, emoji: 'Optional[str]' = None, default: 'bool' = False)
SelectMenu(custom_id: 'str', options: 'Optional[List[SelectOption]]' = None, *, placeholder: 'str' = '', min_values: 'int' = 1, max_values: 'int' = 1, disabled: 'bool' = False)
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)
ActionRow(*children)