> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Account merging

> Complete an account merge when a user adds a credential that belongs to a mergeable second account — including resolving field-level conflicts.

## Prerequisites

Before this page: understand [account conflict resolution](https://docs.dynamic.xyz/docs/javascript/building-ui/account-conflict-resolution.md). Merging is the recovery path for one specific conflict — a `MergeAccountsConfirmationError`.

> [!NOTE]
>
> This page covers **only** the merging case (`MergeAccountsConfirmationError`). Linking a credential can fail in other ways — the credential already belongs to an account that **can't** be merged, or it's already in use elsewhere. For the full set of conflicts and how to handle each, see [account conflict resolution](https://docs.dynamic.xyz/docs/javascript/building-ui/account-conflict-resolution.md); use this page for the merge path only.

## What you'll build

When a signed-in user adds a credential that already belongs to **another mergeable account**, verification throws a `MergeAccountsConfirmationError`. Merging folds that second account into the current one so the user ends up with a single account holding both credentials.

Two cases:

1. **No conflicts** — the accounts don't disagree on any profile field. You confirm and merge in one call.
2. **Field-level conflicts** — both accounts have a value for the same field (say, a display name). The user picks which value to keep for each field, then you merge.

You'll build the confirmation UI, the per-field chooser, and the call that completes the merge.

> [!WARNING]
>
> Merging is destructive: the second account is absorbed into the current one. Always confirm with the user before calling `mergeUserAccounts` — don't merge automatically on catching the error.

## The merge flow

```
add credential → MergeAccountsConfirmationError → confirm → resolve conflicts (if any) → mergeUserAccounts → merged
```

The error carries `mergeConflicts`. When it's `undefined` there's nothing to resolve — merge directly. When present, it lists each conflicting `field` with the value held by `fromUser` (the account being merged in) and `currentUser` (the signed-in account).

## Completing the merge

> [tab] TypeScript
>
> ```typescript theme={"system"}
> import {
>   MergeAccountsConfirmationError,
>   mergeUserAccounts,
> } from '@dynamic-labs-sdk/client';
> import type {
>   MergeConflicts,
>   MergeUserConflictResolution,
> } from '@dynamic-labs-sdk/client';
>
> // `linkCredential` is your verification call (e.g. verifyOTP or wallet verify).
> async function addCredentialWithMerge(linkCredential: () => Promise<void>) {
>   try {
>     await linkCredential();
>   } catch (error) {
>     if (!(error instanceof MergeAccountsConfirmationError)) throw error;
>
>     // confirmMerge is your implementation — show a dialog explaining the two
>     // accounts will be combined, and resolve to true only if the user agrees.
>     const confirmed = await confirmMerge();
>     if (!confirmed) return;
>
>     if (!error.mergeConflicts) {
>       // No field-level conflicts — merge directly.
>       await mergeUserAccounts();
>       return;
>     }
>
>     // Ask the user which value to keep for each conflicting field.
>     const conflictResolutions = await resolveConflicts(error.mergeConflicts);
>     await mergeUserAccounts({ conflictResolutions });
>   }
> }
>
> // Turn the surfaced conflicts into one resolution per field. `chooseValue` is
> // your UI (a radio group, etc.) returning the userId whose value to keep.
> async function resolveConflicts(
>   mergeConflicts: MergeConflicts,
> ): Promise<MergeUserConflictResolution[]> {
>   const resolutions: MergeUserConflictResolution[] = [];
>
>   for (const conflict of mergeConflicts.conflicts) {
>     const { label, name, type } = conflict.field;
>     // `type` is optional on the field but required on a resolution, so a
>     // conflict without one can't be resolved. Skip it rather than prompt for a
>     // choice you can't send; the merge keeps the current user's value.
>     if (!type) continue;
>
>     const chosenUserId = await chooseValue({
>       label: label ?? name,
>       keepCurrent: conflict.currentUser.value,
>       keepIncoming: conflict.fromUser.value,
>       currentUserId: conflict.currentUser.userId,
>       incomingUserId: conflict.fromUser.userId,
>     });
>
>     resolutions.push({ fieldKey: name, userId: chosenUserId, type });
>   }
>
>   return resolutions;
> }
> ```

> [tab] React
>
> When you catch a `MergeAccountsConfirmationError`, read `error.mergeConflicts`: if it's present, render the `ConflictResolver` below with it; if it's `undefined`, confirm and call `merge()` with no arguments. The component renders one control per conflict and merges once every field has a decision:
>
> ```tsx theme={"system"}
> import type {
>   MergeConflicts,
>   MergeUserConflictResolution,
> } from '@dynamic-labs-sdk/client';
> import { useMergeUserAccounts } from '@dynamic-labs-sdk/react-hooks';
> import { useState } from 'react';
>
> function ConflictResolver({ mergeConflicts }: { mergeConflicts: MergeConflicts }) {
>   // Track the chosen userId per field key.
>   const [choices, setChoices] = useState<Record<string, string>>({});
>   const { mutateAsync: merge, isPending } = useMergeUserAccounts();
>
>   const allChosen = mergeConflicts.conflicts.every(
>     (conflict) => choices[conflict.field.name],
>   );
>
>   const handleMerge = async () => {
>     const conflictResolutions: MergeUserConflictResolution[] = [];
>     for (const conflict of mergeConflicts.conflicts) {
>       const { name, type } = conflict.field;
>       // `type` is optional on the field but required on a resolution, so a
>       // conflict without one can't be resolved and the merge keeps the
>       // current user's value.
>       if (!type) continue;
>       conflictResolutions.push({ fieldKey: name, type, userId: choices[name] });
>     }
>     await merge({ conflictResolutions });
>   };
>
>   return (
>     <form onSubmit={(event) => { event.preventDefault(); handleMerge(); }}>
>       <h2>Combine your accounts</h2>
>       {mergeConflicts.conflicts.map((conflict) => {
>         const field = conflict.field.name;
>         return (
>           <fieldset key={field}>
>             <legend>{conflict.field.label ?? field}</legend>
>             <label>
>               <input
>                 type="radio"
>                 name={field}
>                 checked={choices[field] === conflict.currentUser.userId}
>                 onChange={() =>
>                   setChoices((prev) => ({
>                     ...prev,
>                     [field]: conflict.currentUser.userId,
>                   }))
>                 }
>               />
>               Keep “{conflict.currentUser.value}”
>             </label>
>             <label>
>               <input
>                 type="radio"
>                 name={field}
>                 checked={choices[field] === conflict.fromUser.userId}
>                 onChange={() =>
>                   setChoices((prev) => ({
>                     ...prev,
>                     [field]: conflict.fromUser.userId,
>                   }))
>                 }
>               />
>               Use “{conflict.fromUser.value}”
>             </label>
>           </fieldset>
>         );
>       })}
>       <button type="submit" disabled={!allChosen || isPending}>
>         {isPending ? 'Merging…' : 'Combine accounts'}
>       </button>
>     </form>
>   );
> }
> ```
>
> For the no-conflict case, call `merge()` (with no arguments) straight from the confirmation dialog.

## Handling errors

| Situation | What to do |
| - | - |
| `MergeAccountsConfirmationError` with `mergeConflicts === undefined` | Confirm, then `mergeUserAccounts()` with no arguments. |
| `MergeAccountsConfirmationError` with conflicts | Collect one `MergeUserConflictResolution` per conflict, then `mergeUserAccounts({ conflictResolutions })`. |
| `mergeUserAccounts` rejects | Keep the user on their current account (the merge didn't happen) and let them retry or cancel. |
| Missing a resolution for a conflicting field | Don't call `mergeUserAccounts` until every conflict has a choice — the backend needs one resolution per field. |
| Any other error linking the credential (not `MergeAccountsConfirmationError`) | This page doesn't cover it — see [account conflict resolution](https://docs.dynamic.xyz/docs/javascript/building-ui/account-conflict-resolution.md) for the full set of link conflicts. |

> [!TIP]
>
> On success `mergeUserAccounts` resolves to the merged user and the session updates automatically. Re-render from your auth state (the `useUser` hook, in the [React hooks catalog](https://docs.dynamic.xyz/docs/javascript/reference/react-hooks-catalog.md)) rather than the returned value if the rest of your UI already reads from there.

## See also

* [Account conflict resolution](https://docs.dynamic.xyz/docs/javascript/building-ui/account-conflict-resolution.md) — detecting the conflict that leads here
* [Post-login user setup](https://docs.dynamic.xyz/docs/javascript/building-ui/post-auth-user-setup.md) — where added credentials often originate


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.