dc = DiscordClient()cordslite API
Discord has three main APIs: - REST API - for actions (send message, get channels, fetch history). Request/response style. - Gateway API - for real-time events via WebSocket (new messages, reactions, user joins). - Voice API - for real-time streaming of audio via UDP
We start with the REST API. To use it, you need a bot token from the Discord Developer Portal: 1. Create an application 2. Go to “Bot” section and create a bot 3. Copy the token (keep it secret!) 4. Under “OAuth2 → URL Generator”, select bot scope and choose permissions (e.g., Send Messages, Read Message History) 5. Use the generated URL to invite the bot to your server
The DiscordClient wraps httpx.Client with Discord’s base URL (https://discord.com/api/v10) and auth headers pre-configured.
Failed to import opuslib-next
DiscordClient
def DiscordClient(
token:NoneType=None, user_token:NoneType=None, name:str='cordslite', ver:str='0.1'
):Initialize self. See help(type(self)) for accurate signature.
The token defaults to the DISCORD_BOT_TOKEN environment variable if none is provided.
REST endpoints
Every REST call goes through _req, which handles two concerns automatically: - Rate limiting — if Discord returns 429 Too Many Requests, we wait the Retry-After duration and retry. - Error handling — non-2xx responses are raised as DiscordError with Discord’s error code, message, and HTTP status, so callers don’t need to check responses manually.
DiscordError
def DiscordError(
code, msg, status
):Common base class for all non-exit exceptions.
Discord’s REST endpoints return JSON with many fields. Rather than defining properties for every field, we use a flexible base class pattern:
DiscordObject provides __getitem__ and __getattr__ so you can access data as obj.name or obj['name']. The __dir__ method enables autocomplete in notebooks and solveit dialogs—just type obj. and see all available fields!
This means when Discord adds new fields to their API, our code automatically supports them without changes.
DiscordObject
def DiscordObject(
data, client
):dict subclass that also provides access to keys as attrs, and has a pretty markdown repr
Discord’s hierarchy is Guild → Channels → Messages. A Guild (server) contains channels, and channels contain messages. Each has its own REST endpoints: - GET /guilds/{id} - fetch guild info - GET /guilds/{id}/channels - list channels - GET /channels/{id}/messages - fetch messages - POST /channels/{id}/messages - send a message
We build wrapper classes for each, inheriting from DiscordObject and just adding a nice __repr__.
Guild
def Guild(
data, client
):dict subclass that also provides access to keys as attrs, and has a pretty markdown repr
We use @patch from fastcore to add methods incrementally—great for interactive development where you want to test each piece as you build it. This guild method fetches guild data and wraps it in our Guild class.
DiscordClient.guild
async def guild(
guild_id
):Call self as a function.
gid = '1493461895615873044'
gld = await dc.guild(gid); gldGuild(id=1327046393453613076, name="natedog's server")Channels have a type field: 0=text, 2=voice, 4=category. The Channels class inherits from list and adds _repr_html_ for nice table display in notebooks/solveit. This pattern—a wrapper class plus a collection class with HTML repr—makes exploring the API much more pleasant.
Guild.channels
async def channels(
limit:NoneType=None
):Call self as a function.
Channels
def Channels(
*args, **kwargs
):Built-in mutable sequence.
If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.
Channel
def Channel(
data, client
):dict subclass that also provides access to keys as attrs, and has a pretty markdown repr
html_table
def html_table(
items, hdrs, fn
):Call self as a function.
chs = await gld.channels()
chs| ID | Name | Type |
|---|---|---|
| 1327046393453613077 | Text Channels | 4 |
| 1327046393453613078 | Voice Channels | 4 |
| 1327046393453613079 | general | 0 |
| 1327046393453613080 | General | 2 |
| 1327954661960978512 | private | 0 |
| 1475600987627458812 | forwarded | 0 |
| 1501265913222266961 | test-webhooks | 0 |
| 1506656999293849753 | test | 0 |
| 1506657701688508456 | test-vch | 2 |
DiscordClient.channel
async def channel(
channel_id
):Call self as a function.
Sometimes you don’t know the guild ID up front. dc.guilds() lists every guild the bot has been invited to, using Discord’s /users/@me/guilds endpoint (which returns partial guild objects—id, name, owner, permissions). Handy for building navigation UIs without hardcoding IDs.
DiscordClient.guilds
async def guilds(
limit:Unset=UNSET
):List the guilds the bot is a member of
Guilds
def Guilds(
*args, **kwargs
):Built-in mutable sequence.
If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.
glds = await dc.guilds()
assert any(g.id == gid for g in glds)
glds| ID | Name |
|---|---|
| 1200111522916094103 | Answer.ai |
| 1311580085442318366 | erikgaas's server |
| 1327046393453613076 | natedog's server |
| 1344332715226300458 | Kerem's server |
chid = '1493461896139903028'
ch = await dc.channel(chid)
chMessages are the core of most bot functionality. Note that Discord returns messages in reverse chronological order (newest first), so we reverse() the data to get chronological order. The table shows a preview of content, author, and timestamp.
Note: To read message content, your bot needs the MESSAGE_CONTENT privileged intent enabled in the Developer Portal! Without it, the content field will be empty for messages not sent by your bot or mentioning it.
Channel.messages
async def messages(
limit:int=50, before:Unset=UNSET, after:Unset=UNSET, around:Unset=UNSET
):Fetch channel messages. before, after, and around are mutually exclusive message IDs.
Messages
def Messages(
*args, **kwargs
):Built-in mutable sequence.
If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.
Message
def Message(
data, dobj
):dict subclass that also provides access to keys as attrs, and has a pretty markdown repr
msgs = await ch.messages(5)
msgs| ID | Author | Content | Date |
|---|---|---|---|
| 1507463593162047658 | DBuddy | Test our event listener! Otters are awesome 🦦 | 2026-05-22 |
| 1507463611893809202 | DBuddy | Houston, do we have a problem? | 2026-05-22 |
| 1507463630873039008 | DBuddy | Did we re-identify? | 2026-05-22 |
| 1523685329700393064 | DBuddy | cordslite voice-robustness validation: gateway res | 2026-07-06 |
| 1523696905593557075 | DBuddy | Back from the dead? (listener auto-resume) | 2026-07-06 |
Message.around
async def around(
limit:int=11
):Fetch messages around this message in the same channel/thread.
Message.after
async def after(
limit:int=5
):Fetch messages after this message in the same channel/thread.
Message.before
async def before(
limit:int=5
):Fetch messages before this message in the same channel/thread.
Message.url
def url():Call self as a function.
Channel.url
def url():Call self as a function.
Guild.url
def url():Call self as a function.
gld.url, ch.url, msgs[0].url('https://discord.com/channels/1327046393453613076',
'https://discord.com/channels/1327046393453613076/1327046393453613079',
'https://discord.com/channels/1327046393453613076/1327046393453613079/1507463593162047658')
Guild search uses snowflake IDs for date filtering (min_id/max_id), but we autoconvert 'YYYY-MM-DD' strings for convenience for before/after.
Guild.search
async def search(
content:Unset=UNSET, author_id:Unset=UNSET, channel_id:Unset=UNSET, mentions:Unset=UNSET, has:Unset=UNSET,
before:Unset=UNSET, after:Unset=UNSET, pinned:Unset=UNSET, sort_by:Unset=UNSET, sort_order:Unset=UNSET,
offset:Unset=UNSET, limit:Unset=UNSET, use_user:bool=False, nothread:bool=True
):Search guild messages. before/after accept ‘YYYY-MM-DD’ strings or snowflake IDs.
date2snowflake
def date2snowflake(
date_str
):Convert ‘YYYY-MM-DD’ to a Discord snowflake ID
msgs = await gld.search(after='2026-02-16', limit=5)
msgs| ID | Author | Content | Date |
|---|---|---|---|
| 1523696905593557075 | DBuddy | Back from the dead? (listener auto-resume) | 2026-07-06 |
| 1523685329700393064 | DBuddy | cordslite voice-robustness validation: gateway res | 2026-07-06 |
| 1516773826589888522 | Captain Hook | Usage logging failed for instance -1: instance[-1] | 2026-06-17 |
| 1516773813948518441 | Captain Hook | Usage billing failed: 0 row(s) completed, usage_id | 2026-06-17 |
| 1516773578807316492 | Captain Hook | Usage logging failed for instance -1: instance[-1] | 2026-06-17 |
Sometimes you need to search by name rather than snowflake ID. find_member searches the guild’s members by username, nickname, or display name using Discord’s member search endpoint, and returns the first match’s user ID. This makes it easy to chain into search.
Guild.find_member
async def find_member(
name
):Search guild members by name/nick, return first match’s user ID or None
uid = await gld.find_member('nate.dawgg')
assert uid
await gld.search(author_id=uid, limit=5)| ID | Author | Content | Date |
|---|---|---|---|
| 1506695870760878161 | nate.dawgg | 2026-05-20 | |
| 1503844716091936919 | nate.dawgg | 2026-05-12 | |
| 1483815592933720094 | nate.dawgg | 2026-03-18 | |
| 1483812396307976363 | nate.dawgg | 2026-03-18 | |
| 1481403515963183155 | nate.dawgg | 2026-03-11 |
Sending messages is a POST request with JSON body. Pass reply_id to thread a reply under an existing message. For file attachments, we switch to multipart/form-data—Discord expects a payload_json field with the message JSON, plus files[n] fields for each file.
Channel.send
async def send(
content:str='', files:NoneType=None, reply_id:NoneType=None
):Send a message with optional file attachments
msg = await ch.send('Hi, from Solveit!'); msgMessage(id=1524525340767289389, author='DBuddy', content='Hi, from Solveit!')reply_msg = await ch.send("I'm replying to myself 🤓", reply_id=msg.id); reply_msgMessage(id=1524525362850304280, author='DBuddy', content="I'm replying to myself 🤓")await msg.channelChannel(id=1327046393453613079, name='general', type=0)msg = await ch.send('Here is a file!', files=['../README.md']); msgMessage(id=1524525372698529911, author='DBuddy', content='Here is a file!')Channel.search
async def search(
content:Unset=UNSET, author_id:Unset=UNSET, mentions:Unset=UNSET, has:Unset=UNSET, before:Unset=UNSET,
after:Unset=UNSET, pinned:Unset=UNSET, sort_by:Unset=UNSET, sort_order:Unset=UNSET, offset:Unset=UNSET,
limit:Unset=UNSET, use_user:bool=False, nothread:bool=True
):Search guild messages. before/after accept ‘YYYY-MM-DD’ strings or snowflake IDs.
await ch.search(has='file', limit=1)| ID | Author | Content | Date |
|---|---|---|---|
| 1507463577617825936 | DBuddy | Here is a file! | 2026-05-22 |
Discord messages can have file attachments. The Attachment class wraps them as DiscordObjects—giving you attribute access to filename, size, content_type, url, etc. The fetch method downloads the file content using the existing httpx client.
Message.attachments
def attachments():Call self as a function.
Attachment
def Attachment(
data, client
):dict subclass that also provides access to keys as attrs, and has a pretty markdown repr
atts = msg.attachments; atts[Attachment(filename='README.md', size=28163, type=text/markdown; charset=utf-8)]
readme = (await atts[0].fetch()).decode()
print(readme[:16])# cordslite 🍺
DMs (Direct Messages) are just regular channels in Discord’s API — no special handling needed! To start a DM conversation, POST to /users/@me/channels with a recipient_id. Discord returns a standard channel object, so send() and messages() work exactly as they do for guild channels.
To detect DMs in the gateway, check for the absence of guild_id — DM messages don’t belong to any guild, so this field is missing or None. This makes it easy to route DM vs guild messages in your bot’s handler.
DiscordClient.create_dm
async def create_dm(
user_id
):Call self as a function.
# # Commented out so we don't spam Nate
# dm = await dc.create_dm('346450717025894400') # nathan's user ID
# await dm.send('Hello from DMs!')Members vs Users: A User is a global Discord account. A Member is a user within a specific guild—it has guild-specific data like nickname, roles, and join date. The nick or user['username'] pattern shows the server nickname if set, otherwise falls back to the global username.
Note: The members endpoint requires the GUILD_MEMBERS privileged intent enabled in your bot settings on the Developer Portal!
Guild.members
async def members(
limit:int=100
):Call self as a function.
Members
def Members(
*args, **kwargs
):Built-in mutable sequence.
If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.
Member
def Member(
data, client
):dict subclass that also provides access to keys as attrs, and has a pretty markdown repr
User
def User(
data, client
):dict subclass that also provides access to keys as attrs, and has a pretty markdown repr
mems = await gld.members(5); mems| ID | Name | Joined | Roles |
|---|---|---|---|
| 346450717025894400 | nathan | 2025-01-09 | 0 |
| 1327047896436178954 | SearchBuddy | 2025-01-09 | 1 |
| 1361823507679543306 | DBuddy | 2025-04-15 | 1 |
| 1448038710229733398 | Dizcord Util Bot | 2025-12-09 | 1 |
| 1467222191182712986 | Search Agent | 2026-01-31 | 1 |
Guild.tree
async def tree(
include_members:bool=True, member_limit:int=1000
):Call self as a function.
lbl
def lbl(
ch
):Call self as a function.
print(await gld.tree())natedog's server [1327046393453613076]
|-- Uncategorized
| |-- #test [1506656999293849753]
| |-- 🔊test-vch [1506657701688508456]
|-- Text Channels
| |-- #general: General conversations [1327046393453613079]
| |-- #private [1327954661960978512]
| |-- #forwarded [1475600987627458812]
| |-- #test-webhooks [1501265913222266961]
|-- Voice Channels
| |-- 🔊General [1327046393453613080]
|-- Members
| |-- nathan [346450717025894400]
| |-- SearchBuddy [1327047896436178954]
| |-- DBuddy [1361823507679543306]
| |-- Dizcord Util Bot [1448038710229733398]
| |-- Search Agent [1467222191182712986]
Channel.search_all
async def search_all(
limit:int=500, delay:float=1.0, max_age_days:NoneType=None, show:bool=False
):Paginated search returning up to limit messages
Guild.search_all
async def search_all(
limit:int=500, delay:float=1.0, max_age_days:NoneType=None, show:bool=False, **kwargs
):Paginated search returning up to limit messages
# r = await ch.search_all()
# len(r)Channel.bulk_delete
async def bulk_delete(
message_ids
):Bulk delete messages (must be <14 days old)
Channel.delete_message
async def delete_message(
message_id
):Delete a message by ID
Message.delete
async def delete():Delete this message
DiscordClient.thread
async def thread(
thread_id
):Fetch a thread (which is a Channel)
Message.create_thread
async def create_thread(
name
):Create a thread from this message
Channel.search_and_delete_all
async def search_and_delete_all(
content, delay:int=2, show:bool=False, **kwargs
):Bulk delete recent msgs, individually delete older ones
Webhooks let external services post to a channel without a bot token—executing one only needs its id and token. We wrap them in a Webhook class, with webhooks listings on both Channel and Guild, and edit/delete/send on the webhook itself. send uses ?wait=true so Discord returns the created message, and supports per-message username/avatar_url overrides.
DiscordClient.webhook
async def webhook(
webhook_id
):Fetch a webhook by ID
Guild.webhooks
async def webhooks():List all webhooks in this guild
Channel.webhooks
async def webhooks():List this channel’s webhooks
Webhooks
def Webhooks(
*args, **kwargs
):Built-in mutable sequence.
If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.
Webhook
def Webhook(
data, client
):dict subclass that also provides access to keys as attrs, and has a pretty markdown repr
Channel.create_webhook
async def create_webhook(
name
):Add webhook name to Channel
Webhook.send
async def send(
content:str='', username:Unset=UNSET, avatar_url:Unset=UNSET
):Execute this webhook, optionally overriding the display username/avatar_url
Webhook.delete
async def delete():Delete this webhook
Webhook.edit
async def edit(
name:Unset=UNSET, channel_id:Unset=UNSET
):Modify this webhook’s name or move it to channel_id