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.
Generate a session key pair
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.
Send and verify the code
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.
What you must persist
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.
Next step
Hand the JWT and the session private key to the wallet client: Use Agent Wallets.
API reference