Email OTP Authentication

Email OTP (One-Time Password) authentication is a two-step process:

  1. The user enters their email address and requests a verification code
  2. The user enters the 6-digit code they receive in their inbox to complete authentication

You can implement Email OTP authentication in two ways:

Pre-built UI Components

Account Kit provides pre-built UI components that handle the entire Email OTP authentication flow with minimal code.

Step 1: Add Authentication Components to Your Page

Before configuring your authentication, first add one of the pre-built components to your application:

Using Modal Authentication

To add authentication in a modal popup:

import React from "react";
import { 
const useAuthModal: () => { isOpen: boolean; openAuthModal: () => void; closeAuthModal: () => void; }

A hook that returns the open and close functions for the Auth Modal if uiConfig is enabled on the Account Provider

useAuthModal
} from "@account-kit/react";
export default function
function MyPage(): JSX.Element
MyPage
() {
const {
const openAuthModal: () => void
openAuthModal
} =
function useAuthModal(): { isOpen: boolean; openAuthModal: () => void; closeAuthModal: () => void; }

A hook that returns the open and close functions for the Auth Modal if uiConfig is enabled on the Account Provider

useAuthModal
();
return <
React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
button
React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefined
onClick
={
const openAuthModal: () => void
openAuthModal
}>Sign in</
React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
button
>;
}

For more details on modal configuration, see the Modal Authentication documentation.

Or:

Using Embedded Authentication

To embed authentication directly in your page:

import React from "react";
import { 
const AuthCard: (props: AuthCardProps) => JSX.Element

React component containing an Auth view with configured auth methods and options based on the config passed to the AlchemyAccountProvider

AuthCard
} from "@account-kit/react";
export default function
function MyLoginPage(): JSX.Element
MyLoginPage
() {
return ( <
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
React.HTMLAttributes<HTMLDivElement>.className?: string | undefined
className
="flex flex-row p-4 bg-white border border-gray-200 rounded-lg">
<
const AuthCard: (props: AuthCardProps) => JSX.Element

React component containing an Auth view with configured auth methods and options based on the config passed to the AlchemyAccountProvider

AuthCard
/>
</
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
); }

For more details on embedded authentication, see the Embedded Authentication documentation.

Step 2: Configure Email OTP in UI Components

After adding the components, configure the Email OTP authentication in your application config:

To customize the Email OTP authentication experience in your pre-built components, configure the UI as follows:

import { 
type AlchemyAccountsUIConfig = { auth?: { addPasskeyOnSignup?: boolean; header?: React.ReactNode; hideError?: boolean; onAuthSuccess?: () => void; sections: AuthType[][]; hideSignInText?: boolean; }; illustrationStyle?: "outline" | "linear" | "filled" | "flat" | undefined; modalBaseClassName?: string; supportUrl?: string | undefined; }
AlchemyAccountsUIConfig
,
const createConfig: (props: CreateConfigProps, ui?: AlchemyAccountsUIConfig) => AlchemyAccountsConfigWithUI

Wraps the createConfig that is exported from @aa-sdk/core to allow passing an additional argument, the configuration object for the Auth Components UI (the modal and AuthCard).

createConfig
} from "@account-kit/react";
import {
const sepolia: Chain
sepolia
,
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.

alchemy
} from "@account-kit/infra";
const
const uiConfig: AlchemyAccountsUIConfig
uiConfig
:
type AlchemyAccountsUIConfig = { auth?: { addPasskeyOnSignup?: boolean; header?: React.ReactNode; hideError?: boolean; onAuthSuccess?: () => void; sections: AuthType[][]; hideSignInText?: boolean; }; illustrationStyle?: "outline" | "linear" | "filled" | "flat" | undefined; modalBaseClassName?: string; supportUrl?: string | undefined; }
AlchemyAccountsUIConfig
= {
auth?: { addPasskeyOnSignup?: boolean; header?: React.ReactNode; hideError?: boolean; onAuthSuccess?: () => void; sections: AuthType[][]; hideSignInText?: boolean; } | undefined
auth
: {
sections: AuthType[][]

Each section can contain multiple auth types which will be grouped together and separated by an OR divider

sections
: [
[ {
type: "email"
type
: "email",
emailMode?: "magicLink" | "otp" | undefined
emailMode
: "otp",
// Optional customizations:
buttonLabel?: string | undefined
buttonLabel
: "Continue with Email",
placeholder?: string | undefined
placeholder
: "Enter your email address",
}, ], ], }, }; export const
const config: AlchemyAccountsConfigWithUI
config
=
function createConfig(props: CreateConfigProps, ui?: AlchemyAccountsUIConfig): AlchemyAccountsConfigWithUI

Wraps the createConfig that is exported from @aa-sdk/core to allow passing an additional argument, the configuration object for the Auth Components UI (the modal and AuthCard).

createConfig
(
{
transport: AlchemyTransport
transport
:
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.

alchemy
({
apiKey: string
apiKey
: "your-api-key" }),
chain: Chain
chain
:
const sepolia: Chain
sepolia
,
},
const uiConfig: AlchemyAccountsUIConfig
uiConfig
,
);

Email OTP configuration accepts the following options:

type 
type EmailAuthType = { type: "email"; emailMode: "otp"; hideButton?: boolean; buttonLabel?: string; placeholder?: string; }
EmailAuthType
= {
type: "email"
type
: "email";
emailMode: "otp"
emailMode
: "otp";
// hides the continue button
hideButton?: boolean | undefined
hideButton
?: boolean;
// changes the button label
buttonLabel?: string | undefined
buttonLabel
?: string;
// changes the placeholder text in the input
placeholder?: string | undefined
placeholder
?: string;
};

You can find the full type definition in the Account Kit source code.

For more details on UI component customization, see the UI Components documentation.

Custom UI

If you need complete control over the user experience, you can implement your own custom UI for Email OTP authentication using Account Kit hooks.

Step 1: Send the OTP

First, prompt your user for their email address and send an OTP:

import { 
function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResult

Hook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.

This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.

useAuthenticate
} from "@account-kit/react";
// Inside your component const {
const authenticate: UseMutateFunction<User, Error, AuthParams, unknown>
authenticate
} =
function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResult

Hook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.

This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.

useAuthenticate
();
// When the user submits their email const
const handleSendCode: (email: string) => void
handleSendCode
= (
email: string
email
: string) => {
const authenticate: (variables: AuthParams, options?: MutateOptions<User, Error, AuthParams, unknown> | undefined) => void
authenticate
(
{
type: "email"
type
: "email",
emailMode?: "otp" | "magicLink" | undefined
emailMode
: "otp",
email: string
email
,
}, {
MutateOptions<User, Error, AuthParams, unknown>.onSuccess?: ((data: User, variables: AuthParams, context: unknown) => void) | undefined
onSuccess
: () => {
// onSuccess only fires once the entire flow is done (email OTP + optional MFA). // It still runs even if the final step completes in another tab/window. },
MutateOptions<User, Error, AuthParams, unknown>.onError?: ((error: Error, variables: AuthParams, context: unknown) => void) | undefined
onError
: (
error: Error
error
) => {
// Handle error }, }, ); };

Step 2: Show OTP Input on Status Change

Use the useSignerStatus hook and AlchemySignerStatus enum to react to status changes:

import React from "react";
import { 
const useSignerStatus: (override?: AlchemyAccountContextProps) => UseSignerStatusResult

Hook to get the signer status, optionally using an override configuration, useful if you’re building your own login.

useSignerStatus
} from "@account-kit/react";
import {
enum AlchemySignerStatus
AlchemySignerStatus
} from "@account-kit/signer";
const
const TrackStatus: () => JSX.Element
TrackStatus
= () => {
const {
const status: AlchemySignerStatus
status
} =
function useSignerStatus(override?: AlchemyAccountContextProps): UseSignerStatusResult

Hook to get the signer status, optionally using an override configuration, useful if you’re building your own login.

useSignerStatus
();
return ( <> {
const status: AlchemySignerStatus
status
===
enum AlchemySignerStatus
AlchemySignerStatus
.
function (enum member) AlchemySignerStatus.AWAITING_EMAIL_AUTH = "AWAITING_EMAIL_AUTH"
AWAITING_EMAIL_AUTH
&& (
<
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>Prompt the user to enter the OTP code</
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
)} </> ); };

Step 3: Verify the OTP

Once the user receives the code, they’ll enter it in your application:

import { 
function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResult

Hook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.

This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.

useAuthenticate
} from "@account-kit/react";
// Inside your component const {
const authenticate: UseMutateFunction<User, Error, AuthParams, unknown>
authenticate
} =
function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResult

Hook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.

This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.

useAuthenticate
();
// When the user submits the OTP code const
const handleVerifyCode: (otpCode: string) => void
handleVerifyCode
= (
otpCode: string
otpCode
: string) => {
const authenticate: (variables: AuthParams, options?: MutateOptions<User, Error, AuthParams, unknown> | undefined) => void
authenticate
(
{
type: "otp"
type
: "otp",
otpCode: string
otpCode
,
}, {
MutateOptions<User, Error, AuthParams, unknown>.onSuccess?: ((data: User, variables: AuthParams, context: unknown) => void) | undefined
onSuccess
: () => {
// onSuccess only fires once the entire flow is done (email OTP + optional MFA). // It still runs even if the final step completes in another tab/window. },
MutateOptions<User, Error, AuthParams, unknown>.onError?: ((error: Error, variables: AuthParams, context: unknown) => void) | undefined
onError
: (
error: Error
error
) => {
// Handle invalid code error }, }, ); };

Step 4: Check authentication status

Use the useSignerStatus hook to determine if the user is authenticated:

import { 
const useSignerStatus: (override?: AlchemyAccountContextProps) => UseSignerStatusResult

Hook to get the signer status, optionally using an override configuration, useful if you’re building your own login.

useSignerStatus
} from "@account-kit/react";
// Inside your component const {
const isConnected: boolean
isConnected
} =
function useSignerStatus(override?: AlchemyAccountContextProps): UseSignerStatusResult

Hook to get the signer status, optionally using an override configuration, useful if you’re building your own login.

useSignerStatus
();
// You can use isConnected to conditionally render UI

Next Steps

Add Authenticator App (TOTP) Verification (Optional)

If you’d like to add a second security step to Email OTP, you can enable Multi-Factor Authentication. This prompts users for a 6-digit TOTP code from their authenticator app (e.g. Google Authenticator, Authy) after they verify their email.