> ## Documentation Index
> Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Sign In with Email OTP

> 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](/node/agents/sign-in-with-a-private-key).

<Note>
  Before this: read the [Agent Wallets overview](/node/agents/overview) and install `@dynamic-labs-wallet/node`.
</Note>

## Generate a session key pair

```typescript theme={"system"}
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.

<Tip>
  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.
</Tip>

## 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.

<Warning>
  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.
</Warning>

```typescript theme={"system"}
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](/overview/rate-limits) — so agents must not retry `sendOtp` in a loop. If the code doesn't arrive, see [Email OTP delivery](/overview/troubleshooting/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](/node/agents/storage-and-security) for the full persistence matrix.

## Next step

Hand the JWT and the session private key to the wallet client: [Use Agent Wallets](/node/agents/use-agent-wallets).

## API reference

* [Email OTP sign-in](/node/reference/auth/email-otp-sign-in)
* [generateSessionKeyPair](/node/reference/auth/generate-session-key-pair)
* [createAuthClient](/node/reference/auth/create-auth-client)
