Skip to main content

Prerequisites

Before this page: understand account conflict resolution. Merging is the recovery path for one specific conflict — a MergeAccountsConfirmationError.
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; 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.
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

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 chosenUserId = await chooseValue({
      label: conflict.field.label ?? conflict.field.name,
      keepCurrent: conflict.currentUser.value,
      keepIncoming: conflict.fromUser.value,
      currentUserId: conflict.currentUser.userId,
      incomingUserId: conflict.fromUser.userId,
    });

    resolutions.push({
      fieldKey: conflict.field.name,
      userId: chosenUserId,
      type: conflict.field.type,
    });
  }

  return resolutions;
}

Handling errors

SituationWhat to do
MergeAccountsConfirmationError with mergeConflicts === undefinedConfirm, then mergeUserAccounts() with no arguments.
MergeAccountsConfirmationError with conflictsCollect one MergeUserConflictResolution per conflict, then mergeUserAccounts({ conflictResolutions }).
mergeUserAccounts rejectsKeep the user on their current account (the merge didn’t happen) and let them retry or cancel.
Missing a resolution for a conflicting fieldDon’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 for the full set of link conflicts.
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) rather than the returned value if the rest of your UI already reads from there.

See also

Last modified on July 10, 2026