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

source

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.

dc = DiscordClient()

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.


source

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.


source

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__.


source

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.


source

DiscordClient.guild

async def guild(
    guild_id
):

Call self as a function.

gid = '1493461895615873044'
gld = await dc.guild(gid); gld
Guild(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.


source

Guild.channels

async def channels(
    limit:NoneType=None
):

Call self as a function.


source

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.


source

Channel

def Channel(
    data, client
):

dict subclass that also provides access to keys as attrs, and has a pretty markdown repr


source

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

source

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.


source

DiscordClient.guilds

async def guilds(
    limit:Unset=UNSET
):

List the guilds the bot is a member of


source

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)
ch

Messages 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.


source

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.


source

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.


source

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

source

Message.around

async def around(
    limit:int=11
):

Fetch messages around this message in the same channel/thread.


source

Message.after

async def after(
    limit:int=5
):

Fetch messages after this message in the same channel/thread.


source

Message.before

async def before(
    limit:int=5
):

Fetch messages before this message in the same channel/thread.


source

Message.url

def url():

Call self as a function.


source

Channel.url

def url():

Call self as a function.


source

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.


source

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.


source

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.


source

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.


source

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!'); msg
Message(id=1524525340767289389, author='DBuddy', content='Hi, from Solveit!')
reply_msg = await ch.send("I'm replying to myself 🤓", reply_id=msg.id); reply_msg
Message(id=1524525362850304280, author='DBuddy', content="I'm replying to myself 🤓")
await msg.channel
Channel(id=1327046393453613079, name='general', type=0)
msg = await ch.send('Here is a file!', files=['../README.md']); msg
Message(id=1524525372698529911, author='DBuddy', content='Here is a file!')

source

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.


source

Message.attachments

def attachments():

Call self as a function.


source

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.


source

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!


source

Guild.members

async def members(
    limit:int=100
):

Call self as a function.


source

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.


source

Member

def Member(
    data, client
):

dict subclass that also provides access to keys as attrs, and has a pretty markdown repr


source

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

source

Guild.tree

async def tree(
    include_members:bool=True, member_limit:int=1000
):

Call self as a function.


source

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]

source

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


source

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)

source

Channel.bulk_delete

async def bulk_delete(
    message_ids
):

Bulk delete messages (must be <14 days old)


source

Channel.delete_message

async def delete_message(
    message_id
):

Delete a message by ID


source

Message.delete

async def delete():

Delete this message


source

DiscordClient.thread

async def thread(
    thread_id
):

Fetch a thread (which is a Channel)


source

Message.create_thread

async def create_thread(
    name
):

Create a thread from this message


source

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.


source

DiscordClient.webhook

async def webhook(
    webhook_id
):

Fetch a webhook by ID


source

Guild.webhooks

async def webhooks():

List all webhooks in this guild


source

Channel.webhooks

async def webhooks():

List this channel’s webhooks


source

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.


source

Webhook

def Webhook(
    data, client
):

dict subclass that also provides access to keys as attrs, and has a pretty markdown repr


source

Channel.create_webhook

async def create_webhook(
    name
):

Add webhook name to Channel


source

Webhook.send

async def send(
    content:str='', username:Unset=UNSET, avatar_url:Unset=UNSET
):

Execute this webhook, optionally overriding the display username/avatar_url


source

Webhook.delete

async def delete():

Delete this webhook


source

Webhook.edit

async def edit(
    name:Unset=UNSET, channel_id:Unset=UNSET
):

Modify this webhook’s name or move it to channel_id