---
name: discord-oauth-app
description: Creates a Discord application in the developer portal and obtains OAuth2 client ID and client secret plus a bot token — with the right redirect URIs, scope strings, the `bot` scope and its permissions bitfield, privileged gateway intents, app verification and a safe credential handoff. Use when asked to get Discord OAuth credentials, create or configure a Discord app, pick Discord scopes or bot permissions, get a Discord bot token, enable the MESSAGE CONTENT or SERVER MEMBERS intent, get an app verified or listed in the App Directory, rotate a Discord client secret or bot token, or fix a Discord install error like `invalid_scope`, gateway close code `4014`, or "Only the application owner can add this bot". For any other vendor's developer portal, use that vendor's skill instead.
---

# Discord OAuth2 App Registration

Get a working Discord application — client ID, client secret, bot token, redirect URIs, scopes, a permissions
bitfield and whatever intents you actually need — for a platform that connects *other organizations'* Discord servers
on behalf of many customers.

Discord's portal is free and instant, and that is the trap: the expensive decisions are invisible in it. Three of
them. First, **a Discord app hands you two unrelated credentials** — a long-lived **bot token** that acts as the app
inside every server the bot was installed into, and a short-lived **OAuth2 access token** that acts as one signed-in
user (§5). They reach different data, and most connectors need both. Second, the **`bot` scope plus a `permissions`
bitfield** is a different install than a plain user authorization, and that bitfield is a *permissions* bitfield, not
an *intents* bitfield — two different numbering schemes that look identical and are constantly confused (§7).
Third, **privileged gateway intents** are the wall: `MESSAGE_CONTENT` and `GUILD_MEMBERS` are portal toggles that
stop being self-serve once the app can reach **10,000 users**, and a wrong intent bitfield does not degrade, it
closes the gateway connection with code `4014` (§8).

## Inputs to collect before you start

Ask in one batch. Never invent.

| Input | Notes |
| --- | --- |
| **App name**, icon, description | Shown on the install/consent screen and in every server's member list |
| **Discord account, and ideally a Team** | Teams need 2FA; ownership transfer to a team is one-way (§2) |
| **A test server you own** | You need `MANAGE_GUILD` in it to install the bot (§2) |
| **Redirect URIs** | Every callback host your platform serves (§4) |
| **Does the connector need a bot token, a user token, or both?** | The decisive question (§5) |
| **Exact scope strings** | Must match what the authorize URL sends (§6) |
| **Bot permissions** | The `permissions` bitfield — pick the minimum, not `ADMINISTRATOR` (§7) |
| **Which gateway intents, if any** | Only if you run a gateway/WebSocket bot; privileged ones gate growth (§8) |
| **Privacy policy URL, terms of service URL** | Needed for App Directory discovery (§11) |

## Quick Start

1. Confirm a **new app** is needed — existing installs and tokens are bound to the current app (§1).
2. Sign in, enable 2FA, create or join a **Team**, and pick a **test server** you control (§2).
3. Create the app at `https://discord.com/developers/applications`; add a **bot user** (§3).
4. Add **every** redirect URI under **OAuth2 → Redirects** (§4).
5. Decide **bot token, user token, or both** — this drives everything below (§5).
6. Set the **scope strings**; remember there is no `openid` scope on Discord (§6).
7. Compute the **`permissions` bitfield** for the `bot` scope — permissions, not intents (§7).
8. Toggle only the **privileged gateway intents** you truly need, and read the 10,000-user rule (§8).
9. Copy **Application ID**, **client secret** (OAuth2 page) and **bot token** (Bot page — shown once) (§9).
10. Install into the test server through the real flow and make one call with **each** credential (§12).
11. Hand the credentials over — never commit them (§13).

## Platform state (verified 2026-09-20 — re-verify before trusting)

- **The docs moved.** Discord's **10 February 2026** "Next Generation Docs" change migrated the developer
  documentation to Mintlify. `discord.com/developers/docs/...` now **301s to `docs.discord.com/developers/...`**.
  The portal itself is still at `discord.com/developers/applications`. Older runbooks linking the old paths are not
  wrong, just redirected.
- **Privileged intent access changed on 10 June 2026, and the old number everyone quotes is dead.** The threshold
  used to be **100 servers**. It is now **10,000 users** — the number of unique users your app can reach across every
  server it is installed in. Below that, you toggle privileged intents yourself in the portal. At or above it you get
  a system DM and/or email, have **90 days** to apply for review, and must **reapply annually** once granted. Apps
  are **no longer blocked from joining new servers** while a submission is pending — that block was part of the old
  regime. Privileged intent review and **App Verification are now separate processes** (§8).
- **App Verification is the *other* threshold and it is still 100 servers.** Discord's developer help center states
  that verification is required for an app to scale past 100 servers, that the checklist lives under an **App
  Verification** tab in the portal, and that the **development team's owner must verify their identity through
  Stripe**. Do not merge these two gates in your head: 100 servers = verification to keep growing; 10,000 users =
  review to keep privileged intents (§11).
- **There is no `openid` scope.** Discord's scope table has `identify` and `email`; it does not document an `openid`
  scope and Discord is not documented as an OpenID Connect provider. Anything asking for `openid` on Discord is
  carrying over a habit from another vendor.
- **PKCE is not documented anywhere in Discord's OAuth2 docs.** This is a confidential-client flow with a client
  secret. Do not add `code_challenge` expecting it to be honoured.
- **Access tokens are short; refresh tokens are the mechanism.** The documented example response carries
  `expires_in: 604800` — seven days. Token exchange and refresh are `application/x-www-form-urlencoded` POSTs;
  **JSON bodies are rejected**, and the docs pass client credentials via **HTTP Basic** (client ID as username,
  client secret as password).
- **`shard` became required on `GET /users/@me/guilds` for large-bot-sharding apps on 15 September 2026.** The
  endpoint also caps at **200 guilds** (`limit` 1–200). Non-sharding apps are unaffected, but a connector that
  enumerates a bot's servers through this route needs to know both facts.
- **`PIN_MESSAGES` split out of `MANAGE_MESSAGES`** — announced 24 November 2025, effective February 2026. If you
  copied a permissions bitfield from an older runbook, re-derive it.

If the portal or the docs do not look like this, stop and report what you actually see rather than clicking on.

## 1. Decide: reuse the existing app, or register a new one

A new app means a **new Application ID, a new client secret and a new bot user**. Every existing install is bound to
the old app: customers would have to remove the old bot from their server and re-install the new one, and every
stored access token and refresh token becomes worthless. That is a customer-visible migration, not a config change.

Reuse the existing app for: adding a redirect URI, adding a scope, changing the permissions bitfield, toggling an
intent, rotating a secret, or diagnosing an install failure.

Register a **new** app only when the user explicitly wants one: a replacement for a compromised app, a separate app
for a different product surface, or a deliberate split of environments. Say which path you are taking before you
touch anything.

Note that even on the same app, **changing scopes or the permissions bitfield does not upgrade existing installs.**
A token carries the scopes granted when it was issued, and a bot's role in a server carries the permissions granted
when it was added. Broadening either is a re-authorization / re-install event per customer.

## 2. Account, 2FA, Team, and a test server

There is no separate developer account and no approval gate to create an app. You need:

- **A Discord account** with **2FA enabled**. 2FA is not optional in practice: Discord requires it to create or join
  a Team, and several powerful permissions — including `ADMINISTRATOR`, `MANAGE_GUILD`, `MANAGE_ROLES`,
  `MANAGE_WEBHOOKS`, `MANAGE_CHANNELS`, `MANAGE_MESSAGES`, `KICK_MEMBERS`, `BAN_MEMBERS`, `MODERATE_MEMBERS` —
  "require the owner account to use two-factor authentication when used on a guild that has server-wide 2FA enabled."
  A connector that requests those permissions will hit this at customers who enforce server-wide 2FA.
- **A Team, not a person.** Teams are "groups of developers (or other Discord users) who want to collaborate and
  share access to an app's configuration, management, and payout settings," with roles **Owner** (one per team, can
  delete apps and the team), **Admin**, **Developer** and **Read-only**. For a platform integration the app should
  be owned by a team the company controls, not by whoever happened to click *New Application*. **Transfer is
  one-way**: "Once an app has been transferred to a team, it *cannot* be transferred back." Do it deliberately, and
  record who the team Owner is — that person is also who will have to complete identity verification at 100 servers
  (§11).
- **A test server you own,** so you can install the bot. Installing to a server requires `MANAGE_GUILD` in it.
  Create a fresh free server rather than installing a half-configured bot into anything real.

Hand control back to the user for anything only they can do: sign-in, email/phone verification, enabling 2FA,
accepting terms, Stripe identity verification, or approving an install in a server where they are not an admin. Do
not retry a blocked step in a loop.

**If this session has no browser automation** (the usual case for a CLI or cloud run), do not pretend to click. Hand
the user an exact, ordered click path with the literal values to paste — the redirect URIs from §4, the scope strings
from §6, the computed bitfield from §7, the intent toggles from §8 — then continue once they report back with the
Application ID.

## 3. Create the app

At **`https://discord.com/developers/applications`** → **New Application**. Then, on the app's pages:

| Page | Holds |
| --- | --- |
| **General Information** | **Application ID**, **Public Key**, **Interaction Endpoint URL**, app name/icon/description, transfer-to-team |
| **Bot** | The **bot token** (via **Reset Token**), **Public Bot**, **Requires OAuth2 Code Grant**, **Privileged Gateway Intents** |
| **OAuth2** | **Client secret** (**Reset Secret**), **Redirects**, the URL generator |
| **Installation** | Install Link and **Default Install Settings** — the scopes and bot permissions a Discord-provided link uses |
| **App Verification** | The checklist that gates growth past 100 servers (§11) |
| **Discovery** | **Discovery Status** checklist and **Enable Discovery** for the App Directory (§11) |

Two settings on the **Bot** page decide whether anyone but you can install it:

- **Public Bot** — "If unchecked, only you can add the bot to guilds. If marked as public, anyone with your bot's
  URL can add it to guilds in which they have proper permissions." A multi-tenant connector **must** have this on.
  Leaving it off is the cause of *"Only the application owner can add this bot"* at every customer.
- **Requires OAuth2 Code Grant** — when enabled it "requires anyone adding your bot to a server to go through a full
  OAuth2 authorization code grant flow." Turn this on only if your platform genuinely completes the code exchange on
  the callback. If your flow is a bare install link with no exchange, this setting strands every install.

The **bot token is shown once.** Discord's own guidance: "You won't be able to view your token again unless you
regenerate it, so make sure to keep it somewhere safe." Capture it the moment it appears (§9).

Discord's reference documentation describes the API, not the portal UI, so treat the page names above as a map to
confirm on screen rather than a contract — if a page is missing or renamed, say so instead of guessing.

## 4. Redirect URIs

Add **every** callback host your platform serves under **OAuth2 → Redirects**. For Unified.to these are one per data
center; confirm the current list with the platform owner rather than assuming:

```
https://api.unified.to/oauth/code          # us (default)
https://api-eu.unified.to/oauth/code       # eu
https://api-au.unified.to/oauth/code       # au
https://api-dev.unified.to/oauth/code      # dev
```

Discord's rule, stated plainly in the OAuth2 docs, is that the `redirect_uri` you send is "whatever URL you
registered when creating your application, **url-encoded**" — and the same value must be sent again in the token
exchange, where it is one of the documented POST fields. Register all four before you test, because the first thing
a missing entry produces is an error page the user sees instead of a consent screen, with no callback fired and
nothing in your logs.

A bot-only install link (`scope=bot`, no `response_type`) does not use a redirect URI at all. The moment you add any
other scope, `response_type=code` becomes mandatory and the redirect URI is back in play (§7). This is why an install
link that "worked" can break the day someone adds `identify` to it.

## 5. The decisive choice: bot token or OAuth2 access token

One Discord app can issue **two credentials with different reach**. Choosing wrong is the expensive mistake here,
because the wrong one does not fail at install — it fails later with a 401 or an empty list.

| | **Bot token** | **OAuth2 access token** |
| --- | --- | --- |
| Where it comes from | The **Bot** page in the portal, once, by hand | The authorization code exchange, per customer |
| Header | `Authorization: Bot <token>` | `Authorization: Bearer <token>` |
| Acts as | The application, inside every server the bot was installed into | The one user who authorized, across what *they* can see |
| Lifetime | Long-lived until reset | `expires_in: 604800` (7 days), refreshed with `grant_type=refresh_token` |
| Multi-tenant shape | **One token for all customers** | One token per customer connection |
| Reaches server content | Yes, subject to its role's permissions in each server | Only through user-scoped endpoints and the scopes granted |
| Returned by the OAuth exchange? | **No.** The token response never contains the bot token | Yes |

That last row is the single most common misunderstanding. Completing a `bot`-scope authorization **installs** the
bot into the chosen server; it does not hand you the bot's token. The bot token is a separate, manually copied
secret, and it is the same value for every customer — which also means it is the single credential whose compromise
affects all of them at once.

Most real connectors are **hybrid**: they use the bot token to read and write server content, and the user's OAuth
token only to establish *which* servers that particular customer is entitled to see (otherwise every customer of the
platform would see every server the bot is in). If you are building that shape, you need **both** credentials
configured, and your scope list must include `bot` *and* the user scopes.

> **As of 2026-09-20, the Unified.to Discord connector is exactly this hybrid.** It requires an OAuth2 client ID and
> secret **and** a separately supplied bot token, and it sends the **bot token** on essentially every API call
> (`Authorization: Bot …` against `https://discord.com/api/v10/`). The customer's OAuth access token is used almost
> solely as a visibility filter: the connector lists the servers the *bot* is in, lists the servers the *user* is in,
> and intersects the two. So the OAuth app and the bot must be **the same Discord application**, the bot must be
> installed in a customer's server *and* the authorizing user must be a member of it, or that server simply will not
> appear. Confirm the current design with the connector's owner before registering anything.

## 6. Scopes

Scopes are **space-delimited** in the authorize URL and must be URL-encoded. Discord's documented catalogue, with
the strings exactly as Discord spells them:

| Scope | Grants |
| --- | --- |
| `identify` | "allows `/users/@me` without `email`" |
| `email` | "enables `/users/@me` to return an `email`" |
| `guilds` | "allows `/users/@me/guilds` to return basic information about all of a user's guilds" |
| `guilds.members.read` | "allows `/users/@me/guilds/{guild.id}/member` to return a user's member information in a guild" |
| `guilds.join` | "allows `/guilds/{guild.id}/members/{user.id}` to be used for joining users to a guild" |
| `gdm.join` | join users to a group DM |
| `bot` | "for oauth2 bots, this puts the bot in the user's selected guild by default" — see §7 |
| `applications.commands` | "allows your app to add commands to a guild — included by default with the `bot` scope" |
| `applications.commands.update` | update its own commands with a Bearer token — client credentials grant only |
| `applications.commands.permissions.update` | update command permissions in a guild the user has permissions in |
| `connections` | "allows `/users/@me/connections` to return linked third-party accounts" |
| `role_connections.write` | update a user's connection and metadata for the app |
| `webhook.incoming` | "generates a webhook that is returned in the oauth token response for authorization code grants" |
| `applications.builds.read`, `applications.entitlements`, `applications.store.update` | app build/store/entitlement data |
| `messages.read` | local RPC server access only — not a REST scope |

**`guilds` and `guilds.members.read` are not the same thing, and the difference bites.** `guilds` gets you the
*list* of servers the user is in — id, name, icon, whether they own it, and their permissions in it. It tells you
nothing about who else is in those servers. `guilds.members.read` gets you **one** member object: the authorizing
user's own membership (nickname, roles, joined date) in **one specific server**, one server at a time. Neither is a
membership roster. Enumerating a server's members is a **bot-token** operation, and pulling it over the gateway
needs the privileged `GUILD_MEMBERS` intent (§8). A connector that maps "employees" or "members" from Discord is
doing bot-token work, and no amount of user scopes substitutes.

**Scopes Discord gates behind approval.** The catalogue marks these "only available to approved partners" or
otherwise needing Discord's approval: `activities.read`, `activities.write`, `applications.builds.upload`,
`dm_channels.read`, `identify.premium`, `relationships.read`, `voice`, and every `rpc*` scope (`rpc`,
`rpc.activities.write`, `rpc.notifications.read`, `rpc.voice.read`, `rpc.voice.write`). Do not put any of these in a
scope list on the assumption you can request them; if the connector needs one, that is a partnership conversation
with Discord, not a portal toggle. (One recent movement: **21 January 2026**, `relationships.read` was opened to
Activities under the Social SDK terms without requiring approval — evidence this list moves, so re-check it.)

**There is no `openid` scope.** Use `identify` and `email` for sign-in.

> **As of 2026-09-20, the Unified.to Discord connector requests `bot`, `identify`, `email` and `guilds` for its
> messaging objects, and adds `guilds.members.read` for the people/employee object.** Its sign-in-only flow requests
> `identify`, `email` and **`openid`** — and `openid` is not in Discord's documented catalogue, so that entry should
> be reviewed with the connector's owner rather than replicated. The connector does **not** send PKCE.

## 7. The `bot` scope, the permissions bitfield, and how a server install actually works

Adding `bot` to the scope list turns the authorization into a **server install**. The extra query parameters:

| Parameter | Meaning |
| --- | --- |
| `permissions` | "an integer corresponding to the permission calculations for the bot" — the role the bot gets in that server |
| `guild_id` | pre-selects a server: "that guild will be preselected in the dialog if that user has permission to add the bot to that guild" |
| `disable_guild_select` | `true` "disallow[s] the user from picking a different guild" |
| `prompt` | `consent` re-approves the authorization; `none` skips the screen for an existing authorization. **Those are the two documented values** |
| `integration_type` | `0` = GUILD_INSTALL (server), `1` = USER_INSTALL (the authorizing user only) |

`response_type` is optional for a bare `bot` (+ `applications.commands`) install link. **Request any scope outside
those two and `response_type=code` becomes mandatory**, the flow becomes a full authorization code grant, and the
token response additionally contains a `guild` object describing the server the bot was just added to — useful for
recording the install without a second call. The bot token is still not in there (§5).

**The trap: `permissions` and intents are different bitfields with colliding bit numbers.** Both are integers, both
are built from `1 << n`, and they mean completely unrelated things. `1 << 15` is:

- **`ATTACH_FILES`** (`0x0000000000008000`, 32768) in the **permissions** bitfield — "Allows for uploading images
  and files";
- **`MESSAGE_CONTENT`** in the **gateway intents** bitfield (§8).

Putting the `MESSAGE_CONTENT` bit into the `permissions` parameter does **not** grant message content. It silently
asks for `ATTACH_FILES` and moves on. Message content is a portal toggle plus an intents value sent on the gateway
identify — nothing in the authorize URL can turn it on. If you inherit a bitfield with a comment that does not match
the permission at that bit, re-derive it from the permissions table rather than trusting the comment.

Values worth having at hand: `ADMINISTRATOR` = `1 << 3` (8), `MANAGE_GUILD` = `1 << 5` (32), `VIEW_CHANNEL` =
`1 << 10` (1024), `SEND_MESSAGES` = `1 << 11` (2048), `ATTACH_FILES` = `1 << 15` (32768), `READ_MESSAGE_HISTORY` =
`1 << 16` (65536), `MANAGE_WEBHOOKS` = `1 << 29` (536870912).

**Do not reach for `ADMINISTRATOR`.** It "allows all permissions and bypasses channel permission overwrites" — it is
the whole server, including channels the installing admin never intended to share, and it "overrides any potential
permission overwrites," so a customer cannot scope it down afterwards with channel settings. It also drags in the
server-wide-2FA requirement (§2). Every customer security review asks about it, and "it was easier" is a bad answer.
For a read-and-post messaging connector the honest minimum is closer to `VIEW_CHANNEL | READ_MESSAGE_HISTORY |
SEND_MESSAGES` (1024 + 65536 + 2048 = 68608), plus `ATTACH_FILES` if you upload and `MANAGE_WEBHOOKS` if you create
webhooks. Compute it from the table, write down which bits you chose and why, and hand that list over.

> **As of 2026-09-20, the Unified.to Discord connector's authorize URL hard-codes `permissions=32776`** — that is
> `ADMINISTRATOR` (8) plus the `1 << 15` bit (32768), which in a permissions bitfield is `ATTACH_FILES`, not
> `MESSAGE_CONTENT`. It also sends `prompt=select_account`, which is not one of Discord's two documented `prompt`
> values, and it does not send `guild_id`, `disable_guild_select` or `integration_type`. Raise all three with the
> connector's owner rather than copying them into a new app's install link.

## 8. Privileged gateway intents — the wall

Intents only matter if you open a **gateway (WebSocket) connection**. A REST-only integration can skip this section
entirely. If you do connect to the gateway, this is where Discord integrations most often stall.

Three intents are **privileged**:

| Intent | Bit | Needed for |
| --- | --- | --- |
| `GUILD_MEMBERS` | `1 << 1` | Member list and member add/update/remove events |
| `GUILD_PRESENCES` | `1 << 8` | Presence / online status |
| `MESSAGE_CONTENT` | `1 << 15` | The **text** of messages your app was not directly mentioned in |

Non-privileged ones you will pair with them include `GUILDS` (`1 << 0`) and `GUILD_MESSAGES` (`1 << 9`) — note that
`GUILD_MESSAGES` gives you message *events*, and without `MESSAGE_CONTENT` those events arrive with empty content
fields. That is the failure people describe as "the bot sees messages but they're blank."

How it works now:

1. **Toggle them in the portal** — on the **Bot** page, under **Privileged Gateway Intents**. Discord: you must
   "toggle the privileged intents on the Bot page under the Privileged Gateway Intents section" before using them.
   `GUILD_PRESENCES` and `GUILD_MEMBERS` events "are turned off by default on all API versions."
2. **Send the matching bitfield on identify.** If you pass a privileged intent your app is not configured or
   approved for, "your Gateway connection will be closed with a **4014** close code." It does not degrade to a
   partial feed — it disconnects.
3. **At 10,000 users, it stops being self-serve.** Once the app can reach 10,000 unique users across its servers,
   the app or team owner gets a system DM and/or email and has **90 days** to submit a privileged intent review.
   Miss the window and "your app's current Privileged Intents access will be removed" — you can reapply later.
   Approved apps must **reapply annually**.
4. **You keep growing during review.** The app "will continue to function with the intents you are requesting
   access to" while the submission is pending, and — unlike the pre-June-2026 regime — it is no longer blocked from
   joining new servers.

The review asks you to "be specific about your use case" with concrete examples, request only the intents you
genuinely need, and explain data handling, retention and security. A request is rejected when "the information
provided in your submission does not demonstrate that the stated use case requires access." Resubmission is allowed.

**Plan for this at design time, not at 10,000 users.** If the connector's value proposition depends on reading
message bodies across customers' servers, `MESSAGE_CONTENT` is a business dependency with an annual renewal and a
human reviewer attached. Say so out loud in the summary. If the feature can be built on data the app is directly
addressed with (mentions, slash commands, interactions) the intent is not needed at all, and that is worth an hour
of design conversation before it is worth a submission.

Note one documentation inconsistency to expect: the application-object flags still describe
`GATEWAY_MESSAGE_CONTENT_LIMITED` / `GATEWAY_GUILD_MEMBERS_LIMITED` in terms of a **100-server** split, which is the
pre-June-2026 framing. The gateway and review pages carry the current 10,000-user rule. Trust the newer pages and
flag the discrepancy rather than reasoning from the stale one.

## 9. Capture the credentials

| Credential | Where | Re-readable? |
| --- | --- | --- |
| **Application ID** (= OAuth2 client ID) | General Information | Yes |
| **Public Key** | General Information — verifies inbound interaction request signatures (Ed25519) | Yes |
| **Client secret** | OAuth2 page, **Reset Secret** | Treat as reset-only |
| **Bot token** | Bot page, **Reset Token** | **No — shown once** |

Endpoints to record alongside them:

- Authorize: `https://discord.com/oauth2/authorize`
- Token exchange **and** refresh: `POST https://discord.com/api/oauth2/token`
- Revoke: `POST https://discord.com/api/oauth2/token/revoke`
- API base: `https://discord.com/api`, current version **v10** (`https://discord.com/api/v10/`)

**Token mechanics:**

- Exchange and refresh are `Content-Type: application/x-www-form-urlencoded` POSTs. **JSON is rejected.** Exchange
  sends `grant_type=authorization_code`, `code`, `redirect_uri`; refresh sends `grant_type=refresh_token`,
  `refresh_token`.
- Discord documents client credentials on the OAuth2 token endpoints via **HTTP Basic authentication** — client ID
  as the username, client secret as the password. (Many clients instead put `client_id`/`client_secret` in the form
  body; if yours does, note it as a deviation from the documented form and verify it against a live exchange rather
  than assuming.)
- The documented response is `access_token`, `token_type: "Bearer"`, `expires_in: 604800`, `refresh_token`, `scope`
  — and, for a `bot`-scope authorization combined with other scopes, a `guild` object.
- **`state` is not required by Discord but is strongly recommended**: Discord calls out CSRF and clickjacking and
  says `state` "should be a value that binds the user's request to their authenticated state." Send it; validate it.
- **Revocation is authorization-wide.** `POST .../token/revoke` takes `token` and an optional `token_type_hint`, and
  "when you revoke a token, any active access or refresh tokens associated with that authorization will be revoked,
  regardless of the `token` and `token_type_hint` values you pass in." There is no revoke-just-this-one.
- **The client credentials grant** (`grant_type=client_credentials`, Basic auth) returns "an access token for the
  bot owner" — it is a testing convenience for your own account, not a multi-tenant mechanism. Discord's own warning
  about keeping the secret out of source code is unusually emphatic here; take the hint.

**Rotation.** Resetting the **client secret** invalidates the old one immediately: existing access tokens live out
their remaining time, but every future exchange and refresh fails until the new secret is deployed. Resetting the
**bot token** is worse — it is the credential doing the actual work, so every server's reads and writes stop the
instant it is reset, across all customers at once, until the new token is deployed. Never reset either without
explicit go-ahead and a cutover plan.

Report each secret once so the user can paste it into their secret store, say plainly that it is now in the
transcript and can be reset, then move on.

## 10. Rate limits, and the Cloudflare ban nobody plans for

- **Global:** "All bots can make up to **50 requests per second** to our API," independent of per-route limits.
- **Per route, per top-level resource.** Limits are bucketed by route *and* by the top-level resource in the path
  (channel, guild, webhook), so "an endpoint with two different top-level resources may calculate limits
  independently." One busy server does not have to throttle another — unless your client shares a bucket by
  accident.
- **Headers on every response:** `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (epoch seconds),
  `X-RateLimit-Reset-After` (seconds), `X-RateLimit-Bucket` (the bucket's opaque id), and on a 429
  `X-RateLimit-Scope` with value `user`, `global` or `shared`. Honour `X-RateLimit-Reset-After` and `X-RateLimit-
  Bucket`; a client that only reacts to 429s is already late.
- **429 body** carries `retry_after` (seconds to wait), `global` (whether it is the global limit) and `message`.
- **The one that takes you off the air: Cloudflare bans.** "IP addresses that make too many invalid HTTP requests
  are automatically and temporarily restricted from accessing the Discord API," with the threshold at **10,000
  responses of 401, 403 or 429 per 10 minutes**. Note what counts: **expired tokens (401) and missing permissions
  (403) count as invalid**, not just rate-limit hits. A connector looping over thousands of connections with stale
  tokens, or retrying 403s from servers where the bot lacks a permission, can ban the *platform's own egress IP* for
  every customer — a multi-tenant outage caused by single-tenant misconfiguration. Back off on 401/403 and stop
  re-trying them.
- **Send a real User-Agent.** Discord requires the form `User-Agent: DiscordBot ($url, $versionNumber)` and warns
  that "client requests that do not have a valid User Agent specified may be blocked and return a Cloudflare error."
  Confirm your HTTP client actually sets one rather than shipping a library default.

## 11. Verification, App Directory, and growth gates

Two separate gates, now decoupled (they were one process before June 2026):

- **App Verification — required to grow past 100 servers.** The portal has an **App Verification** tab with a
  checklist; the headline requirement is that the **owner of the development team must verify their identity through
  Stripe**, Discord's identity provider. Until verified, the app cannot join its 101st server. Start this early:
  it depends on a specific human completing an identity check, which is exactly the kind of thing that blocks a
  launch for a week.
- **Discovery / App Directory listing — optional.** Enabling Discovery requires "your team owner to complete
  identity and application verification"; you then work the **Discovery → Discovery Status** checklist in the portal
  and click **Enable Discovery**, after which "it may take up to 24 hours for your app to appear in the App Directory
  and App Launcher." Discord's inclusion guidelines additionally require a **publicly hosted privacy policy and
  terms of service**, compliance with the Discord Terms of Service, Community Guidelines, Developer Terms of Service
  and Developer Policy, no age-restricted content, a configured **install link**, implemented **slash commands**,
  and **2FA on your own account**. A listing is a go-to-market decision; it is not needed to serve customers.

**Stop and ask** before answering anything you would have to invent on a verification or Discovery submission:
company identity details, data-retention periods, subprocessors, security questionnaires, support SLAs, or user
counts.

## 12. Verify end-to-end

A half-working registration surfaces later as a customer who cannot connect. Prove it, and prove **each credential
separately** — this is the step that catches the §5 mistake:

1. **Install into your test server through the real flow** — your platform's connect button or the real install
   link, not a hand-built URL, so the redirect URI matching is actually exercised.
2. **Read the consent screen.** Confirm it names the server, lists the permissions you expect, and does not mention
   permissions you did not intend (`ADMINISTRATOR` shows up as "Administrator" and is unmistakable).
3. **Complete the token exchange**, confirm you got `access_token`, `refresh_token`, `expires_in` and the `scope`
   string you asked for — and, for a `bot` install with extra scopes, the `guild` object.
4. **Make one call with the Bearer token** (e.g. the current user, or the user's guild list) and **one call with the
   `Bot` token** (e.g. a guild's channels). Both must work if your connector is a hybrid. Passing one and failing
   the other is the whole failure mode.
5. **Force a refresh** before the 7 days are up and confirm the connection survives it.
6. **If you use the gateway**, connect and confirm you receive a message event *with non-empty content* from a
   channel the bot was not mentioned in. That is the only real test of `MESSAGE_CONTENT`.
7. **Test from a second account in a server you do not own**, where the installer is an admin but not you. That is
   the path a customer takes.

| Symptom | Cause |
| --- | --- |
| "Only the application owner can add this bot" | **Public Bot** is off on the Bot page (§3) |
| Error page instead of a consent screen; no callback fired | `redirect_uri` not registered, or not url-encoded / not identical (§4) |
| Install link works until someone adds a scope, then breaks | Any scope beyond `bot`/`applications.commands` makes `response_type=code` mandatory (§7) |
| `invalid_scope` | Scope string not in Discord's catalogue (e.g. `openid`), or a scope that needs Discord's approval (§6) |
| Install "succeeds" but every API call 401s | Using the OAuth access token where the bot token is required, or vice versa (§5) |
| Waiting for a bot token in the token exchange response | It is never returned there — copy it from the Bot page (§5, §9) |
| Bot is in the server but sees no channels | Permissions bitfield too narrow, or channel-level overwrites exclude the bot's role (§7) |
| Message events arrive with empty content | `MESSAGE_CONTENT` intent not toggled / not approved (§8) |
| Gateway closes with **4014** | A privileged intent was sent that the app is not configured or approved for (§8) |
| Member list is empty or partial | `GUILD_MEMBERS` privileged intent, or a bot-token endpoint being called with a Bearer token (§6, §8) |
| Everything dies ~7 days after connecting | Client is not refreshing; access tokens are `expires_in: 604800` (§9) |
| `invalid_grant` on refresh | Authorization revoked, bot removed from the server, or the user revoked the app (§9) |
| Bot stops joining new servers at exactly 100 | App Verification not completed (§11) |
| Privileged intents silently removed | 90-day review window elapsed, or the annual reapplication was missed (§8) |
| Whole platform gets Cloudflare-blocked from the API | 10,000+ 401/403/429 responses in 10 minutes from one IP (§10) |
| Random requests blocked with a Cloudflare error | No valid `User-Agent: DiscordBot (...)` header (§10) |
| Customer's admin cannot install despite being an admin | Installing requires `MANAGE_GUILD`; server-wide 2FA also gates powerful permissions (§2) |

## 13. Hand off — never commit the secret

- **Do not** write the client secret, the bot token or the public key into source control, a test, a fixture, a
  committed `.env`, a ticket, a PR body, or a chat channel. Values go to the user, for the secret store or console.
  The bot token deserves extra care: it is one credential shared across every customer, and it cannot be re-read
  after it is reset.
- If a code change is needed (a new callback host, a scope list, a permissions bitfield), keep it secret-free and
  say what the human must set out of band.
- Close with: app name, Application ID, and the **Team** that owns it; where the client secret and bot token were
  delivered; the authorize / token / revoke URLs and the API base; **the exact scope strings**; **the permissions
  bitfield and which bits it is made of**; which gateway intents are toggled and whether any are privileged;
  Public Bot and Requires OAuth2 Code Grant states; verification and Discovery status; and anything left for the
  user to do.

## Stop and ask

Hand back to a human rather than guessing when: someone proposes `ADMINISTRATOR` in the permissions bitfield;
resetting a bot token or client secret that live connections depend on; enabling **Requires OAuth2 Code Grant** on
an app whose install flow does not complete an exchange; a privileged intent review or App Verification form asks
for identity, compliance, retention or volume claims; a scope on the list is one Discord gates behind partner
approval; the app is approaching 100 servers or 10,000 users and nobody owns the submission; transferring an app to
a Team (one-way); installing into a server you were not told to touch; or the portal and docs do not match the
**Platform state** section above.

## References

Official Discord documentation only; every URL below returned HTTP 200 on 2026-09-20. Discord's developer help
center (`support-dev.discord.com`) carries the App Verification and privileged-intent policy articles but is
Cloudflare-protected and not machine-fetchable — open those from the portal or a browser. Note that old
`discord.com/developers/docs/...` links 301 to `docs.discord.com/developers/...`.

- OAuth2 (scopes, flows, bot authorization, token exchange, revocation) — https://docs.discord.com/developers/topics/oauth2
- Permissions and the bitwise permission flags table — https://docs.discord.com/developers/topics/permissions
- Gateway, intents and privileged intents (close code 4014) — https://docs.discord.com/developers/events/gateway
- Getting started with privileged intent review (10,000 users, 90 days, annual renewal) — https://docs.discord.com/developers/gateway/getting-started-with-privileged-intent-review
- Gateway events — https://docs.discord.com/developers/topics/gateway-events
- API reference (auth headers, base URL, API v10, User-Agent) — https://docs.discord.com/developers/reference
- Rate limits, `X-RateLimit-*` headers and the invalid-request/Cloudflare ban — https://docs.discord.com/developers/topics/rate-limits
- Opcodes and status codes — https://docs.discord.com/developers/topics/opcodes-and-status-codes
- Application resource (installation contexts, install params, application flags) — https://docs.discord.com/developers/resources/application
- User resource (`/users/@me/guilds`, `shard`, the 200 cap, `/users/@me/guilds/{id}/member`) — https://docs.discord.com/developers/resources/user
- Guild resource — https://docs.discord.com/developers/resources/guild
- Channel resource — https://docs.discord.com/developers/resources/channel
- Message resource — https://docs.discord.com/developers/resources/message
- Teams (roles, 2FA, one-way transfer) — https://docs.discord.com/developers/topics/teams
- Getting started (portal walkthrough, bot token shown once) — https://docs.discord.com/developers/getting-started
- Enabling Discovery / App Directory — https://docs.discord.com/developers/discovery/enabling-discovery
- Webhook events — https://docs.discord.com/developers/events/webhook-events
- Change log (docs migration, privileged intent change, `shard` requirement) — https://docs.discord.com/developers/change-log
- Developer platform documentation index — https://docs.discord.com/developers/intro
- Developer portal — applications — https://discord.com/developers/applications
- Developer portal — teams — https://discord.com/developers/teams
- Discord Terms of Service — https://discord.com/terms
