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.
The React SDK offers three approaches to step-up authentication, from fully managed to fully custom:
promptStepUpAuth — One call that automatically picks the right verification method and shows Dynamic’s built-in UI. Recommended for most use cases.
promptMfa / promptReauthenticate — Dynamic’s built-in UI with explicit control over which verification path is used.
- Individual verify methods — Full headless control for building your own UI.
After verification, the elevated access token is automatically stored and applied to subsequent API calls. You never need to manually handle the token.
For concepts, scopes, and token lifecycle, see Step-up authentication overview.
Prerequisites
DynamicContextProvider configured with your environment ID
- At least one verification method enabled in your dashboard security settings
- Step-up authentication enabled for your environment
Approach 1: Automatic prompt (recommended)
promptStepUpAuth checks whether the user has MFA methods and automatically routes to the correct UI — passkey/TOTP for MFA users, OTP/wallet for non-MFA users.
import { useStepUpAuthentication } from '@dynamic-labs/sdk-react-core';
import { TokenScope } from '@dynamic-labs/sdk-api-core';
const ExportButton = () => {
const { isStepUpRequired, promptStepUpAuth } = useStepUpAuthentication();
const handleExport = async () => {
if (await isStepUpRequired({ scope: TokenScope.Walletexport })) {
try {
await promptStepUpAuth({
requestedScopes: [TokenScope.Walletexport],
});
} catch {
return; // User cancelled or verification failed
}
}
// Token is stored — proceed with the operation.
// The SDK attaches it to the API call automatically.
await exportWallet();
};
return <button onClick={handleExport}>Export Wallet</button>;
};
How promptStepUpAuth routes
| User state | Verification method | UI shown |
|---|
| Has passkey + TOTP | Most recently created MFA method | Passkey prompt or TOTP input |
| Has passkey only | Passkey | Browser passkey prompt |
| Has TOTP only | TOTP | Dynamic’s TOTP code input |
| No MFA, has email | Email OTP | Dynamic’s re-auth method selection |
| No MFA, has SMS | SMS OTP | Dynamic’s re-auth method selection |
| No MFA, has wallet | Wallet signature | Dynamic’s re-auth method selection |
| No MFA, has social | Social OAuth | Dynamic’s re-auth method selection |
| Multiple non-MFA methods | User selects | Dynamic’s method chooser |
Approach 2: Dynamic’s built-in UI (explicit path)
Use promptMfa or promptReauthenticate when you want to control which verification path is used while still using Dynamic’s UI:
MFA prompt
Re-auth prompt
Shows Dynamic’s MFA verification UI (passkey or TOTP). Only works when the user has registered MFA methods.const { promptMfa } = useStepUpAuthentication();
await promptMfa({ requestedScopes: [TokenScope.Walletexport] });
Shows Dynamic’s re-authentication UI (email OTP, SMS OTP, wallet signature, or social OAuth). Works for all users.const { promptReauthenticate } = useStepUpAuthentication();
await promptReauthenticate({ requestedScopes: [TokenScope.Credentiallink] });
When MFA is enabled and the user has MFA methods, promptReauthenticate will be blocked by the backend. Use promptStepUpAuth or promptMfa instead.
Approach 3: Headless (custom UI)
For full control over the UI, use the individual verify methods. You build the UI — the hook handles verification and token storage.
Checking if step-up is required
checkStepUpAuth (recommended)
checkStepUpAuth is the recommended approach. It performs a server-authoritative check and returns both whether step-up is required and the available credentials, so you can route the user to the right verification method without extra API calls.
const { checkStepUpAuth } = useStepUpAuthentication();
const { isRequired, credentials, defaultCredentialId } =
await checkStepUpAuth({ scope: TokenScope.Walletexport });
if (isRequired) {
// credentials contains the available verification methods
// Each credential has: { id, format, type?, alias? }
//
// defaultCredentialId is the recommended credential to authenticate with:
// - For MFA users: the configured default device or most recently added
// - For re-auth users: the credential used to sign in
const defaultCred = credentials.find((c) => c.id === defaultCredentialId);
}
It does a fast local check first (if an elevated token exists, returns { isRequired: false } without an API call), then calls the backend. On failure, it defaults to { isRequired: true } for safety.
isStepUpRequired (simple boolean)
Use isStepUpRequired if you only need a boolean and don’t need the available credentials:
const { isStepUpRequired } = useStepUpAuthentication();
if (await isStepUpRequired({ scope: TokenScope.Walletexport })) {
// Show your step-up verification UI
}
Returns true when step-up auth is enabled for the environment and no valid elevated token exists for the scope. Returns false if the token already exists or step-up is not enabled.
Email / SMS OTP
const { sendOtp, verifyOtp } = useStepUpAuthentication();
// Send OTP to the user's sign-in enabled email or SMS credential
const verification = await sendOtp();
// verification.verificationType tells you whether it was 'email' or 'sms'
// After the user enters the code:
await verifyOtp({
verificationToken: code,
requestedScopes: [TokenScope.Credentiallink],
});
To target a specific credential, pass credentialId at call time or when initializing the hook:
// Option 1: pass credentialId at call time (preferred for dynamic selection)
const { sendOtp, verifyOtp } = useStepUpAuthentication();
await sendOtp({ credentialId: 'specific-email-credential-id' });
// Option 2: pass credentialId when initializing the hook
const { sendOtp, verifyOtp } = useStepUpAuthentication({
credentialId: 'specific-email-credential-id',
});
Wallet signature (external wallets only)
Wallet-based step-up verification is only available for external wallets. Embedded wallets cannot be used for step-up authentication.
const { verifyWallet } = useStepUpAuthentication();
// Pass walletId at call time — defaults to the first connected wallet when omitted
await verifyWallet({
walletId: walletCredentialId,
requestedScopes: [TokenScope.Walletexport],
});
Passkey MFA
const { verifyPasskeyMfa } = useStepUpAuthentication();
// Triggers the browser's passkey prompt
await verifyPasskeyMfa({ requestedScopes: [TokenScope.Walletexport] });
TOTP MFA
const { verifyTotpMfa } = useStepUpAuthentication();
await verifyTotpMfa({
code: totpCode,
requestedScopes: [TokenScope.Walletexport],
});
Social (OAuth)
import { ProviderEnum } from '@dynamic-labs/sdk-api-core';
const { verifySocial } = useStepUpAuthentication();
// Opens a popup for the user to re-authenticate with their linked social account
await verifySocial({
provider: ProviderEnum.Google,
requestedScopes: [TokenScope.Credentiallink],
});
The user must have the social account already linked. The SDK opens a popup to the OAuth provider — no redirect is needed. If the browser blocks the popup (e.g., on mobile), the SDK falls back to redirect and resumes the flow automatically on return.
Recovery code
const { verifyRecoveryCode } = useStepUpAuthentication();
await verifyRecoveryCode({
code: recoveryCode,
requestedScopes: [TokenScope.Credentialunlink],
});
Error handling
All methods update the shared state object and throw on failure:
const { verifyPasskeyMfa, state, resetState } = useStepUpAuthentication();
try {
await verifyPasskeyMfa({ requestedScopes: [TokenScope.Walletexport] });
} catch (error) {
// state.error contains the error message
// Call resetState() before retrying
}
// Render loading and error states
{state.isLoading && <Spinner />}
{state.error && (
<div>
<p>{state.error}</p>
<button onClick={resetState}>Try again</button>
</div>
)}
Full example: headless credential linking
import { FC, useState } from 'react';
import { useStepUpAuthentication } from '@dynamic-labs/sdk-react-core';
import { TokenScope } from '@dynamic-labs/sdk-api-core';
const LinkEmailButton: FC = () => {
const { isStepUpRequired, sendOtp, verifyOtp, state, resetState } =
useStepUpAuthentication();
const [step, setStep] = useState<'idle' | 'otp'>('idle');
const [code, setCode] = useState('');
const handleStart = async () => {
resetState();
if (!(await isStepUpRequired({ scope: TokenScope.Credentiallink }))) {
// Step-up not needed — proceed directly to link
await linkEmail();
return;
}
await sendOtp();
setStep('otp');
};
const handleVerify = async () => {
await verifyOtp({
verificationToken: code,
requestedScopes: [TokenScope.Credentiallink],
});
// Token stored — proceed with linking
await linkEmail();
setStep('idle');
};
if (step === 'otp') {
return (
<div>
<input
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Enter verification code"
/>
<button onClick={handleVerify} disabled={state.isLoading}>
{state.isLoading ? 'Verifying...' : 'Verify'}
</button>
{state.error && <p>{state.error}</p>}
</div>
);
}
return <button onClick={handleStart}>Link Email</button>;
};
Hook reference
See the full useStepUpAuthentication reference for all return values and parameter types.
External auth (Bring Your Own Auth)
If you use external auth (BYOA), your backend can issue elevated access tokens directly using the useExternalAuth hook — no user interaction required. See the External Auth Step-Up guide.