This guide is only relevant if you previously used action-based MFA to protect sensitive wallet operations. If you didn’t, you can skip it.
What changed
| Action-based MFA (before) | Step-up authentication (after) | |
|---|---|---|
| Hook | useMfa / useAuthenticatePasskeyMFA | useStepUpAuthentication |
| Token type | Unscoped MFA token (createMfaToken) | Scoped elevated access token (requestedScopes) |
| Token handling | SDK-managed MFA token | SDK-managed elevated access token (scoped JWT) |
| Who can verify | Only users with MFA methods (TOTP/Passkey) | All users — MFA methods, email OTP, SMS OTP, wallet signature, or social OAuth |
| Scope | None — token accepted for any action | Bound to specific scopes (wallet:export, wallet:sign, credential:link, credential:unlink) |
| Expiration | Single-use, no time limit | Time-limited (5 min single-use, 10 min multi-use) |
Dashboard changes
No dashboard changes are required — your existing action-based MFA event configuration (WaaS Export, WaaS Refresh, WaaS Sign, WaaS Reshare) carries over.Make sure your code is fully updated before accepting the minimum API version at Dashboard > Developers > API & SDK Keys. Once set to
2026_04_01, the backend enforces step-up authentication immediately.AI-assisted upgrade prompts
These prompts are designed for AI coding agents (Cursor, Claude Code, Copilot). Always review generated code before committing — agents can misread your project structure.Install the Dynamic MCP below so your agent can reference live documentation automatically. If you don’t have it installed, the prompts fall back to
https://www.dynamic.xyz/docs/llms.txt.- Cursor
- Claude Code
- Codex
- Other
claude mcp add --transport http dynamic https://www.dynamic.xyz/docs/mcp
codex mcp add dynamic --url https://www.dynamic.xyz/docs/mcp
If your editor or agent supports remote HTTP MCP, add a server that points at
https://www.dynamic.xyz/docs/mcp. The exact steps depend on the product; use its MCP documentation for URL-based or HTTP transport setup.- React
- JavaScript
- React Native
- Flutter
- Kotlin
- Swift
I'm migrating my Dynamic SDK headless integration from action-based MFA to step-up authentication as part of the 2026_04_01 API upgrade.
If you have the Dynamic MCP installed, use it to reference the latest API.
Otherwise reference: https://www.dynamic.xyz/docs/llms.txt
In my codebase:
1. Find all usages of `useMfa`, `useAuthenticatePasskeyMFA`, and `usePromptMfaAuth`
2. Replace them with `useStepUpAuthentication` from `@dynamic-labs/sdk-react-core`
3. Replace all `createMfaToken` options with `requestedScopes` using the
appropriate `TokenScope` from `@dynamic-labs/sdk-api-core`
4. Replace all `useIsMfaRequiredForAction` calls with `isStepUpRequired`
5. Do not modify any files outside of the auth flow
After making changes, summarize what was updated and flag anything that
needs manual review.
I'm migrating my Dynamic SDK headless integration from action-based MFA
to step-up authentication as part of the 2026_04_01 API upgrade.
If you have the Dynamic MCP installed, use it to reference the latest API.
Otherwise reference: https://www.dynamic.xyz/docs/llms.txt
In my codebase:
1. Find all usages of `isMfaRequiredForAction` from `@dynamic-labs-sdk/client`
2. Replace them with `checkStepUpAuth`
3. Replace all `createMfaToken` and `createMfaTokenOptions` with `requestedScopes`
using the appropriate `TokenScope` from `@dynamic-labs/sdk-api-core`
on both `authenticateTotpMfaDevice` and `authenticatePasskeyMFA`
4. Do not modify any files outside of the auth flow
After making changes, summarize what was updated and flag anything that
needs manual review.
I'm migrating my Dynamic SDK headless integration from action-based MFA
to step-up authentication as part of the 2026_04_01 API upgrade.
If you have the Dynamic MCP installed, use it to reference the latest API.
Otherwise reference: https://www.dynamic.xyz/docs/llms.txt
In my codebase:
1. Find all usages of `client.mfa.isRequiredForAction` and replace with
`client.stepUpAuth.isStepUpRequired`
2. Find all usages of `client.mfa.authenticateDevice` and replace with
`client.stepUpAuth.verifyTotpMfa`
3. Find all usages of `client.passkeys.authenticateMFA` and replace with
`client.stepUpAuth.verifyPasskeyMfa`
4. Find all usages of `client.ui.mfa.show` and replace with
`client.stepUpAuth.promptStepUpAuth`
5. Replace all `createMfaToken` options with `requestedScopes` using
the appropriate `TokenScope`
6. Do not modify any files outside of the auth flow
After making changes, summarize what was updated and flag anything that
needs manual review.
I'm migrating my Dynamic SDK headless integration from action-based MFA
to step-up authentication as part of the 2026_04_01 API upgrade.
If you have the Dynamic MCP installed, use it to reference the latest API.
Otherwise reference: https://www.dynamic.xyz/docs/llms.txt
In my codebase:
1. Find all usages of DynamicSDK.instance.mfa related to action-based MFA checks
2. Replace them with DynamicSDK.instance.stepUpAuth.isStepUpRequired
3. Replace verification calls with the appropriate method:
- TOTP: DynamicSDK.instance.stepUpAuth.verifyTotpMfa
- Passkey: DynamicSDK.instance.stepUpAuth.verifyPasskeyMfa
- Prompt: DynamicSDK.instance.stepUpAuth.promptStepUpAuth
4. Replace all scope arguments with the string equivalents:
'wallet:export', 'wallet:sign', 'credential:link', 'credential:unlink'
5. Do not modify any files outside of the auth flow
After making changes, summarize what was updated and flag anything that
needs manual review.
I'm migrating my Dynamic SDK headless integration from action-based MFA
to step-up authentication as part of the 2026_04_01 API upgrade.
If you have the Dynamic MCP installed, use it to reference the latest API.
Otherwise reference: https://www.dynamic.xyz/docs/llms.txt
In my codebase:
1. Find all usages of action-based MFA checks in my Kotlin integration
2. Replace them with sdk.stepUpAuth.isStepUpRequired(scope)
3. Replace verification calls with the appropriate method:
- TOTP: sdk.stepUpAuth.verifyTotpMfa(code, deviceId, requestedScopes)
- Passkey: sdk.stepUpAuth.verifyPasskeyMfa(requestedScopes)
- OTP: sdk.stepUpAuth.verifyOtp(verificationToken, requestedScopes)
- Or let the SDK decide: sdk.stepUpAuth.promptStepUpAuth(requestedScopes)
4. Replace all scope arguments with the appropriate string equivalents:
'wallet:export', 'wallet:sign', 'credential:link', 'credential:unlink'
5. Do not modify any files outside of the auth flow
After making changes, summarize what was updated and flag anything that
needs manual review.
I'm migrating my Dynamic SDK headless integration from action-based MFA
to step-up authentication as part of the 2026_04_01 API upgrade.
If you have the Dynamic MCP installed, use it to reference the latest API.
Otherwise reference: https://www.dynamic.xyz/docs/llms.txt
In my codebase:
1. Find all usages of action-based MFA checks in my Swift integration
2. Replace them with sdk.stepUpAuth.isStepUpRequired(scope:)
3. Replace verification calls with the appropriate method:
- TOTP: sdk.stepUpAuth.verifyTotpMfa(code:deviceId:requestedScopes:)
- Passkey: sdk.stepUpAuth.verifyPasskeyMfa(requestedScopes:)
- OTP: sdk.stepUpAuth.verifyOtp(verificationToken:requestedScopes:)
- Or let the SDK decide: sdk.stepUpAuth.promptStepUpAuth(requestedScopes:)
4. Replace all scope arguments with the appropriate string equivalents:
'wallet:export', 'wallet:sign', 'credential:link', 'credential:unlink'
5. Do not modify any files outside of the auth flow
After making changes, summarize what was updated and flag anything that
needs manual review.
Code migration: before and after
The After examples include an
else branch that falls through to legacy action-based MFA. It is only needed if some of your users already have legacy MFA enabled and you’re deploying the updated SDK before the 2026_04_01 API version is confirmed. Once both the SDK and the minimum API version are upgraded, the transitional else branches can be removed since they are no longer needed.Using Dynamic’s built-in UI
- React
- React Native
- Flutter (no built-in UI)
Before — After —
usePromptMfaAuth with createMfaToken: import { usePromptMfaAuth, useIsMfaRequiredForAction } from "@dynamic-labs/sdk-react-core";
import { MFAAction } from '@dynamic-labs/sdk-api-core';
const isMfaRequiredForAction = useIsMfaRequiredForAction();
const promptMfaAuth = usePromptMfaAuth();
const isMfaRequired = await isMfaRequiredForAction({
mfaAction: MFAAction.WalletWaasExport,
});
if (isMfaRequired) {
await promptMfaAuth({ createMfaToken: true });
}
await primaryWallet.exportWaasPrivateKey();
useStepUpAuthentication with requestedScopes. The else branch is only needed if some of your users already have legacy MFA enabled — it handles the window when the frontend is deployed before the 2026_04_01 API version is confirmed. Remove it once both the SDK and the minimum API version are upgraded: import {
useStepUpAuthentication,
usePromptMfaAuth,
useIsMfaRequiredForAction,
} from '@dynamic-labs/sdk-react-core';
import { TokenScope, MFAAction } from '@dynamic-labs/sdk-api-core';
const { isStepUpRequired, promptStepUpAuth } = useStepUpAuthentication();
const promptMfaAuth = usePromptMfaAuth();
const isMfaRequiredForAction = useIsMfaRequiredForAction();
if (await isStepUpRequired({ scope: TokenScope.Walletexport })) {
await promptStepUpAuth({
requestedScopes: [TokenScope.Walletexport],
});
} else {
// Transitional: 2026_04_01 API version not yet confirmed.
// Legacy MFA prompt for users with an existing passkey / TOTP / device.
const isMfaRequired = await isMfaRequiredForAction({
mfaAction: MFAAction.WalletWaasExport,
});
if (isMfaRequired) {
await promptMfaAuth({ createMfaToken: true });
}
}
// Token is stored — SDK attaches it automatically
await primaryWallet.exportWaasPrivateKey();
Before — After —
client.ui.mfa.show with createMfaToken: import { MFAAction } from '@dynamic-labs/sdk-api-core';
const isMfaRequired = await client.mfa.isRequiredForAction({
mfaAction: MFAAction.WalletWaasExport,
});
if (isMfaRequired) {
await client.ui.mfa.show({ createMfaToken: true });
}
await primaryWallet.exportWaasPrivateKey();
client.stepUpAuth.promptStepUpAuth with requestedScopes. The else branch is only needed if some of your users already have legacy MFA enabled — it handles the window when the frontend is deployed before the 2026_04_01 API version is confirmed. Remove it once both the SDK and the minimum API version are upgraded: import { TokenScope, MFAAction } from '@dynamic-labs/sdk-api-core';
const isRequired = await client.stepUpAuth.isStepUpRequired({
scope: TokenScope.Walletexport,
});
if (isRequired) {
await client.stepUpAuth.promptStepUpAuth({
requestedScopes: [TokenScope.Walletexport],
});
} else {
// Transitional: 2026_04_01 API version not yet confirmed.
// Legacy MFA prompt for users with an existing passkey / TOTP / device.
const mfaRequired = await client.mfa.isRequiredForAction({
mfaAction: MFAAction.WalletWaasExport,
});
if (mfaRequired) {
await client.ui.mfa.show({ createMfaToken: true });
}
}
// Token is stored — SDK attaches it automatically
await primaryWallet.exportWaasPrivateKey();
Flutter did not have a built-in action-based MFA UI. Use
promptStepUpAuth to add step-up verification: import 'package:dynamic_sdk/dynamic_sdk.dart';
final isRequired = await DynamicSDK.instance.stepUpAuth
.isStepUpRequired('wallet:export');
if (isRequired) {
await DynamicSDK.instance.stepUpAuth.promptStepUpAuth(
requestedScopes: ['wallet:export'],
);
}
// Token is stored — SDK attaches it automatically
await performExport();
Headless TOTP
- React
- JavaScript
- React Native
- Flutter
Before — After —
useMfa with authenticateDevice and createMfaToken: import { useMfa, useIsMfaRequiredForAction } from "@dynamic-labs/sdk-react-core";
import { MFAAction } from '@dynamic-labs/sdk-api-core';
const { authenticateDevice } = useMfa();
const isMfaRequiredForAction = useIsMfaRequiredForAction();
const requires = await isMfaRequiredForAction({
mfaAction: MFAAction.WalletWaasExport,
});
if (requires) {
await authenticateDevice({
code: totpCode,
createMfaToken: { singleUse: true },
});
}
useStepUpAuthentication with verifyTotpMfa. The else branch is only needed if some of your users already have legacy MFA enabled — it handles the window before the 2026_04_01 API version is confirmed. Remove it once both the SDK and the minimum API version are upgraded: import {
useStepUpAuthentication,
useMfa,
useIsMfaRequiredForAction,
} from '@dynamic-labs/sdk-react-core';
import { TokenScope, MFAAction } from '@dynamic-labs/sdk-api-core';
const { isStepUpRequired, verifyTotpMfa } = useStepUpAuthentication();
const { authenticateDevice } = useMfa();
const isMfaRequiredForAction = useIsMfaRequiredForAction();
if (await isStepUpRequired({ scope: TokenScope.Walletexport })) {
await verifyTotpMfa({
code: totpCode,
requestedScopes: [TokenScope.Walletexport],
});
} else {
// Transitional: 2026_04_01 API version not yet confirmed.
// Legacy TOTP verification for users with an existing MFA device.
const requires = await isMfaRequiredForAction({
mfaAction: MFAAction.WalletWaasExport,
});
if (requires) {
await authenticateDevice({
code: totpCode,
createMfaToken: { singleUse: true },
});
}
}
Before — After — same function with
authenticateTotpMfaDevice with createMfaTokenOptions: import { isMfaRequiredForAction, authenticateTotpMfaDevice, MFAAction } from '@dynamic-labs-sdk/client';
const required = await isMfaRequiredForAction({ mfaAction: MFAAction.WalletWaasExport });
if (required) {
await authenticateTotpMfaDevice({
code: '123456',
createMfaTokenOptions: { singleUse: true },
});
}
requestedScopes. The else branch is only needed if some of your users already have legacy MFA enabled — it handles the window before the 2026_04_01 API version is confirmed. Remove it once both the SDK and the minimum API version are upgraded: import {
checkStepUpAuth,
authenticateTotpMfaDevice,
isMfaRequiredForAction,
MFAAction,
} from '@dynamic-labs-sdk/client';
import { TokenScope } from '@dynamic-labs/sdk-api-core';
const { isRequired } = await checkStepUpAuth({ scope: TokenScope.Walletexport });
if (isRequired) {
await authenticateTotpMfaDevice({
code: '123456',
requestedScopes: [TokenScope.Walletexport],
});
} else {
// Transitional: 2026_04_01 API version not yet confirmed.
// Legacy TOTP verification for users with an existing MFA device.
const required = await isMfaRequiredForAction({ mfaAction: MFAAction.WalletWaasExport });
if (required) {
await authenticateTotpMfaDevice({
code: '123456',
createMfaTokenOptions: { singleUse: true },
});
}
}
Before — After —
client.mfa.authenticateDevice with createMfaToken: import { MFAAction } from '@dynamic-labs/sdk-api-core';
const requires = await client.mfa.isRequiredForAction({
mfaAction: MFAAction.WalletWaasExport,
});
if (requires) {
await client.mfa.authenticateDevice({
code: totpCode,
createMfaToken: { singleUse: true },
});
}
client.stepUpAuth.verifyTotpMfa with requestedScopes. The else branch is only needed if some of your users already have legacy MFA enabled — it handles the window before the 2026_04_01 API version is confirmed. Remove it once both the SDK and the minimum API version are upgraded: import { TokenScope, MFAAction } from '@dynamic-labs/sdk-api-core';
const isRequired = await client.stepUpAuth.isStepUpRequired({
scope: TokenScope.Walletexport,
});
if (isRequired) {
await client.stepUpAuth.verifyTotpMfa({
code: totpCode,
requestedScopes: [TokenScope.Walletexport],
});
} else {
// Transitional: 2026_04_01 API version not yet confirmed.
// Legacy TOTP verification for users with an existing MFA device.
const requires = await client.mfa.isRequiredForAction({
mfaAction: MFAAction.WalletWaasExport,
});
if (requires) {
await client.mfa.authenticateDevice({
code: totpCode,
createMfaToken: { singleUse: true },
});
}
}
import 'package:dynamic_sdk/dynamic_sdk.dart';
final isRequired = await DynamicSDK.instance.stepUpAuth
.isStepUpRequired('wallet:export');
if (isRequired) {
await DynamicSDK.instance.stepUpAuth.verifyTotpMfa(
code: totpCode,
requestedScopes: ['wallet:export'],
);
}
Headless Passkey
- React
- JavaScript
- React Native
- Flutter
Before — After —
useAuthenticatePasskeyMFA with createMfaToken: import {
useAuthenticatePasskeyMFA,
useGetPasskeys,
useIsMfaRequiredForAction,
useRegisterPasskey,
} from "@dynamic-labs/sdk-react-core";
import { MFAAction } from '@dynamic-labs/sdk-api-core';
const authenticatePasskeyMFA = useAuthenticatePasskeyMFA();
const getPasskeys = useGetPasskeys();
const registerPasskey = useRegisterPasskey();
const isMfaRequiredForAction = useIsMfaRequiredForAction();
const requires = await isMfaRequiredForAction({
mfaAction: MFAAction.WalletWaasExport,
});
if (requires) {
const passkeys = await getPasskeys();
if (passkeys.length === 0) {
await registerPasskey();
}
await authenticatePasskeyMFA({
createMfaToken: { singleUse: false },
});
}
useStepUpAuthentication with verifyPasskeyMfa. The else branch is only needed if some of your users already have legacy MFA enabled — it handles the window before the 2026_04_01 API version is confirmed. Remove it once both the SDK and the minimum API version are upgraded: import {
useStepUpAuthentication,
useAuthenticatePasskeyMFA,
useGetPasskeys,
useRegisterPasskey,
useIsMfaRequiredForAction,
} from '@dynamic-labs/sdk-react-core';
import { TokenScope, MFAAction } from '@dynamic-labs/sdk-api-core';
const { isStepUpRequired, verifyPasskeyMfa } = useStepUpAuthentication();
const authenticatePasskeyMFA = useAuthenticatePasskeyMFA();
const getPasskeys = useGetPasskeys();
const registerPasskey = useRegisterPasskey();
const isMfaRequiredForAction = useIsMfaRequiredForAction();
if (await isStepUpRequired({ scope: TokenScope.Walletexport })) {
await verifyPasskeyMfa({
requestedScopes: [TokenScope.Walletexport],
});
} else {
// Transitional: 2026_04_01 API version not yet confirmed.
// Legacy passkey MFA for users with an existing registered passkey.
const requires = await isMfaRequiredForAction({
mfaAction: MFAAction.WalletWaasExport,
});
if (requires) {
const passkeys = await getPasskeys();
if (passkeys.length === 0) {
await registerPasskey();
}
await authenticatePasskeyMFA({
createMfaToken: { singleUse: false },
});
}
}
Before — After — same function with
authenticatePasskeyMFA with createMfaToken: import { isMfaRequiredForAction, authenticatePasskeyMFA, MFAAction } from '@dynamic-labs-sdk/client';
const required = await isMfaRequiredForAction({ mfaAction: MFAAction.WalletWaasExport });
if (required) {
await authenticatePasskeyMFA({
createMfaToken: { singleUse: true },
});
}
requestedScopes. The else branch is only needed if some of your users already have legacy MFA enabled — it handles the window before the 2026_04_01 API version is confirmed. Remove it once both the SDK and the minimum API version are upgraded: import {
checkStepUpAuth,
authenticatePasskeyMFA,
isMfaRequiredForAction,
MFAAction,
} from '@dynamic-labs-sdk/client';
import { TokenScope } from '@dynamic-labs/sdk-api-core';
const { isRequired } = await checkStepUpAuth({ scope: TokenScope.Walletexport });
if (isRequired) {
await authenticatePasskeyMFA({
requestedScopes: [TokenScope.Walletexport],
});
} else {
// Transitional: 2026_04_01 API version not yet confirmed.
// Legacy passkey MFA for users with an existing registered passkey.
const required = await isMfaRequiredForAction({ mfaAction: MFAAction.WalletWaasExport });
if (required) {
await authenticatePasskeyMFA({
createMfaToken: { singleUse: true },
});
}
}
Before — After —
client.passkeys.authenticateMFA with createMfaToken: import { MFAAction } from '@dynamic-labs/sdk-api-core';
const requires = await client.mfa.isRequiredForAction({
mfaAction: MFAAction.WalletWaasExport,
});
if (requires) {
const passkeys = await client.passkeys.get();
if (passkeys.length === 0) {
await client.passkeys.register();
}
await client.passkeys.authenticateMFA({
createMfaToken: { singleUse: false },
});
}
client.stepUpAuth.verifyPasskeyMfa with requestedScopes. The else branch is only needed if some of your users already have legacy MFA enabled — it handles the window before the 2026_04_01 API version is confirmed. Remove it once both the SDK and the minimum API version are upgraded: import { TokenScope, MFAAction } from '@dynamic-labs/sdk-api-core';
const isRequired = await client.stepUpAuth.isStepUpRequired({
scope: TokenScope.Walletexport,
});
if (isRequired) {
await client.stepUpAuth.verifyPasskeyMfa({
requestedScopes: [TokenScope.Walletexport],
});
} else {
// Transitional: 2026_04_01 API version not yet confirmed.
// Legacy passkey MFA for users with an existing registered passkey.
const requires = await client.mfa.isRequiredForAction({
mfaAction: MFAAction.WalletWaasExport,
});
if (requires) {
const passkeys = await client.passkeys.get();
if (passkeys.length === 0) {
await client.passkeys.register();
}
await client.passkeys.authenticateMFA({
createMfaToken: { singleUse: false },
});
}
}
import 'package:dynamic_sdk/dynamic_sdk.dart';
final isRequired = await DynamicSDK.instance.stepUpAuth
.isStepUpRequired('wallet:export');
if (isRequired) {
await DynamicSDK.instance.stepUpAuth.verifyPasskeyMfa(
requestedScopes: ['wallet:export'],
);
}
Migration checklist
- React
- JavaScript
- React Native
- Flutter
- Upgrade to React SDK
4.76.0or later - Replace
useMfa/useAuthenticatePasskeyMFA/usePromptMfaAuthwithuseStepUpAuthentication - Replace
createMfaTokenwithrequestedScopesusing the appropriateTokenScope - Replace
useIsMfaRequiredForActionwithisStepUpRequired - If you have existing legacy MFA users and you’re deploying the SDK update before accepting the new API version, keep the transitional
elsebranch shown in each After example so those users still get prompted - Test the step-up flow end-to-end for each protected action
- Accept the minimum API version
2026_04_01in your dashboard - Remove the transitional
elsebranches
- Upgrade to JavaScript SDK
0.24.1or later - Replace
isMfaRequiredForActionwithcheckStepUpAuth - Replace
createMfaToken/createMfaTokenOptionswithrequestedScopesonauthenticateTotpMfaDeviceandauthenticatePasskeyMFA - If you have existing legacy MFA users and you’re deploying the SDK update before accepting the new API version, keep the transitional
elsebranch shown in each After example so those users still get prompted - Test the step-up flow end-to-end for each protected action
- Accept the minimum API version
2026_04_01in your dashboard - Remove the transitional
elsebranches
- Upgrade to React Native SDK
4.76.0or later - Replace
client.mfa.isRequiredForActionwithclient.stepUpAuth.isStepUpRequired - Replace
client.mfa.authenticateDevicewithclient.stepUpAuth.verifyTotpMfa - Replace
client.passkeys.authenticateMFAwithclient.stepUpAuth.verifyPasskeyMfa - Replace
client.ui.mfa.showwithclient.stepUpAuth.promptStepUpAuth - If you have existing legacy MFA users and you’re deploying the SDK update before accepting the new API version, keep the transitional
elsebranch shown in each After example so those users still get prompted - Test the step-up flow end-to-end for each protected action
- Accept the minimum API version
2026_04_01in your dashboard - Remove the transitional
elsebranches
- Upgrade to Flutter SDK
1.2.10or later - Implement step-up checks with
DynamicSDK.instance.stepUpAuth.isStepUpRequired - Add verification flows using
promptStepUpAuth,verifyTotpMfa, orverifyPasskeyMfa - Test the step-up flow end-to-end for each protected action
- Accept the minimum API version
2026_04_01in your dashboard