Prerequisites
Before this page: get a user signed in and, ideally, set up. Conflicts only arise while a user is authenticated and adding another credential to their account.
What you’ll build
When a signed-in user adds a new credential — verifying another email or phone, connecting a social account, or linking a wallet — that credential is attached to their current account. But it might already belong to someone else. This page shows how to detect that conflict and guide the user to a resolution instead of failing silently.
There are three outcomes the SDK surfaces, and each needs different UI:
| Outcome | The SDK throws | What it means |
|---|
| Mergeable | MergeAccountsConfirmationError | The credential belongs to another account, but the two can be merged into one. You ask the user to confirm, then complete the merge. |
| Wallet already taken | WalletAlreadyLinkedToAnotherUserError | The wallet is verified on another account and can’t be moved. |
| Can’t reassign | LinkCredentialError | The credential is tied to another account and cannot be reassigned or merged. |
Linking uses the same verification calls you use to sign in — verifyOTP for email/phone, the social verification flow, and wallet verification. When a user is already signed in, a successful verification links the credential; a conflict throws one of the errors above.
Detecting and routing the conflict
Wrap the verification call and branch on the error type. The three cases map directly to three pieces of UI.
import {
MergeAccountsConfirmationError,
WalletAlreadyLinkedToAnotherUserError,
LinkCredentialError,
} from '@dynamic-labs-sdk/client';
// `linkCredential` is your call to the relevant verification function —
// e.g. verifyOTP({ ... }) for an email/phone, or the wallet verification flow.
async function addCredential(linkCredential: () => Promise<void>) {
try {
await linkCredential();
// showLinked is your implementation.
showLinked('Added to your account.');
} catch (error) {
if (error instanceof MergeAccountsConfirmationError) {
// Mergeable: hand off to the merge flow with the surfaced conflicts.
// startMergeFlow is your implementation — see "Account merging".
startMergeFlow(error.mergeConflicts);
return;
}
if (error instanceof WalletAlreadyLinkedToAnotherUserError) {
showConflict('That wallet is already connected to a different account.');
return;
}
if (error instanceof LinkCredentialError) {
showConflict(
'That credential already belongs to another account and can\'t be moved.',
);
return;
}
throw error;
}
}
import {
MergeAccountsConfirmationError,
WalletAlreadyLinkedToAnotherUserError,
LinkCredentialError,
} from '@dynamic-labs-sdk/client';
function AddCredentialButton() {
// useLinkCredential is your own TanStack mutation wrapping the verification
// call you use to add a credential — verifyOtp for email/phone, the social
// flow, or wallet verification. A successful mutation links the credential;
// a conflict rejects with one of the errors below, so the routing lives in
// onError. showLinked / showConflict / startMergeFlow are your implementation.
const { mutate: linkCredential, isPending } = useLinkCredential({
onSuccess: () => showLinked('Added to your account.'),
onError: (error) => {
if (error instanceof MergeAccountsConfirmationError) {
// Mergeable: hand off to the merge flow with the surfaced conflicts.
startMergeFlow(error.mergeConflicts);
return;
}
if (error instanceof WalletAlreadyLinkedToAnotherUserError) {
showConflict('That wallet is already connected to a different account.');
return;
}
if (error instanceof LinkCredentialError) {
showConflict(
"That credential already belongs to another account and can't be moved.",
);
return;
}
},
});
return (
<button onClick={() => linkCredential()} disabled={isPending}>
Add credential
</button>
);
}
Presenting each outcome
Mergeable (MergeAccountsConfirmationError). This is the only recoverable conflict. The error carries mergeConflicts — the fields where the two accounts disagree (for example, both have a display name). Confirm the merge with the user, let them pick a value for each conflicting field, and complete it. The full walkthrough lives in Account merging.
Wallet already taken (WalletAlreadyLinkedToAnotherUserError). The wallet is verified on another account, so it can’t be linked here. Tell the user plainly and offer an alternative (connect a different wallet, or sign in to the account that owns this one).
Can’t reassign (LinkCredentialError). A hard stop — the credential can’t be moved or merged. Keep the user on their current account and suggest using a different credential.
Show the conflict message next to the input the user just acted on (the email field, the connect-wallet button), not as a full-screen error. The user is signed in and nothing they’ve done is lost — only the new credential wasn’t added.
Handling errors
| Error | When | What to do |
|---|
MergeAccountsConfirmationError | Credential belongs to another account that can be merged | Start the merge flow; read error.mergeConflicts for the fields to resolve. |
WalletAlreadyLinkedToAnotherUserError | Wallet is verified on another account | Ask the user to connect a different wallet, or sign in to the owning account. |
LinkCredentialError | Credential can’t be reassigned or merged | Keep the current account; suggest a different credential. |
WalletScreeningBlockedError | Wallet was blocked by compliance screening | Handle separately — see Handling blocked wallets. |
See also