The real problem
Why this is harder than it looks
Discord’s OAuth2 flow is standard and the developer documentation is solid. A working proof-of-concept is an afternoon’s work. The complexity arrives when you try to run this for real users, in a product, with multiple tenants — and Discord has some sharp edges that don’t show up until you’re in production.
The first thing most developers underestimate is Discord’s scope model. Unlike providers with broad catch-all scopes, Discord’s scopes are fine-grained and additive — identify gives you basic profile data, guilds gives you server membership, guilds.members.read lets you read member details within a specific guild, and connections surfaces linked third-party accounts. The consent screen shows each scope explicitly, and Discord users are more privacy-conscious than average. Over-scoping kills authorization rates. Under-scoping means runtime failures when a tool call hits a permission boundary. Getting this right requires understanding exactly what each tool call needs before you send the user to the consent screen — and there’s no way to incrementally request more scopes without forcing a full re-authorization.
Then there’s the redirect URI matching behavior. Discord performs an exact string comparison against the URI you registered in the Developer Portal. A trailing slash, a protocol mismatch, or any character difference returns an invalid_redirect_uri error with no hint about which part mismatched. In a multi-environment setup — dev, staging, production — each environment needs its own registered URI, and keeping those in sync across deployments is operationally tedious.
Beyond those: per-user token isolation across a multi-tenant system, proactive refresh before token expiry, and detecting revocation when users disconnect your app from their Discord settings. None of these is hard in isolation. Together they’re a sprint worth of plumbing that has nothing to do with what your agent is supposed to do. Scalekit handles all of it — your code only has to name the tool and pass parameters.
Capabilities
What your agent can do with Discord
Once connected, your agent has 16 pre-built tools covering Discord’s user, guild, and identity surface:
- Read user profiles: fetch the authenticated user’s full profile, or look up any user by ID
- List and inspect guilds: retrieve all servers the user belongs to with member counts, permissions, and features
- Read guild membership: fetch the user’s member object within a specific guild — roles, nickname, join date, and avatar
- Check entitlements: verify which premium SKUs and subscriptions the user has access to for your application
- Retrieve linked accounts: read third-party connections like Twitch, YouTube, GitHub, and Steam from the user’s profile
- Verify OAuth authorization: inspect the current token’s granted scopes and expiration before making downstream calls
Setup context
What we're building
This guide connects a community assistant agent to Discord — helping users check their server memberships, verify account details, and surface premium entitlements without leaving your product.
🤖
Example agent
Community assistant reading guild membership and user profile data on behalf of each user
🔐
Auth model
B2B SaaS — each user connects their own Discord account. identifier = your user ID
Setup
1 Setup: One SDK, One credential
Install the Scalekit SDK. The only credential your application manages is the Scalekit API key — no Discord secrets, no user tokens, nothing belonging to your users.
pip install scalekit-sdk-python
npm install @scalekit-sdk/node
import scalekit.client
import os
from dotenv import load_dotenv
load_dotenv()
scalekit = scalekit.client.ScalekitClient(
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
env_url=os.getenv("SCALEKIT_ENV_URL"),
)
actions = scalekit.actions
import { ScalekitClient } from '@scalekit-sdk/node';
import 'dotenv/config';
const connectionName = 'discord';
const identifier = 'user_123';
const scalekit = new ScalekitClient(
process.env.SCALEKIT_ENV_URL,
process.env.SCALEKIT_CLIENT_ID,
process.env.SCALEKIT_CLIENT_SECRET
);
const actions = scalekit.actions;
Already have credentials?
Connected Accounts
2 Per-User Auth: Creating connected accounts
Each user gets their own Connected Account, giving them a dedicated auth context. The identifier is any unique string from your system — a UUID, email, whatever you use internally.
response = actions.get_or_create_connected_account(
connection_name="discord",
identifier="user_dc_456" # your internal user ID
)
connected_account = response.connected_account
print(f"Status: {connected_account.status}")
# Status: PENDING — user hasn't authorized yet
const response = await actions.getOrCreateConnectedAccount({
connectionName: "discord",
identifier: "user_dc_456" // your internal user ID
});
const connectedAccount = response.connectedAccount;
console.log(`Status: ${connectedAccount.status}`);
// Status: PENDING — user hasn't authorized yet
This call is idempotent — safe to call on every session start. Returns the existing account if one already exists.
Authorization Flow
3 The authorization flow
The user authorizes your agent once. Scalekit generates the OAuth URL with correct scopes, PKCE challenge, and redirect handling pre-configured. After approval, you never see the token.
if connected_account.status != "ACTIVE":
link = actions.get_authorization_link(
connection_name="discord",
identifier="user_dc_456"
)
# Redirect user → Discord's native OAuth consent screen
# Scalekit captures the token on callback
return redirect(link.link)
if (connectedAccount.status !== "ACTIVE") {
const { link } = await actions.getAuthorizationLink({
connectionName: "discord",
identifier: "user_dc_456"
});
// Redirect user → Discord's native OAuth consent screen
// Scalekit captures the token on callback
return redirect(link);
}
Token management is automatic
After the user approves, Scalekit stores encrypted tokens and the connected account moves to ACTIVE. Access
tokens refresh before expiry. If a user disconnects your app from their Discord settings, the account moves to REVOKED
— no silent failures. Check account.status before critical operations.
Bring Your Own Credentials — required
Discord requires you to register your own application in the Discord Developer Portal and supply your Client ID
and Client Secret. Create your app at discord.com/developers/applications, add the Scalekit redirect URI
under OAuth2 → Redirects, then paste your credentials into the Scalekit dashboard. The redirect URI must
match exactly — including protocol and no trailing slash. Token management is fully handled after that one-time setup.
Calling Discord
4 Calling Discord: What your agent writes
With the connected account active, your agent calls Discord actions using execute_tool. Name the tool, pass parameters. Scalekit handles token retrieval, request construction, and response parsing.
Fetch the authenticated user’s profile
Returns the connected user’s Discord profile — username, avatar, discriminator, locale, and email if the email scope was granted. No parameters needed.
result = actions.execute_tool(
identifier="user_dc_456",
tool_name="get_my_user",
tool_input={}
)
# Returns: { "id": "...", "username": "alice", "avatar": "...", "email": "alice@example.com", ... }
const result = await actions.executeTool({
identifier: "user_dc_456",
toolName: "get_my_user",
toolInput: {}
});
// Returns: { id: "...", username: "alice", avatar: "...", email: "alice@example.com", ... }
List the user’s guilds
Returns all servers the authenticated user belongs to. Use with_counts to include approximate member and presence counts — useful for filtering by server size or activity.
result = actions.execute_tool(
identifier="user_dc_456",
tool_name="list_my_guilds",
tool_input={
"with_counts": True,
"limit": 100
}
)
# Returns array of guild objects: id, name, icon, owner, permissions, features, approximate_member_count
const result = await actions.executeTool({
identifier: "user_dc_456",
toolName: "list_my_guilds",
toolInput: {
"with_counts": true,
"limit": 100
}
});
// Returns array of guild objects: id, name, icon, owner, permissions, features, approximate_member_count
Read the user’s guild membership
Fetch the authenticated user’s member object within a specific guild — their roles, nickname, join date, and guild-specific avatar. Requires the guilds.members.read scope.
result = actions.execute_tool(
identifier="user_dc_456",
tool_name="get_my_guild_member",
tool_input={
"guild_id": "1234567890123456789"
}
)
# Returns: { "roles": [...], "nick": "Alice", "joined_at": "2024-01-15T...", "avatar": "..." }
const result = await actions.executeTool({
identifier: "user_dc_456",
toolName: "get_my_guild_member",
toolInput: {
"guild_id": "1234567890123456789"
}
});
// Returns: { roles: [...], nick: "Alice", joined_at: "2024-01-15T...", avatar: "..." }
Verify OAuth authorization and granted scopes
Before making downstream calls, check what scopes the current token actually has. Returns the app info, granted scope list, token expiration, and user data for the connected account.
result = actions.execute_tool(
identifier="user_dc_456",
tool_name="get_my_oauth2_authorization",
tool_input={}
)
# Returns: { "application": { "id": "...", "name": "..." }, "scopes": ["identify", "guilds"], "expires": "2026-06-01T..." }
const result = await actions.executeTool({
identifier: "user_dc_456",
toolName: "get_my_oauth2_authorization",
toolInput: {}
});
// Returns: { application: { id: "...", name: "..." }, scopes: ["identify", "guilds"], expires: "2026-06-01T..." }
Framework wiring
5 Wiring into your agent framework
Scalekit integrates directly with LangChain. The agent decides what to call; Scalekit handles auth on every invocation. No token plumbing in your agent logic.
from langchain_anthropic import ChatAnthropic
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from scalekit.langchain import get_tools
dc_tools = get_tools(
connection_name="discord",
identifier="user_dc_456"
)
prompt = ChatPromptTemplate.from_messages([
("system", "You are a community assistant. Use the available tools to help users manage their Discord profile, guild memberships, and account connections."),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
agent = create_tool_calling_agent(ChatAnthropic(model="claude-sonnet-4-6"), dc_tools, prompt)
result = AgentExecutor(agent=agent, tools=dc_tools).invoke({
"input": "List all the Discord servers I'm in and show me my member details for the largest one"
})
import { ChatAnthropic } from "@langchain/anthropic";
import { AgentExecutor, createToolCallingAgent } from "langchain/agents";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { getTools } from "@scalekit-sdk/langchain";
const dcTools = getTools({
connectionName: "discord",
identifier: "user_dc_456"
});
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a community assistant. Use the available tools to help users manage their Discord profile, guild memberships, and account connections."],
new MessagesPlaceholder("chat_history", true),
["human", "{input}"],
new MessagesPlaceholder("agent_scratchpad"),
]);
const agent = await createToolCallingAgent({
llm: new ChatAnthropic({ model: "claude-sonnet-4-6" }),
tools: dcTools,
prompt
});
const result = await AgentExecutor.fromAgentAndTools({
agent,
tools: dcTools
}).invoke({
input: "List all the Discord servers I'm in and show me my member details for the largest one"
});
Other frameworks supported
Tool reference
All 16 Discord tools
Grouped by capability. Your agent calls tools by name — no API wrappers to write.
Fetch the authenticated user’s full Discord profile — username, avatar, discriminator, locale, and email (if email scope granted). No parameters required
Retrieve a Discord user by ID, or pass @me to get the authenticated user. Returns username, avatar, discriminator, and premium status
get_openid_connect_userinfo
Return standardized OIDC claims (sub, email, nickname, picture, locale) for the authenticated user. Requires the openid scope; additional fields require identify and email
List all guilds the authenticated user belongs to with id, name, icon, owner flag, permissions, and features. Supports pagination and optional member/presence counts. Requires guilds scope
Retrieve the authenticated user’s member object within a specific guild — roles, nickname, join date, and guild avatar. Requires guilds.members.read scope
Retrieve a guild’s public widget data as JSON — online member count and invite URL. Widget must be enabled in the guild’s server settings
Retrieve a PNG image representation of the guild widget for embedding on external websites. Widget must be enabled
Retrieve details about a guild template by its code — useful for auditing template configurations or creating new servers from a template
Resolve a Discord invite code and return the associated guild, channel, scheduled event, and inviter details. Prefer this over the deprecated get_invite tool
Deprecated — use resolve_invite for new integrations. Retrieves guild and channel details for an invite code
get_current_user_application_entitlements
Check which premium SKUs or subscriptions the authenticated user has for your application. Supports filtering by SKU, guild, and pagination. Requires applications.entitlements scope
retrieve_user_connections
Read the user’s linked third-party accounts — Twitch, YouTube, GitHub, Steam, and others. Returns service name, account ID, name, and visibility. Requires connections scope
get_my_oauth2_authorization
Inspect the current OAuth2 token’s granted scopes, expiration date, application info, and user data. Use to verify access before making downstream calls
Retrieve Discord’s OAuth2 public keys (JWKS) for verifying token signatures and performing cryptographic validation
Retrieve the WebSocket (wss://) URL for establishing a Gateway connection to Discord for real-time events. No authentication required
Retrieve all available Discord Nitro sticker packs — name, description, stickers, cover sticker, and banner. No authentication required
Connector notes
Discord-specific behavior
Redirect URI must match exactly
Discord performs a strict string comparison against the redirect URI registered in your Developer Portal app. Any deviation — trailing slash, http vs https, or environment mismatch — returns an invalid_redirect_uri error with no diagnostic detail. Register a separate redirect URI for each environment (dev, staging, production) and ensure the Scalekit-generated URI is copied verbatim.
Scope selection is final at authorization time
Discord has no incremental scope request mechanism. The scopes your agent requests at authorization time are all it will ever have without forcing a full re-authorization. Map out every tool your agent will call and request all required scopes upfront. Adding a tool that needs guilds.members.read later means re-authorizing all affected users.
Guild widget tools require widget to be enabled
get_guild_widget and get_guild_widget_png both require the guild owner to have enabled the widget feature under Server Settings → Widget. If the widget is disabled, these calls return a 403. Your agent cannot enable widgets — this is a server configuration that must be done by a guild administrator.
Infrastructure decision
Why not build this yourself
Discord’s OAuth flow is documented. Token storage isn’t technically hard. But here’s what you’re actually signing up for:
PROBLEM 01
Discord’s fine-grained scope model means a single missing scope causes a runtime failure — and there’s no way to add scopes without re-authorizing the user from scratch
PROBLEM 02
Redirect URI exact-match enforcement across multiple deployment environments requires careful per-environment registration and zero tolerance for configuration drift
PROBLEM 03
Per-user token isolation across a multi-tenant system — one user’s Discord credentials must never be accessible to another, even within the same application
PROBLEM 04
Revocation detection and graceful handling when users disconnect your app from Discord’s Authorized Apps settings — without silent failures continuing to attempt API calls on their behalf
That’s one connector. Your agent product will eventually need Salesforce, GitHub, Slack, Notion, and whatever else your customers ask for. Each has its own OAuth quirks and failure modes.
Scalekit maintains every connector. You maintain none of them.