registerTotpMfaDevice and deleteMfaDevice (below) both require an elevated access token — TokenScope.Credentiallink and TokenScope.Credentialunlink respectively — with no fallback. Calling either directly returns a 403 (Elevated access token required) unless the user already holds one.Check first with checkStepUpAuth({ scope }). If it’s required, re-verify the user with an existing credential (email OTP shown here) and pass the scope in requestedScopes — the resulting elevated token is picked up automatically by the gated call that follows.
TypeScript
import { checkStepUpAuth, sendEmailOTP, verifyOTP, TokenScope,} from '@dynamic-labs-sdk/client';async function ensureElevatedAccess(scope: TokenScope) { const { isRequired } = await checkStepUpAuth({ scope }); if (!isRequired) return; // Re-verify with an existing credential — email OTP shown here. const otpVerification = await sendEmailOTP({ email }); const code = await promptForCode(); // your implementation await verifyOTP({ otpVerification, verificationToken: code, requestedScopes: [scope], });}
registerTotpMfaDevice returns { id, secret, uri }. Render uri as a QR code (with your preferred QR library) and show secret as a manual-entry fallback. The user scans it, then enters the 6-digit code so you can verify with authenticateTotpMfaDevice.
TypeScript
React
import { registerTotpMfaDevice, authenticateTotpMfaDevice, TokenScope,} from '@dynamic-labs-sdk/client';async function enroll() { await ensureElevatedAccess(TokenScope.Credentiallink); const { id, secret, uri } = await registerTotpMfaDevice(); // renderQrCode / showSecret are your implementation. renderQrCode(uri); showSecret(secret); // promptForCode is your implementation — the 6-digit code from the app. const code = await promptForCode(); try { await authenticateTotpMfaDevice({ code, deviceId: id }); // showEnrolled is your implementation. showEnrolled(); } catch (error) { // handleMfaError is your implementation — see the error table below. handleMfaError(error); }}
import { useState } from 'react';import { TokenScope } from '@dynamic-labs-sdk/client';import { useRegisterTotpMfaDevice, useAuthenticateTotpMfaDevice,} from '@dynamic-labs-sdk/react-hooks';function EnrollTotp() { const { mutateAsync: register } = useRegisterTotpMfaDevice(); const { mutateAsync: verify, isPending } = useAuthenticateTotpMfaDevice(); const [device, setDevice] = useState<{ id: string; uri: string } | null>(null); const startEnrollment = async () => { // ensureElevatedAccess is the helper defined above. await ensureElevatedAccess(TokenScope.Credentiallink); setDevice(await register()); }; if (!device) { return ( <button type="button" onClick={startEnrollment}> Set up authenticator app </button> ); } return ( <form onSubmit={async (event) => { event.preventDefault(); const code = String(new FormData(event.currentTarget).get('code')); await verify({ code, deviceId: device.id }); }} > {/* Render device.uri as a QR code with your QR library. */} <input name="code" inputMode="numeric" autoComplete="one-time-code" /> <button type="submit" disabled={isPending}>Verify</button> </form> );}
After enrolling, show the user their recovery codes once and record that they saw them with acknowledgeRecoveryCodes.
TypeScript
React
import { getMfaRecoveryCodes, acknowledgeRecoveryCodes,} from '@dynamic-labs-sdk/client';async function showRecoveryCodes() { const { recoveryCodes } = await getMfaRecoveryCodes(); // displayCodes is your implementation — show them once, let the user save them. await displayCodes(recoveryCodes); await acknowledgeRecoveryCodes();}
List enrolled devices, set a default, and remove one.
Removal needs the Credentialunlink elevated access token from Before you start: elevated access tokens. A plain authenticateTotpMfaDevice({ code, deviceId }) re-verify — with no requestedScopes — does not produce a token deleteMfaDevice will accept, even though its mfaAuthToken parameter is optional. Pass requestedScopes: [TokenScope.Credentialunlink] on the re-verification call instead; deleteMfaDevice then picks up the resulting elevated token automatically.
TypeScript
React
import { getMfaDevices, setDefaultMfaDevice, authenticateTotpMfaDevice, deleteMfaDevice, TokenScope,} from '@dynamic-labs-sdk/client';async function listDevices(list: HTMLUListElement) { const devices = await getMfaDevices(); list.replaceChildren(); for (const device of devices) { const item = document.createElement('li'); item.textContent = `${device.type}${device._default ? ' (default)' : ''}`; list.append(item); }}async function makeDefault(deviceId: string) { await setDefaultMfaDevice({ deviceId });}async function remove(deviceId: string, code: string) { // Re-verifying with the Credentialunlink scope grants the elevated // token deleteMfaDevice() needs — it's picked up automatically. await authenticateTotpMfaDevice({ code, deviceId, requestedScopes: [TokenScope.Credentialunlink], }); await deleteMfaDevice({ deviceId });}