Email OTP
Authenticate with a one-time code
Email OTP is a two-step login flow: send a code to the user's email address, then verify the code they enter. Use OTP when you want a familiar email-based flow without redirecting the user away from your app. After verification succeeds, the ZeroDev Wagmi connector is connected.
Add OTP login
Use useSendOTP to send the code and useVerifyOTP to complete authentication.
import { useSendOTP, useVerifyOTP } from '@zerodev/wallet-react'
import { useState } from 'react'
import { useAccount, useDisconnect } from 'wagmi'
export function EmailOTPLogin() {
const [email, setEmail] = useState('')
const [code, setCode] = useState('')
const [otpId, setOtpId] = useState<string | null>(null)
const [otpEncryptionTargetBundle, setOtpEncryptionTargetBundle] =
useState<string | null>(null)
const { address, isConnected } = useAccount()
const { disconnect } = useDisconnect()
const sendOTP = useSendOTP()
const verifyOTP = useVerifyOTP()
if (isConnected) {
return (
<div>
<p>Connected: {address}</p>
<button type="button" onClick={() => disconnect()}>
Disconnect
</button>
</div>
)
}
if (!otpId) {
return (
<div>
<input
type="email"
autoComplete="email"
placeholder="you@example.com"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
<button
type="button"
disabled={sendOTP.isPending || !email}
onClick={async () => {
const result = await sendOTP.mutateAsync({ email })
setOtpId(result.otpId)
setOtpEncryptionTargetBundle(result.otpEncryptionTargetBundle)
}}
>
{sendOTP.isPending ? 'Sending code...' : 'Send code'}
</button>
{sendOTP.error ? <p>{sendOTP.error.message}</p> : null}
</div>
)
}
return (
<div>
<p>Code sent to {email}</p>
<input
inputMode="numeric"
autoComplete="one-time-code"
placeholder="Enter code"
value={code}
onChange={(event) => setCode(event.target.value)}
/>
<button
type="button"
disabled={verifyOTP.isPending || !code || !otpEncryptionTargetBundle}
onClick={() =>
otpEncryptionTargetBundle &&
verifyOTP.mutate({
otpId,
otpEncryptionTargetBundle,
code,
})
}
>
{verifyOTP.isPending ? 'Verifying...' : 'Verify code'}
</button>
<button
type="button"
onClick={() => {
setOtpId(null)
setOtpEncryptionTargetBundle(null)
}}
>
Use a different email
</button>
{verifyOTP.error ? <p>{verifyOTP.error.message}</p> : null}
</div>
)
}How it works
useSendOTPsends a one-time code and returns anotpIdplus anotpEncryptionTargetBundle.- Store both values while the user enters the code.
useVerifyOTPverifies theotpId,otpEncryptionTargetBundle, andcode.- After verification succeeds, the SDK creates a session and connects the ZeroDev Wagmi connector.
Notes
- Keep the
otpIdandotpEncryptionTargetBundleon the client until verification completes. For a single-page flow, component state is enough. - The authenticated user's email is available in
emailContactsfromuseAuthenticators. - If you need link-based email login instead of manual code entry, use Magic Link.
Next steps
- Send a transaction
- Sign a message
- Integrate other login methods: Passkeys, Magic Link, or Google OAuth