---
title: Manual account linking
description: Manual account linking enables control over linking accounts with different identifiers or post-login.
sidebar:
  order: 4
---

## Overview

<PaidFeatureCallout />

Manual account linking allows you to take control of when and which accounts link.
With this, you can implement flows like:
- Connecting social login accounts to an existing account post login.
- Adding a password to an account that a social or passwordless login created.
- Linking accounts which don't have the same email or phone number, or have a different identifier altogether.


## Steps

### 1. Initialize the account linking recipe

<DependentContent passive group="backend-language">
<ContentOption title="Go" value="go">
:::note[At the moment this feature is not supported through the Go SDK.]
:::
</ContentOption>
</DependentContent>

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx
import supertokens, { User, RecipeUserId } from "supertokens-node";
import AccountLinking from "supertokens-node/recipe/accountlinking";
import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types";
import { SessionContainerInterface } from "supertokens-node/recipe/session/types";

supertokens.init({
  supertokens: {
    connectionURI: "...",
    apiKey: "...",
  },
  appInfo: {
    apiDomain: "...",
    appName: "...",
    websiteDomain: "...",
  },
  recipeList: [AccountLinking.init()],
});
```
</Tab>
<Tab title="Go" value="go">

</Tab>
<Tab title="Python" value="python">
```python check=false reason="Partial configuration example"
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import accountlinking


init(
    app_info=InputAppInfo(
        app_name="...",
        api_domain="...",
        website_domain="...",
    ),
    supertokens_config=SupertokensConfig(
        connection_uri="...",
    ),
    framework='...',
    recipe_list=[
        accountlinking.init()
    ],
)
```
</Tab>
</CodeGroup>

In the above, SuperTokens does not automatically link accounts (during sign up or sign in APIs) by returning `shouldAutomaticallyLink: false`. Initializing the recipe is still important to use the functions from the SDK as shown below.

It is of course possible to [enable auto account linking](./automatic-account-linking) and still use the functions for manual account linking below.

### 2. Create a primary user

To link two accounts, you first need to make one of them a primary user:

<DependentContent passive group="backend-language">
<ContentOption title="Go" value="go">
:::note[At the moment this feature is not supported through the Go SDK.]
:::
</ContentOption>
</DependentContent>

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx
import AccountLinking from "supertokens-node/recipe/accountlinking";
import { RecipeUserId } from "supertokens-node";

async function makeUserPrimary(recipeUserId: RecipeUserId) {
  let response = await AccountLinking.createPrimaryUser(recipeUserId);
  if (response.status === "OK") {
    if (response.wasAlreadyAPrimaryUser) {
      // The input user was already a primary user and accounts can be linked to it.
    } else {
      // User is now primary and accounts can be linked to it.
    }
    let modifiedUser = response.user;
    console.log(modifiedUser.isPrimaryUser); // will print true
  } else if (response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") {
    // This happens if there already exists another primary user with the same email or phone number
    // in at least one of the tenants that this user belongs to.
  } else if (response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR") {
    // This happens if this user is already linked to another primary user.
  }
}
```
</Tab>
<Tab title="Go" value="go">

</Tab>
<Tab title="Python" value="python">
<DependentContent group="python-io-style" label="I/O style">
<ContentOption title="Asyncio" value="asyncio">
```python
from supertokens_python.recipe.accountlinking.asyncio import create_primary_user
from supertokens_python.types import RecipeUserId

async def make_user_primary(recipe_user_id: RecipeUserId):
    response = await create_primary_user(recipe_user_id)
    if response.status == "OK":
        if response.was_already_a_primary_user:
            # The input user was already a primary user and accounts can be linked to it.
            pass
        else:
            # User is now primary and accounts can be linked to it.
            pass
        modified_user = response.user
        print(modified_user.is_primary_user)  # will print True
    elif response.status == "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR":
        # This happens if there already exists another primary user with the same email or phone number
        # in at least one of the tenants that this user belongs to.
        pass
    elif response.status == "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR":
        # This happens if this user is already linked to another primary user.
        pass
```
</ContentOption>
<ContentOption title="Syncio" value="syncio">
```python
from supertokens_python.recipe.accountlinking.syncio import create_primary_user
from supertokens_python.types import RecipeUserId

def make_user_primary(recipe_user_id: RecipeUserId):
    response = create_primary_user(recipe_user_id)
    if response.status == "OK":
        if response.was_already_a_primary_user:
            # The input user was already a primary user and accounts can be linked to it.
            pass
        else:
            # User is now primary and accounts can be linked to it.
            pass
        modified_user = response.user
        print(modified_user.is_primary_user)  # will print True
    elif response.status == "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR":
        # This happens if there already exists another primary user with the same email or phone number
        # in at least one of the tenants that this user belongs to.
        pass
    elif response.status == "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR":
        # This happens if this user is already linked to another primary user.
        pass
```
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

### 3. Link accounts

Once a user has become a primary user, you can link other accounts to this user:

<DependentContent passive group="backend-language">
<ContentOption title="Go" value="go">
:::note[At the moment this feature is not supported through the Go SDK.]
:::
</ContentOption>
</DependentContent>

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx
import AccountLinking from "supertokens-node/recipe/accountlinking";
import { RecipeUserId } from "supertokens-node";

// we are linking the input recipeUserId to the primaryUserId
async function linkAccounts(primaryUserId: string, recipeUserId: RecipeUserId) {
  let response = await AccountLinking.linkAccounts(recipeUserId, primaryUserId);
  if (response.status === "OK") {
    if (response.accountsAlreadyLinked) {
      // The input users were already linked
    } else {
      // The two users are now linked
    }
    let modifiedUser = response.user;
    console.log(modifiedUser.loginMethods); // this will now contain the login method of the recipeUserId as well.
  } else if (response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") {
    // This happens if there already exists another primary user with the same email or phone number
    // as the recipeUserId's account.
  } else if (response.status === "INPUT_USER_IS_NOT_A_PRIMARY_USER") {
    // This happens if the input primaryUserId is not actually a primary user ID.
    // You can call createPrimaryUserId and call linkAccountsAgain
  } else if (response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") {
    // This happens if the input recipe user ID is already linked to another primary user.
    // You can call unlink accounts on the recipe user ID and then try linking again.
  }
}
```
</Tab>
<Tab title="Go" value="go">

</Tab>
<Tab title="Python" value="python">
<DependentContent group="python-io-style" label="I/O style">
<ContentOption title="Asyncio" value="asyncio">
```python
from supertokens_python.recipe.accountlinking.asyncio import link_accounts
from supertokens_python.types import RecipeUserId

async def link_accounts_helper(primary_user_id: str, recipe_user_id: RecipeUserId):
    response = await link_accounts(recipe_user_id, primary_user_id)
    if response.status == "OK":
        if response.accounts_already_linked:
            # The input users were already linked
            pass
        else:
            # The two users are now linked
            pass
        modified_user = response.user
        print(modified_user.login_methods)  # this will now contain the login method of the recipeUserId as well.
    elif response.status == "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR":
        # This happens if there already exists another primary user with the same email or phone number
        # as the recipeUserId's account.
        pass
    elif response.status == "INPUT_USER_IS_NOT_A_PRIMARY_USER":
        # This happens if the input primaryUserId is not actually a primary user ID.
        # You can call create_primary_user and call link_accounts again
        pass
    elif response.status == "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR":
        # This happens if the input recipe user ID is already linked to another primary user.
        # You can call unlink_accounts on the recipe user ID and then try linking again.
        pass
```
</ContentOption>
<ContentOption title="Syncio" value="syncio">
```python
from supertokens_python.recipe.accountlinking.syncio import link_accounts
from supertokens_python.types import RecipeUserId

def link_accounts_helper(primary_user_id: str, recipe_user_id: RecipeUserId):
    response = link_accounts(recipe_user_id, primary_user_id)
    if response.status == "OK":
        if response.accounts_already_linked:
            # The input users were already linked
            pass
        else:
            # The two users are now linked
            pass
        modified_user = response.user
        print(modified_user.login_methods)  # this will now contain the login method of the recipeUserId as well.
    elif response.status == "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR":
        # This happens if there already exists another primary user with the same email or phone number
        # as the recipeUserId's account.
        pass
    elif response.status == "INPUT_USER_IS_NOT_A_PRIMARY_USER":
        # This happens if the input primaryUserId is not actually a primary user ID.
        # You can call create_primary_user and call link_accounts again
        pass
    elif response.status == "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR":
        # This happens if the input recipe user ID is already linked to another primary user.
        # You can call unlink_accounts on the recipe user ID and then try linking again.
        pass
```
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

### 4. Unlink accounts

If you want to unlink an account from its primary user ID, you can use the following function:

<DependentContent passive group="backend-language">
<ContentOption title="Go" value="go">
:::note[At the moment this feature is not supported through the Go SDK.]
:::
</ContentOption>
</DependentContent>

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx
import AccountLinking from "supertokens-node/recipe/accountlinking";
import { RecipeUserId } from "supertokens-node";

async function unlinkAccount(recipeUserId: RecipeUserId) {
  let response = await AccountLinking.unlinkAccount(recipeUserId);
  if (response.status === "OK") {
    if (response.wasLinked) {
      // This means that we unlinked the account from its primary user ID
    } else {
      // This means that the user was never linked in the first place
    }

    if (response.wasRecipeUserDeleted) {
      // This is true if we call unlinkAccount on the recipe user ID of the primary user ID user.
      // We delete this user because if we don't and we call getUserById() on this user's ID, SuperTokens
      // won't know which user info to return - the primary user, or the recipe user.
      // Note that even though the recipe user is deleted, the session, metadata, roles etc for this
      // primary user is still intact, and calling getUserById(primaryUserId) will still return
      // the user object with the other login methods.
    } else {
      // There not exists a user account which is not a primary user, with the recipeUserId = to the
      // input recipeUserId.
    }
  }
}
```
</Tab>
<Tab title="Go" value="go">

</Tab>
<Tab title="Python" value="python">
<DependentContent group="python-io-style" label="I/O style">
<ContentOption title="Asyncio" value="asyncio">
```python
from supertokens_python.recipe.accountlinking.asyncio import unlink_account
from supertokens_python.types import RecipeUserId

async def unlink_account_helper(recipe_user_id: RecipeUserId):
    response = await unlink_account(recipe_user_id)
    if response.was_linked:
        # This means that we unlinked the account from its primary user ID
        pass
    else:
        # This means that the user was never linked in the first place
        pass

    if response.was_recipe_user_deleted:
        # This is true if we call unlink_account on the recipe user ID of the primary user ID user.
        # We delete this user because if we don't and we call get_user_by_id() on this user's ID, SuperTokens
        # won't know which user info to return - the primary user, or the recipe user.
        # Note that even though the recipe user is deleted, the session, metadata, roles etc for this
        # primary user is still intact, and calling get_user_by_id(primary_user_id) will still return
        # the user object with the other login methods.
        pass
    else:
        # There now exists a user account which is not a primary user, with the recipe_user_id equal to the
        # input recipe_user_id.
        pass
```
</ContentOption>
<ContentOption title="Syncio" value="syncio">
```python
from supertokens_python.recipe.accountlinking.syncio import unlink_account
from supertokens_python.types import RecipeUserId

def unlink_account_helper(recipe_user_id: RecipeUserId):
    response = unlink_account(recipe_user_id)
    if response.was_linked:
        # This means that we unlinked the account from its primary user ID
        pass
    else:
        # This means that the user was never linked in the first place
        pass

    if response.was_recipe_user_deleted:
        # This is true if we call unlink_account on the recipe user ID of the primary user ID user.
        # We delete this user because if we don't and we call get_user_by_id() on this user's ID, SuperTokens
        # won't know which user info to return - the primary user, or the recipe user.
        # Note that even though the recipe user is deleted, the session, metadata, roles etc for this
        # primary user is still intact, and calling get_user_by_id(primary_user_id) will still return
        # the user object with the other login methods.
        pass
    else:
        # There now exists a user account which is not a primary user, with the recipe_user_id equal to the
        # input recipe_user_id.
        pass
```
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

### 5. Convert a `userId` into a `recipeUserId`

If you notice, the input to a lot of the functions above is of type `RecipeUserId`.
You can convert a string userID into a `RecipeUserId` in the following way:

<DependentContent passive group="backend-language">
<ContentOption title="Go" value="go">
:::note[At the moment this feature is not supported through the Go SDK.]
:::
</ContentOption>
</DependentContent>

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx
import SuperTokens from "supertokens-node";

async function getAsRecipeUserIdType(userId: string) {
  return SuperTokens.convertToRecipeUserId(userId);
}
```
</Tab>
<Tab title="Go" value="go">

</Tab>
<Tab title="Python" value="python">
```python
from supertokens_python.types import RecipeUserId

user_id = "some_user_id";
recipe_user_id = RecipeUserId(user_id)
```
</Tab>
</CodeGroup>

The reason for this type is that it prevents bugs wherein a function expects a recipe user ID (like `createNewSession`, or `updateEmailOrPassword` from email password recipe). However, you might pass in the primary user ID instead.

### 6. Other helper functions

Our SDK also exposes other helper functions:

- `AccountLinking.createPrimaryUserIdOrLinkAccounts`: Given a recipe user ID, this function attempts linking it with any primary user ID that has the same email or phone number associated with it. If no such primary user exists, this function makes the input user account a primary one.

- `AccountLinking.getPrimaryUserThatCanBeLinkedToRecipeUserId`: Given a recipe user ID, this function returns a primary user ID which this user can link to, based on matching emails / phone numbers. If no such primary user exists, this function returns `undefined`.

- `AccountLinking.canCreatePrimaryUser`: Given a recipe user ID, this function returns a status `OK` if the user can become a primary user, and a different status otherwise (indicating why it can't become a primary user). A user can become a primary user if there exists no other primary user with the same email or phone number across all the tenants that this user belongs to.

- `AccountLinking.canLinkAccounts`: Given a `recipeUserId` and a primary user ID, this function returns a status `OK` if the accounts can link, and if not, it returns a different status (indicating why the accounts can't link). Accounts can link if the recipe user ID is not already linked to another primary user, and if the resulting primary user does not have any email / phone number in common with another primary user across all the tenants that it belongs to.

- `AccountLinking.isSignUpAllowed`: Given the login info (email for example) of the new user, who is trying to sign up, this function returns `true` if it's safe to allow them to sign up, `false` otherwise. See the [error codes in the automatic account linking page](./automatic-account-linking#error-status-codes) to see why this might return `false`.

- `AccountLinking.isSignInAllowed`: Given the login info (email for example) of a user, who is trying to sign in, this function returns `true` if it's safe to allow them to sign in, `false` otherwise. See the [error codes in the automatic account linking page](./automatic-account-linking#error-status-codes) to see why this might return `false`.

- `AccountLinking.isEmailChangeAllowed`: Given the recipe user ID and the new email for update, this function returns `true` if it's safe to update the email, else `false`. Below are the conditions in which `false` returns:
    - If the input recipe user is a primary user, then ensure that the new email doesn't belong to any other primary user. If it does, the change is not allowed since multiple primary users can't have the same email.
    - If the recipe user is not a primary user, and if the new email is not verified, then check if there exists a primary user with the same email. If it exists, do not allow the email change. The disallowance occurs because if this email changes, and the system sends an email verification email, then the primary user may end up clicking on the link by mistake, causing account linking to happen which can result in account takeover if this recipe user is malicious.

---

## See also

<CardGroup cols={3}>
  <Card title="Automatic account linking" href="/post-authentication/account-linking/automatic-account-linking" />
  <Card title="Link social accounts" href="/post-authentication/account-linking/link-social-accounts" />
  <Card title="Add passwords to account" href="/post-authentication/account-linking/add-passwords-to-an-existing-account" />
  <Card title="Account deduplication" href="/post-authentication/user-management/account-deduplication" />
</CardGroup>
