Skip to main content
Documentation - Discord Docs

Search documentation

Type to search this documentation.

On this pageOverview

Merging Accounts

When a player wants to link their account to your game, merging will convert their provisional account to a full Discord account via authenticating with their Discord account.

We start a special version of the access token request flow where the provisional user's external identity is included.

Regardless of whether you merge server-side or from a public client, the flow starts with the standard Client::Authorize step and ends with Discord migrating the provisional account's data into the full Discord account.


Merging still begins on the client with Client::Authorize, but the token exchange must run on your backend because it requires your client secret — which must never ship in the game client. The client drives the authorization flow; the backend holds the secret and performs the exchange.

  1. The client creates a PKCE verifier and calls Client::Authorize with its code challenge. The SDK opens the Discord login UI and handles the redirect at http://127.0.0.1/callback itself, then returns the authorization code and the redirectUri to your callback.
  2. The client sends the code, the PKCE code_verifier, the redirectUri, and the provisional account's external_auth_token to your backend.
  3. Your backend exchanges the code at /oauth2/token — authenticated with your client ID and secret — adding external_auth_type and external_auth_token, and returns Discord's token JSON to the client.
  4. The client passes the returned access token to Client::UpdateToken and calls Client::Connect. The token exchange completes immediately, but Discord migrates the account's data asynchronously (see Data Migration During Merging) to the newly linked Discord account.

On the client, create a PKCE verifier, call Client::Authorize, and in the callback hand the code — along with the code_verifier, redirectUri, and the provisional account's external_auth_token — to your backend. Pass the merged access token your backend returns to Client::UpdateToken, then Client::Connect:

C++
// filepath: your_game/client/merge.cpp
auto codeVerifier = client->CreateAuthorizationCodeVerifier();

discordpp::AuthorizationArgs args{};
args.SetClientId(YOUR_DISCORD_APPLICATION_ID);
// Request the scopes your features need — communication scopes are required to send DMs.
args.SetScopes(discordpp::Client::GetDefaultCommunicationScopes());
args.SetCodeChallenge(codeVerifier.Challenge());

// The provisional account's external_auth_token. For the Bot Token Endpoint this is
// the access_token you received when the provisional account was created.
std::string externalAuthToken = GetProvisionalAccessToken();

client->Authorize(args, [client, codeVerifier, externalAuthToken](
    discordpp::ClientResult result, std::string code, std::string redirectUri) {
  if (!result.Successful()) {
    std::cerr << "❌ Authorization Error: " << result.Error() << std::endl;
    return;
  }

  // POST { code, code_verifier, redirect_uri, external_auth_token } to YOUR backend,
  // which holds the client secret and performs the /oauth2/token merge exchange.
  std::string accessToken =
      MergeOnBackend(code, codeVerifier.Verifier(), redirectUri, externalAuthToken);

  // Connect with the merged account's access token — no reconnect of the flow needed.
  client->UpdateToken(discordpp::AuthorizationTokenType::Bearer, accessToken,
      [client](discordpp::ClientResult result) {
        if (result.Successful()) {
          client->Connect();
        } else {
          std::cerr << "❌ Failed to update token: " << result.Error() << std::endl;
        }
      });
});

Backend: Exchange the Code for a Merged Token

Section titled “Backend: Exchange the Code for a Merged Token”

Extend the standard OAuth2 token exchange by posting to /oauth2/token with two additional parameters — external_auth_type and external_auth_token. Discord uses these to identify the provisional account and merge it into the full Discord account associated with the provided authorization code or device code.

If you created the provisional account using the Bot Token Endpoint, use DISCORD_BOT_ISSUED_ACCESS_TOKEN as the external_auth_type and the access_token returned by that endpoint (see Bot Token Endpoint Response) as the external_auth_token — not the external_user_id.

If you used External Credentials Exchange, the external_auth_token is the same credential you provided when creating the provisional account — for example, your OIDC identity token, Steam session ticket, or EOS access token.

See the External Auth Types table for the full list of supported external_auth_type values.

Request Body Parameters

Parameter Description
grant_type Must be authorization_code. This is the standard OAuth2 authorization code grant — the authorization code from the Client::Authorize flow is exchanged for an access token.
code The authorization code returned to your server after the user completes the Client::Authorize flow.
redirect_uri The redirect URI from the authorization request. The SDK returns this in the Client::Authorize callback — forward it to your backend unchanged; it must match exactly.
code_verifier The PKCE code verifier from Client::CreateAuthorizationCodeVerifier, forwarded from the client. Required because Client::Authorize sends a code challenge.
external_auth_type The type of external identity provider. See External Auth Types.
external_auth_token The external identity token. For example, for OIDC, this is the OIDC identity token. For DISCORD_BOT_ISSUED_ACCESS_TOKEN, this is the access_token returned by the Bot Token Endpoint (not the external_user_id).
Python
import requests

API_ENDPOINT = 'https://discord.com/api/v10'
CLIENT_ID = '332269999912132097'
CLIENT_SECRET = '937it3ow87i4ery69876wqire'
# See External Auth Types for all supported values
EXTERNAL_AUTH_TYPE = 'DISCORD_BOT_ISSUED_ACCESS_TOKEN'

def exchange_code_with_merge(code, redirect_uri, code_verifier, external_auth_token):
  data = {
    'grant_type': 'authorization_code',
    'code': code,
    'redirect_uri': redirect_uri,
    'code_verifier': code_verifier,           # PKCE verifier forwarded from the client
    'external_auth_type': EXTERNAL_AUTH_TYPE,
    'external_auth_token': external_auth_token
  }
  headers = {
    'Content-Type': 'application/x-www-form-urlencoded'
  }
  r = requests.post('%s/oauth2/token' % API_ENDPOINT, data=data, headers=headers, auth=(CLIENT_ID, CLIENT_SECRET))
  r.raise_for_status()
  return r.json()

Request Body Parameters

Parameter Description
grant_type Must be urn:ietf:params:oauth:grant-type:device_code. This is the RFC 8628 device authorization grant, used for consoles and devices without a browser.
device_code The device code from the device authorization flow. See Account Linking on Consoles.
external_auth_type The type of external identity provider. See External Auth Types.
external_auth_token The external identity token. For example, for OIDC, this is the OIDC identity token. For DISCORD_BOT_ISSUED_ACCESS_TOKEN, this is the access_token returned by the Bot Token Endpoint (not the external_user_id).
Python
import requests

API_ENDPOINT = 'https://discord.com/api/v10'
CLIENT_ID = '332269999912132097'
CLIENT_SECRET = '937it3ow87i4ery69876wqire'
# See External Auth Types for all supported values
EXTERNAL_AUTH_TYPE = 'DISCORD_BOT_ISSUED_ACCESS_TOKEN'

def exchange_device_code_with_merge(device_code, external_auth_token):
  data = {
    'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
    'device_code': device_code,
    'external_auth_type': EXTERNAL_AUTH_TYPE,
    'external_auth_token': external_auth_token
  }
  headers = {
    'Content-Type': 'application/x-www-form-urlencoded'
  }
  r = requests.post('%s/oauth2/token' % API_ENDPOINT, data=data, headers=headers, auth=(CLIENT_ID, CLIENT_SECRET))
  r.raise_for_status()
  return r.json()
JSON
{
  "access_token": "<access token>",
  "token_type": "Bearer",
  "expires_in": 604800,
  "refresh_token": "<refresh token>",
  "scope": "sdk.social_layer"
}

Merging Provisional Accounts for Public Clients

Section titled “Merging Provisional Accounts for Public Clients”

If you do not have a backend, leverage the Client::GetTokenFromProvisionalMerge (Desktop & Mobile) or Client::GetTokenFromDeviceProvisionalMerge (Console) method, which will handle the entire process for you. You'll want to first enable Public Client on your Discord application's OAuth2 tab on the Discord developer portal. You can then leverage the Client::GetTokenFromProvisionalMerge or Client::GetTokenFromDeviceProvisionalMerge method using just the client.

This function should be used with the Client::Authorize function whenever a user with a provisional account wants to link an existing Discord account, merging their provisional account into that full Discord account.

The account merging process starts like the normal login flow, invoking the Client::Authorize method to get an authorization code back. Instead of calling GetToken, call this function and pass on the provisional user's identity.

Discord can then find the provisional account with that identity and the new Discord account and merge any data as necessary.

See the documentation for Client::GetToken for more details on the callback. Note that the callback will be invoked when the token exchange is complete, but merging accounts happens asynchronously and will not be complete yet.

C++
// Create a code verifier and challenge if using GetToken
auto codeVerifier = client->CreateAuthorizationCodeVerifier();
discordpp::AuthorizationArgs args{};
args.SetClientId(YOUR_DISCORD_APPLICATION_ID);
// Request the scopes your features need — communication scopes are required to send DMs.
args.SetScopes(discordpp::Client::GetDefaultCommunicationScopes());
args.SetCodeChallenge(codeVerifier.Challenge());

client->Authorize(args, [client, codeVerifier](discordpp::ClientResult result, std::string code, std::string redirectUri) {
  if (!result.Successful()) {
    std::cerr << "❌ Authorization Error: " << result.Error() << std::endl;
  } else {
    std::cout << "✅ Authorization successful! Next step: GetTokenFromProvisionalMerge \n";

    // Retrieve your external auth token
    std::string externalAuthToken = GetExternalAuthToken();

    client->GetTokenFromProvisionalMerge(YOUR_DISCORD_APPLICATION_ID, code, codeVerifier, redirectUri, discordpp::AuthenticationExternalAuthType::OIDC, externalAuthToken,[client](
      discordpp::ClientResult result,
      std::string accessToken,
      std::string refreshToken,
      discordpp::AuthorizationTokenType tokenType,
      int32_t expiresIn,
      std::string scope) {
        if (result.Successful()) {
          std::cout << "🔓 Token received! Establishing connection...\n";
          client->UpdateToken(discordpp::AuthorizationTokenType::Bearer, accessToken, [client](discordpp::ClientResult result) {
            client->Connect();
          });
        } else {
          std::cerr << "❌ Token request failed: " << result.Error() << std::endl;
        }
    });

  }
});

The endpoint validates, mints and returns the OAuth2 access token immediately, then the actual account-data merge runs in the background

When a user merges their provisional account with a Discord account, a new OAuth2 access token immediately for the Discord account is created and returned immediately. The provisional account's data is transferred onto the full Discord account asynchronously, and the provisional account is deleted once the merge completes.

If the user later unlinks, a new provisional account with a new unique ID is created (see Unmerging Accounts).

The following data is automatically transferred:

  • ✅ Friends: All In-game and Discord friendships made through the provisional account
  • ✅ Lobby Memberships: Active and historical lobby participation
  • ✅ DM Messages: Direct messages and history
  • ✅ Blocks: Users the provisional account blocked, and users who blocked the provisional account

This migration ensures users don't lose their social connections built while using the provisional account.

Where the two accounts disagree about a user, the Discord account's existing relationship wins. A block carried over from the provisional account is not applied to someone the Discord account is already friends with, and an existing block on the Discord account is never replaced by a friendship.


You may receive a merge specific error code while attempting this operation:

Code HTTP Status Meaning Solution
50025 403 Invalid OAuth2 access token The external_auth_token is invalid.
530014 400 Invalid merge source The source account is not provisional
530016 400 Invalid merge destination The destination account is provisional
530017 400 Merge source user banned The provisional account being merged is banned from platform
530023 400 Too many application identities User already has an associated external identity for this application
- 423 Resource locked Transient error, wait and retry

Need help? Join the Discord Developers Server and share questions in the #social-sdk-dev-help channel for support from the community.

If you encounter a bug while working with the Social SDK, please report it here: https://dis.gd/social-sdk-bug-report


Date Changes
September 8, 2026 Document block migration during merges
July 14, 2026 Split into the Provisional Accounts section and documented the client-to-server merge flow
March 17, 2025 Initial release
Suggest an edit

Propose a replacement for this page. The site team reviews it before applying any changes.

Export
Documentation menu