Mint a Dynamic user JWT for your agent with a one-time email code, binding the session key at sign-in.
Use email OTP when a human user is in the loop: the agent requests a one-time code, the user receives it by email and relays it to the agent, and the agent exchanges it for a Dynamic user JWT. For sign-in without a human, see Sign In with a Private Key; to sign in with JWTs from your own auth provider, see Sign In with an External JWT.
import { generateSessionKeyPair } from '@dynamic-labs-wallet/node';const { publicKeyHex, privateKeyJwk } = await generateSessionKeyPair();// Store privateKeyJwk in your secrets vault. The SDK does not persist it.
generateSessionKeyPair returns a P-256 key pair: publicKeyHex (33-byte compressed public key, lowercase hex) and privateKeyJwk (the private key as a JWK). The SDK stores nothing — you persist the JWK in your secrets vault.
The returned JWK is extractable so it can be handed back to you — it is a convenience, not a security boundary. For non-extractable custody, skip generateSessionKeyPair: generate a P-256 key in your KMS, HSM, or enclave, pass its compressed public key hex at sign-in, and back getSessionSignature with the external signer.
createAuthClient performs Dynamic sign-in flows headlessly — the server-side counterpart to the JS client SDK’s sign-in. It holds configuration only: no secrets, no persistence.
Pass sessionPublicKey at sign-in. This binds the session key into the minted JWT and is what makes the signed-session proof verifiable. If you omit it, wallet creation and signing with backUpToDynamic: true fail at the backup and recovery calls. This is the most commonly missed step.
import { createAuthClient } from '@dynamic-labs-wallet/node';const auth = createAuthClient({ environmentId: process.env.DYNAMIC_ENVIRONMENT_ID! });const { verificationUUID } = await auth.email.sendOtp('user@example.com');// The user receives the code by email. Collect it through your own channel.const { jwt, expiresAt } = await auth.email.verifyOtp({ verificationUUID, code: otpCodeFromUser, sessionPublicKey: publicKeyHex,});
Sign-in resolves to { jwt, expiresAt, userId }. Sign-in fails with a descriptive error when the code is wrong or expired, when the account requires MFA (the headless client does not support MFA), or when the environment uses cookie-based auth (the response carries no JWT in the body).OTP requests are rate limited per IP address — see Rate Limit Policies — so agents must not retry sendOtp in a loop. If the code doesn’t arrive, see Email OTP delivery.
The SDK is stateless by design — your store is the source of truth. After sign-in you hold two artifacts to keep safe: the session private key (secrets vault or KMS) and the JWT (update it on every refresh). See Storage & Security for the full persistence matrix.