The JavaScript SDK gives you full control over step-up authentication without any UI framework. Use this for vanilla JS, Vue, Svelte, Angular, or any non-React setup.
For concepts, scopes, and token lifecycle, see Step-up authentication overview.
Prerequisites
@dynamic-labs-sdk/client initialized
- At least one verification method enabled in your dashboard security settings
- Step-up authentication enabled for your environment
Quick start
The pattern is always: check → verify → proceed. After verification, the elevated access token is automatically stored and applied to subsequent Dynamic API calls. You never need to manually handle the token.
import {
checkStepUpAuth,
authenticatePasskeyMFA,
} from '@dynamic-labs-sdk/client';
import { TokenScope } from '@dynamic-labs/sdk-api-core';
const exportPrivateKey = async () => {
// 1. Check if step-up is required and get available credentials
const { isRequired, credentials } = await checkStepUpAuth({
scope: TokenScope.Walletexport,
});
if (isRequired) {
// 2. Verify — the token is stored and applied automatically
await authenticatePasskeyMFA({
requestedScopes: [TokenScope.Walletexport],
});
}
// 3. Proceed — the SDK attaches the token to the relevant API call
await performExport();
};
Checking step-up requirements (recommended)
checkStepUpAuth is the recommended way to determine whether step-up is required. It performs a fast local check first (if a valid elevated token already exists, it returns immediately without an API call), then falls back to a server-authoritative check that evaluates your environment configuration, SDK version, MFA settings, and action-based MFA requirements. On failure, it defaults to { isRequired: true } for safety.
import { checkStepUpAuth } from '@dynamic-labs-sdk/client';
import { TokenScope } from '@dynamic-labs/sdk-api-core';
const { isRequired, credentials, defaultCredentialId } =
await checkStepUpAuth({ scope: TokenScope.Walletexport });
if (isRequired) {
// credentials contains the user's 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);
console.log('Recommended credential:', defaultCred);
}
The response type:
type StepUpCheckResponse = {
isRequired: boolean;
credentials: StepUpCredential[];
/** The recommended credential to authenticate with.
* For MFA: the user's default device or most recently added.
* For re-auth: the credential used to sign in. */
defaultCredentialId?: string;
};
type StepUpCredential = {
id: string;
format: JwtVerifiedCredentialFormatEnum;
type?: string;
alias?: string;
};
Checking for a valid token (alternative)
checkStepUpAuth (shown above) is the recommended approach. It combines the token check with credential discovery in a single call. Use hasElevatedAccessToken only if you need a simple synchronous local-only check.
Use hasElevatedAccessToken to check if the user already has a non-expired elevated token for a scope:
import { hasElevatedAccessToken } from '@dynamic-labs-sdk/client';
import { TokenScope } from '@dynamic-labs/sdk-api-core';
if (hasElevatedAccessToken(TokenScope.Walletexport)) {
// Token exists and is valid — proceed with the operation
} else {
// No valid token — trigger verification
}
Verification methods
Pass requestedScopes to any verification method. After success, the elevated token is automatically stored and applied to Dynamic API calls.
import { sendEmailOTP, verifyOTP } from '@dynamic-labs-sdk/client';
import { TokenScope } from '@dynamic-labs/sdk-api-core';
// 1. Send the OTP to the user's verified email
await sendEmailOTP();
// 2. Verify with the code the user entered
await verifyOTP({
type: 'email',
token: otpCode,
requestedScopes: [TokenScope.Credentiallink],
});
// Token is now stored and will be applied automatically
import { sendSmsOTP, verifyOTP } from '@dynamic-labs-sdk/client';
import { TokenScope } from '@dynamic-labs/sdk-api-core';
await sendSmsOTP({ countryCode: 'US', phoneNumber: '5551234567' });
await verifyOTP({
type: 'sms',
token: otpCode,
requestedScopes: [TokenScope.Credentiallink],
});
Wallet-based step-up verification is only available for external wallets. Embedded wallets cannot be used for step-up authentication.import { verifyWallet } from '@dynamic-labs-sdk/client';
import { TokenScope } from '@dynamic-labs/sdk-api-core';
// walletId is optional — defaults to the first connected wallet when omitted
await verifyWallet({
walletId: 'wallet-id',
requestedScopes: [TokenScope.Walletexport],
});
import { verifySocial } from '@dynamic-labs-sdk/client';
import { ProviderEnum, TokenScope } from '@dynamic-labs/sdk-api-core';
// Opens a popup for the user to re-authenticate with their linked social account
await verifySocial({
provider: ProviderEnum.Google,
requestedScopes: [TokenScope.Credentiallink],
});
import { authenticatePasskeyMFA } from '@dynamic-labs-sdk/client';
import { TokenScope } from '@dynamic-labs/sdk-api-core';
// Browser prompts for passkey (biometric, PIN, or security key)
await authenticatePasskeyMFA({
requestedScopes: [TokenScope.Walletexport],
});
import { authenticateTotpMfaDevice } from '@dynamic-labs-sdk/client';
import { TokenScope } from '@dynamic-labs/sdk-api-core';
await authenticateTotpMfaDevice({
code: totpCode,
requestedScopes: [TokenScope.Walletexport],
});
import { authenticateMfaRecoveryCode } from '@dynamic-labs-sdk/client';
import { TokenScope } from '@dynamic-labs/sdk-api-core';
await authenticateMfaRecoveryCode({
code: recoveryCode,
requestedScopes: [TokenScope.Credentialunlink],
});
Choosing the right method
The JavaScript SDK requires you to choose which verification method to present. checkStepUpAuth returns all available credentials — both MFA (passkey, TOTP) and re-auth (email, SMS, OAuth, wallet) — so you can route the user to the right method in a single call:
import { checkStepUpAuth } from '@dynamic-labs-sdk/client';
import {
JwtVerifiedCredentialFormatEnum,
TokenScope,
} from '@dynamic-labs/sdk-api-core';
const chooseMethod = async () => {
const { isRequired, credentials, defaultCredentialId } =
await checkStepUpAuth({ scope: TokenScope.Walletexport });
if (!isRequired) return null; // No step-up needed
// Use the default credential if available, otherwise pick the first
const credential =
credentials.find((c) => c.id === defaultCredentialId) ?? credentials[0];
switch (credential?.format) {
case JwtVerifiedCredentialFormatEnum.Passkey:
return 'passkey'; // Use authenticatePasskeyMFA
case JwtVerifiedCredentialFormatEnum.Totp:
return 'totp'; // Use authenticateTotpMfaDevice (collect code first)
case JwtVerifiedCredentialFormatEnum.Email:
return 'email'; // Use sendEmailOTP + verifyOTP
case JwtVerifiedCredentialFormatEnum.PhoneNumber:
return 'phone'; // Use sendSmsOTP + verifyOTP
case JwtVerifiedCredentialFormatEnum.Oauth:
return 'oauth'; // Use verifySocial
case JwtVerifiedCredentialFormatEnum.Blockchain:
return 'wallet'; // Use verifyWallet
default:
return credential?.format;
}
};
When a user has MFA methods registered, the backend requires MFA for elevated token issuance — re-auth credentials won’t be returned. checkStepUpAuth handles this automatically, so you don’t need separate logic for MFA vs. re-auth users.
For React users, the useStepUpAuthentication hook provides a promptStepUpAuth method that handles all of this routing automatically. See the Step-up authentication overview for concepts.
Full example: wallet export with method selection
import {
checkStepUpAuth,
authenticatePasskeyMFA,
authenticateTotpMfaDevice,
sendEmailOTP,
verifyOTP,
} from '@dynamic-labs-sdk/client';
import {
JwtVerifiedCredentialFormatEnum,
TokenScope,
} from '@dynamic-labs/sdk-api-core';
const SCOPE = TokenScope.Walletexport;
/**
* Step-up verification with automatic method selection.
* Uses checkStepUpAuth to get available credentials and the
* recommended default credential to authenticate with.
*/
const stepUp = async () => {
const { isRequired, credentials, defaultCredentialId } =
await checkStepUpAuth({ scope: SCOPE });
if (!isRequired) return { done: true };
// Use the recommended default credential, or fall back to the first
const credential =
credentials.find((c) => c.id === defaultCredentialId) ?? credentials[0];
if (!credential) return { done: false, needs: 'unknown' };
// MFA credentials
if (credential.format === JwtVerifiedCredentialFormatEnum.Totp) {
return { done: false, needs: 'totp', deviceId: credential.id };
}
if (credential.format === JwtVerifiedCredentialFormatEnum.Passkey) {
await authenticatePasskeyMFA({ requestedScopes: [SCOPE] });
return { done: true };
}
// Re-auth credentials (email, SMS, wallet, social)
if (credential.format === JwtVerifiedCredentialFormatEnum.Email) {
await sendEmailOTP();
return { done: false, needs: 'emailOtp' };
}
return { done: false, needs: credential.format };
};
/** Call after collecting the code from the user. */
const completeStepUp = async (code, method, deviceId) => {
if (method === 'totp') {
await authenticateTotpMfaDevice({
code,
deviceId,
requestedScopes: [SCOPE],
});
} else {
await verifyOTP({ type: 'email', token: code, requestedScopes: [SCOPE] });
}
};
Error handling
Verification methods throw when the user cancels or verification fails:
import { authenticatePasskeyMFA } from '@dynamic-labs-sdk/client';
import { TokenScope } from '@dynamic-labs/sdk-api-core';
try {
await authenticatePasskeyMFA({ requestedScopes: [TokenScope.Walletexport] });
} catch (error) {
// User cancelled the passkey prompt, or verification failed
console.error('Step-up failed:', error.message);
}
Common error scenarios:
- User cancels the browser passkey prompt
- Invalid OTP code entered
- MFA device verification timeout
- Network error during verification
Available scopes
See the complete scopes reference for all supported values.
External auth (Bring Your Own Auth)
If you use external auth (BYOA), your backend can issue elevated access tokens directly — no user interaction required. See the External Auth Step-Up guide.