# Email OTP (/wallets/auth/email-otp)

> For the complete documentation index, see [llms.txt](/llms.txt)



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 [#add-otp-login]

Use `useSendOTP` to send the code and `useVerifyOTP` to complete authentication.

```tsx
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 [#how-it-works]

1. `useSendOTP` sends a one-time code and returns an `otpId` plus an `otpEncryptionTargetBundle`.
2. Store both values while the user enters the code.
3. `useVerifyOTP` verifies the `otpId`, `otpEncryptionTargetBundle`, and `code`.
4. After verification succeeds, the SDK creates a session and connects the ZeroDev Wagmi connector.

## Notes [#notes]

* Keep the `otpId` and `otpEncryptionTargetBundle` on the client until verification completes. For a single-page flow, component state is enough.
* The authenticated user's email is available from `useAuthenticators`: read `data?.emailContacts?.[0]?.email` after the wallet is connected.
* If you need link-based email login instead of manual code entry, use [Magic Link](/wallets/auth/magic-link).

## Next steps [#next-steps]

* [Send a transaction](/wallets/wallet-api/send-transaction)
* [Sign a message](/wallets/wallet-api/sign-message)
* Integrate other login methods: [Passkeys](/wallets/auth/passkeys), [Magic Link](/wallets/auth/magic-link), or [Google OAuth](/wallets/auth/google-oauth)
