A modern, Swift-native Discord API library for building powerful bots
Build Discord bots and integrations with the elegance of Swift โ fully async, strongly typed, and production-ready.
SwiftDisc brings the power of modern Swift to Discord bot development. Whether you're building a simple utility bot or a complex multi-server application, SwiftDisc provides the tools you need with an API that feels natural to Swift developers.
- ๐ฏ Swift-First Design โ Built from the ground up for Swift, leveraging async/await, actors, and structured concurrency
- ๐ Type Safety โ Comprehensive type-safe models that catch errors at compile time
- ๐ Truly Cross-Platform โ Deploy on iOS, macOS, tvOS, watchOS, and Windows with the same codebase
- โก Production Ready โ Automatic rate limiting, connection resilience, and sharding support out of the box
- ๐จ Developer Friendly โ Intuitive APIs inspired by discord.py, adapted for Swift's strengths
- First-time bot developers looking for a modern, well-documented library
- Swift developers wanting to leverage their existing skills
- Cross-platform projects requiring deployment flexibility
- Production applications demanding reliability and performance
Get your first bot running in minutes:
import SwiftDisc
@main
struct MyFirstBot {
static func main() async {
let token = ProcessInfo.processInfo.environment["DISCORD_BOT_TOKEN"] ?? ""
let client = DiscordClient(token: token)
do {
try await client.loginAndConnect(intents: [.guilds, .guildMessages, .messageContent])
for await event in client.events {
switch event {
case .ready(let info):
print("โ
Bot is online as \(info.user.username)!")
case .messageCreate(let message) where message.content == "!hello":
try await client.sendMessage(
channelId: message.channel_id,
content: "๐ Hello, \(message.author.username)!"
)
default:
break
}
}
} catch {
print("โ Error: \(error)")
}
}
}// Create (image should be data URI string, e.g. "data:image/png;base64,<...>")
let created = try await client.createAppEmoji(
applicationId: appId,
name: "party",
imageBase64: "data:image/png;base64,....",
options: ["roles": .array([])] // optional extras
)
// Update
let updated = try await client.updateAppEmoji(
applicationId: appId,
emojiId: "1234567890",
updates: ["name": .string("party_blob")]
)
// Delete
try await client.deleteAppEmoji(
applicationId: appId,
emojiId: "1234567890"
)// Create resource under your application scope
let res = try await client.createUserAppResource(
applicationId: appId,
relativePath: "directory/listings",
payload: ["title": .string("My Awesome App"), "enabled": .bool(true)]
)
// Update resource
let upd = try await client.updateUserAppResource(
applicationId: appId,
relativePath: "directory/listings/abc",
payload: ["enabled": .bool(false)]
)
// Delete resource
try await client.deleteUserAppResource(
applicationId: appId,
relativePath: "directory/listings/abc"
)That's it! You now have a working Discord bot. ๐
Add SwiftDisc to your Package.swift:
dependencies: [
.package(url: "https://github.com/M1tsumi/SwiftDisc.git", from: "0.8.0")
]Then include it in your target:
targets: [
.target(name: "YourBot", dependencies: ["SwiftDisc"])
]| Platform | Minimum Version | Status |
|---|---|---|
| iOS | 14.0+ | โ Fully Supported |
| macOS | 11.0+ | โ Fully Supported |
| tvOS | 14.0+ | โ Fully Supported |
| watchOS | 7.0+ | โ Fully Supported |
| Windows | Swift 5.9+ | โ Fully Supported |
We've created comprehensive examples to help you get started:
Perfect for understanding the basics of event handling and message responses.
// Responds to "!ping" with the bot's latency
case .messageCreate(let message) where message.content == "!ping":
try await client.sendMessage(
channelId: message.channel_id,
content: "๐ Pong! Latency: 42ms"
)Learn how to build a command system with prefix routing and help commands.
let router = CommandRouter(prefix: "!")
router.register("help") { context in
try await context.reply("Available commands: !help, !userinfo, !serverinfo")
}Discover modern Discord interactions with slash commands and autocomplete.
let slash = SlashCommandRouter()
slash.register("greet") { interaction in
try await interaction.reply("Hello from SwiftDisc! ๐")
}Dynamic suggestions for command options using AutocompleteRouter.
Multipart uploads with content-type detection and size guardrails.
Listen to thread lifecycle and guild scheduled events.
Threads & Scheduled Events โ
Our Wiki provides in-depth guides for:
- ๐ฏ Core Concepts โ Understanding intents, events, and the Discord API
- ๐ง Configuration โ Setting up your bot for development and production
- ๐จ Message Features โ Embeds, components, attachments, and more
- โ๏ธ Sharding โ Scaling your bot across multiple servers
- ๐ Deployment โ Best practices for production environments
- ๐ฌ Join our Discord Server โ Get real-time support from the community
- ๐ Browse the Wiki โ Detailed documentation and tutorials
- ๐ Report Issues โ Found a bug? Let us know!
- ๐ก GitHub Discussions โ Share your projects and ideas
- โ Full WebSocket gateway implementation
- โ Automatic heartbeat and session management
- โ Resume support for connection recovery
- โ Structured event system with AsyncSequence
- โ Presence updates and status management
- โ Threads and Scheduled Events (create/update/delete, members add/remove)
- โ
100% event visibility via
DiscordEvent.raw(String, Data)fallback for unmodeled dispatches
- โ Channels โ Create, modify, delete channels and threads
- โ Messages โ Send, edit, delete with embeds and components
- โ Guilds โ Full server management capabilities
- โ Members & Roles โ User and permission management
- โ Slash Commands โ Create and manage application commands
- โ Webhooks โ Create and execute webhooks
- โ Auto Moderation โ Configure moderation rules
- โ Scheduled Events โ Create and manage server events
- โ Forum Channels โ Create threads and posts
- โ
Raw coverage helpers:
rawGET/POST/PATCH/PUT/DELETEfor any unsupported endpoint
// Timeout a member until a specific ISO8601 timestamp
let in10Min = Date().addingTimeInterval(10 * 60)
let updated: GuildMember = try await client.setMemberTimeout(guildId: guildId, userId: userId, until: in10Min)
// Clear timeout
let cleared: GuildMember = try await client.clearMemberTimeout(guildId: guildId, userId: userId)- โ Per-route rate limit handling with automatic retries
- โ Global rate limit detection and backoff
- โ Sharding support with automatic shard count
- โ Health monitoring and shard management
- โ Typed command routing (prefix and slash) + Autocomplete router
- โ Rich embed builder and message components (buttons, select menus)
- โ
File uploads: multipart with content-type detection and configurable guardrails (
maxUploadBytes) - โ Advanced caching: configurable TTLs and per-channel message LRU
- โ
Extensions/Cogs: simple plugin protocol and
Coghelper;DiscordClient.loadExtension(_:) - โ Permissions utilities: effective permission calculator with channel overwrites
Use postMessage(channelId:payload:) with JSONValue to send Components V2 while keeping SwiftDisc zero-dependency and future-proof. Paste the payload from the Discord docs.
// Example skeleton โ replace with the latest Components V2 JSON from docs
let payload: [String: JSONValue] = [
"content": .string("Hello with Components V2"),
"flags": .int(1 << 15), // if docs require enabling V2 via flag
"components": .array([
.object(["type": .int(1), "children": .array([ /* ... */ ])])
])
]
let msg = try await client.postMessage(channelId: channelId, payload: payload)Typed envelope helper:
let v2 = V2MessagePayload(
content: "Hello with V2",
flags: 1 << 15, // if required by docs
components: [
.object(["type": .int(1), "children": .array([ /* ... */ ])])
]
)
let msg = try await client.sendComponentsV2Message(channelId: channelId, payload: v2)Use createPollMessage(channelId:content:poll:flags:components:) with a poll object conforming to the Poll Resource schema.
// Example skeleton โ replace with the Poll Resource JSON from docs
let poll: [String: JSONValue] = [
"question": .object(["text": .string("Your favorite language?")]),
"answers": .array([
.object(["answer_id": .int(1), "poll_media": .object(["text": .string("Swift")])]),
.object(["answer_id": .int(2), "poll_media": .object(["text": .string("Kotlin")])])
]),
"allow_multiple": .bool(false),
"duration": .int(600) // seconds
]
let msg = try await client.createPollMessage(channelId: channelId, content: "Vote now!", poll: poll)Typed envelope helper:
let pollPayload = PollPayload(
question: "Your favorite language?",
answers: ["Swift", "Kotlin"],
durationSeconds: 600,
allowMultiple: false
)
let msg = try await client.createPollMessage(channelId: channelId, payload: pollPayload, content: "Vote now!")Update command name/description localizations:
let updated = try await client.setCommandLocalizations(
applicationId: appId,
commandId: cmdId,
nameLocalizations: [
"en-US": "ping",
"ja": "ใใณ"
],
descriptionLocalizations: [
"en-US": "Check latency",
"ja": "ใฌใคใใณใทใผใ็ขบ่ช"
]
)Post a message in another channel that references an existing message (portable forward):
let forwarded = try await client.forwardMessageByReference(
targetChannelId: targetChannelId,
sourceChannelId: sourceChannelId,
messageId: messageId
)Use these helpers to call application-scoped endpoints with JSONValue payloads. This keeps SwiftDisc current as Discord evolves.
// POST /applications/{appId}/{relativePath}
let createRes = try await client.postApplicationResource(
applicationId: appId,
relativePath: "some/feature",
payload: ["key": .string("value")]
)
// PATCH /applications/{appId}/{relativePath}
let patchRes = try await client.patchApplicationResource(
applicationId: appId,
relativePath: "some/feature/id",
payload: ["enabled": .bool(true)]
)
// DELETE /applications/{appId}/{relativePath}
try await client.deleteApplicationResource(
applicationId: appId,
relativePath: "some/feature/id"
)- โ
Mentions:
Mentions.user(_:),Mentions.channel(_:),Mentions.role(_:),Mentions.slashCommand(name:id:) - โ
Emoji helpers:
EmojiUtils.custom(name:id:animated:) - โ
Timestamps:
DiscordTimestamp.format(date:style:),format(unixSeconds:style:) - โ
Escaping:
MessageFormat.escapeSpecialCharacters(_:)
SwiftDisc is built for real-world applications:
- Automatic Reconnection โ Handles network issues gracefully
- Rate Limit Compliance โ Respects Discord's limits automatically
- Session Resume โ Maintains connection state across reconnects
- Sharding Support โ Built-in multi-shard management
- Health Monitoring โ Track shard status and latency
- Graceful Shutdown โ Clean disconnection handling
- Comprehensive Logging โ Detailed logs for debugging
- Type-Safe APIs โ Catch errors at compile time
- Clear Error Messages โ Actionable error descriptions
// Automatic sharding for large bots
let manager = await ShardingGatewayManager(
token: token,
configuration: .init(
shardCount: .automatic,
connectionDelay: .staggered(interval: 1.5)
),
intents: [.guilds, .guildMessages]
)
try await manager.connect()
// Monitor health across all shards
let health = await manager.healthCheck()
print("Ready shards: \(health.readyShards)/\(health.totalShards)")We're building SwiftDisc together with the community! Whether you're a beginner looking to create your first bot or an experienced developer with feature requests, we'd love to have you.
Get help, share your projects, and connect with other SwiftDisc developers!
What you'll find:
- ๐ Support channels for troubleshooting
- ๐ก Showcase your bots and get feedback
- ๐ข Stay updated with the latest releases
- ๐ค Collaborate with other developers
We're actively developing SwiftDisc with these priorities:
- Autocomplete
- File uploads polish (MIME + guardrails)
- Gateway parity: Threads & Scheduled Events + raw fallback
- Advanced caching & permissions utilities
- Extensions/Cogs
- Voice support (optional module)
- Voice support (sendโonly MVP)
- Performance optimizations
Want to influence the roadmap? Join the Discord server and share your ideas!
Initial voice support is available behind a configuration flag. This is a send-only implementation that connects to Discord Voice, performs UDP IP discovery, negotiates xsalsa20_poly1305, and can transmit Opus frames (no external dependencies required).
Enable and use:
let config = DiscordConfiguration(enableVoiceExperimental: true)
let client = DiscordClient(token: token, configuration: config)
try await client.joinVoice(guildId: guildId, channelId: channelId)
// Option A: Push individual Opus packets (20ms @ 48kHz)
try await client.playVoiceOpus(guildId: guildId, data: opusPacket)
// Option B: Stream from a VoiceAudioSource
struct MySource: VoiceAudioSource {
func nextFrame() async throws -> OpusFrame? { /* return OpusFrame(data:packet,durationMs:20) */ }
}
try await client.play(source: MySource(), guildId: guildId)
try await client.leaveVoice(guildId: guildId)Whatโs included:
- Voice Gateway handshake (Hello โ Identify โ Ready โ Heartbeat)
- UDP IP discovery (Network.framework on Apple platforms)
- Protocol selection (xsalsa20_poly1305)
- Session Description key handling
- RTP packetization + pure-Swift Secretbox encryption (no SwiftPM deps)
- Speaking flag management
Requirements:
- Input must be Opus-encoded packets at 48kHz (20ms recommended). SwiftDisc does not bundle an Opus encoder or media demuxer to maintain zero dependencies.
macOS streaming with ffmpeg (no Swift dependencies):
Use an external ffmpeg (system-installed) to demux/encode your source (YouTube, SoundCloud, etc.) to raw Opus packets and pipe them into SwiftDisc. Implement a small wrapper to length-prefix packets (u32 LE) or use a helper that outputs framed Opus.
Example framing expected by PipeOpusSource:
- Frame format:
[u32 little-endian length][<length> bytes]repeated.
Use PipeOpusSource:
import Foundation
let source = PipeOpusSource(handle: FileHandle.standardInput)
try await client.play(source: source, guildId: guildId)Then run your bot and feed framed Opus via stdin. For example, you can create a small CLI that transforms ffmpeg output to the framed format and pipe to the bot. This keeps SwiftDisc zero-dependency.
iOS guidance:
- iOS cannot spawn
ffmpeg. Provide Opus packets from your app/backend over your own transport (e.g., HTTPS/WebSocket) and feed them to aVoiceAudioSource.
Security and correctness:
- SwiftDisc vendors a pure-Swift Secretbox (XSalsa20-Poly1305) implementation with Poly1305 MAC and Salsa20 core. Nonce derivation follows the Discord RTP header convention. We recommend validating with your own test vectors as part of your CI.
Contributions make SwiftDisc better for everyone! Here's how you can help:
- ๐ Report Bugs โ Found an issue? Open an issue
- ๐ก Suggest Features โ Have an idea? Start a discussion
- ๐ Improve Docs โ Documentation improvements are always welcome
- ๐ง Submit PRs โ Code contributions are appreciated!
Check our Contributing Guidelines for more details.
SwiftDisc is released under the MIT License. See LICENSE for details.
In short: You're free to use SwiftDisc for personal and commercial projects, with attribution.
Ready to build your Discord bot?
๐ Read the Docs โข ๐ฌ Join Discord โข ๐ View Examples
โญ Star us on GitHub if you find SwiftDisc helpful!