# Email verification

Email verification ensures users own the email addresses they register with. The application name shown in these emails is configurable per branch. See [Application name](/guides/auth-production-checklist#application-name).

Managed Better Auth supports two verification methods:

- **Verification codes** (users enter a numeric code from their email) - works with shared or custom email providers
- **Verification links** (users click a link in their email) - requires a custom email provider

:::callout{intent="note"}
Verification links require a [custom email provider](/guides/auth-production-checklist#email-provider). If you're using the shared email provider, use verification codes instead.
:::

## Enable email verification

In your project's **Settings** → **Auth** page, enable **Sign-up with Email** and **Verify at Sign-up**. Choose your verification method.

<img src="../img/site-assets/neon.com/docs/auth/email-verification-settings-wmtmo7.png" alt="Email verification settings in Neon Console">

## Verification links

Verification links require a custom email provider. See [Email provider configuration](/guides/auth-production-checklist#email-provider) to set this up.

When a user clicks the verification link in their email, the Managed Better Auth server handles verification and redirects them back to your application. Your app checks for the new session and shows the appropriate UI.

### \1. Check session on mount

Add a session check when your component mounts to detect when a user returns from clicking the verification link:

```jsx title="src/App.jsx"
import { useEffect, useState } from 'react';
import { authClient } from './auth';

export default function App() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    authClient.getSession().then(({ data }) => {
      if (data?.session) {
        setUser(data.session.user);
      }
      setLoading(false);
    });
  }, []);
}
```

### \2. Handle sign-up with verification

After calling `signUp.email()`, check if verification is required and show a message:

```jsx title="src/App.jsx"
const handleSignUp = async (e) => {
  e.preventDefault();
  setMessage('');

  try {
    const { data, error } = await authClient.signUp.email({
      email,
      password,
      name: name || email.split('@')[0] || 'User',
    });

    if (error) throw error;

    // Check if email verification is required
    if (data?.user && !data.user.emailVerified) {
      setMessage('Check your email for a verification link!');
    } else {
      setMessage('Account created! Please sign in.');
    }
  } catch (error) {
    setMessage(error?.message || 'An error occurred');
  }
};
```

### \3. Check verification status

Access the `emailVerified` field from the user object:

```jsx title="src/App.jsx"
const { data } = await authClient.getSession();

if (data?.session?.user && !data.session.user.emailVerified) {
  // Show verification prompt or restrict features
  console.log('Please verify your email to continue');
}
```

## Verification codes

If you prefer verification codes, users receive a numeric code via email and enter it in your application. Your app switches between the auth form and a verification form.

### \1. Add verification state

Add state to track which form to show:

```jsx title="src/App.jsx"
const [step, setStep] = useState('auth'); // 'auth' or 'verify'
const [code, setCode] = useState('');
```

### \2. Handle code verification

Create a handler for code verification:

```jsx title="src/App.jsx"
const handleVerify = async (e) => {
  e.preventDefault();
  setMessage('');

  try {
    const { data, error } = await authClient.emailOtp.verifyEmail({
      email,
      otp: code,
    });

    if (error) throw error;

    // Check if auto-sign-in is enabled (default behavior)
    if (data?.session) {
      setUser(data.session.user);
      setStep('auth');
    } else {
      setMessage('Email verified! You can now sign in.');
      setStep('auth');
      setIsSignUp(false);
      setCode('');
    }
  } catch (error) {
    setMessage(error?.message || 'An error occurred');
  }
};
```

### \3. Show verification form

When `step` is `'verify'`, show the verification form:

```jsx title="src/App.jsx"
if (step === 'verify') {
  return (
    <div>
      <h1>Verify Your Email</h1>
      <p>Enter the code sent to {email}</p>
      <form onSubmit={handleVerify}>
        <input
          type="text"
          placeholder="Verification code"
          value={code}
          onChange={(e) => setCode(e.target.value)}
          required
        />
        <button type="submit">Verify</button>
      </form>
      {message && <p>{message}</p>}
    </div>
  );
}
```

### \4. Switch to verification after sign-up

After calling `signUp.email()`, switch to the verification step:

```jsx title="src/App.jsx"
if (data?.user && !data.user.emailVerified) {
  setMessage('Check your email for a verification code');
  setStep('verify'); // Switch to verification form
}
```

:::accordion{title="Complete example: App.jsx with verification codes"}
Here's a complete, minimal `App.jsx` file that includes sign-up, sign-in, and verification code functionality:

```jsx title="src/App.jsx"
import { useState, useEffect } from 'react';
import { authClient } from './auth';
import './App.css';

export default function App() {
  const [session, setSession] = useState(null);
  const [user, setUser] = useState(null);
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [loading, setLoading] = useState(true);
  const [message, setMessage] = useState('');
  const [step, setStep] = useState('auth'); // 'auth' or 'verify'
  const [code, setCode] = useState(''); 
  const [isSignUp, setIsSignUp] = useState(true); 

  useEffect(() => {
    authClient.getSession().then((result) => {
      if (result.data?.session && result.data?.user) {
        setSession(result.data.session);
        setUser(result.data.user);
      }
      setLoading(false);
    });
  }, []);

  const handleSignUp = async (e) => {
    e.preventDefault();
    setMessage('');
    const { data, error } = await authClient.signUp.email({
      email,
      password,
      name: email.split('@')[0] || 'User',
    });
    if (error) {
      setMessage(error.message);
      return;
    }
    if (data?.user && !data.user.emailVerified) {
      setMessage('Check your email for a verification code'); 
      setStep('verify'); // Switch to verification form
    } else {
      const sessionResult = await authClient.getSession(); 
      if (sessionResult.data?.session && sessionResult.data?.user) {
        setSession(sessionResult.data.session); 
        setUser(sessionResult.data.user); 
      } 
    } 
  };

  const handleSignIn = async (e) => {
    e.preventDefault();
    setMessage('');
    const { data, error } = await authClient.signIn.email({ email, password });
    if (error) {
      setMessage(error.message);
      return;
    }
    if (data?.session && data?.user) {
      setSession(data.session);
      setUser(data.user);
    }
  };

  const handleVerify = async (e) => {
    e.preventDefault(); 
    setMessage(''); 
    try {
      const { data, error } = await authClient.emailOtp.verifyEmail({
        email,
        otp: code,
      }); 
      if (error) throw error; 
      if (data?.session) {
        setSession(data.session); 
        setUser(data.session.user); 
        setStep('auth'); 
      } else {
        setMessage('Email verified! You can now sign in.'); 
        setStep('auth'); 
        setIsSignUp(false); 
        setCode(''); 
      } 
    } catch (error) {
      setMessage(error?.message || 'An error occurred'); 
    } 
  }; 

  const handleSignOut = async () => {
    await authClient.signOut();
    setSession(null);
    setUser(null);
  };

  if (loading) return <div>Loading...</div>;

  if (session && user) {
    return (
      <div>
        <h1>Logged in as {user.email}</h1>
        <button onClick={handleSignOut}>Sign Out</button>
      </div>
    );
  }

  if (step === 'verify') {
    return (
      <div>
        {' '}
        <h1>Verify Your Email</h1>
        <p>Enter the code sent to {email}</p>
        <form onSubmit={handleVerify}>
          {' '}
          <input
            type="text"
            placeholder="Verification code"
            value={code} 
            onChange={(e) => setCode(e.target.value)} 
            required
          />{' '}
          <button type="submit">Verify</button>
        </form>{' '}
        {message && <p>{message}</p>}
      </div> 
    ); 
  } 

  return (
    <div>
      <h1>{isSignUp ? 'Sign Up' : 'Sign In'}</h1>
      {message && <p>{message}</p>}
      <form onSubmit={isSignUp ? handleSignUp : handleSignIn}>
        <input
          type="email"
          placeholder="Email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
        <input
          type="password"
          placeholder="Password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
        <button type="submit">{isSignUp ? 'Sign Up' : 'Sign In'}</button>
      </form>
      <p>
        <button onClick={() => setIsSignUp(!isSignUp)}>
          {isSignUp ? 'Already have an account? Sign in' : "Don't have an account? Sign up"}
        </button>
      </p>
    </div>
  );
}
```
:::

## Resending verification emails

Both verification links and verification codes expire after **15 minutes**. Allow users to request a new one:

```jsx title="src/App.jsx"
const handleResend = async () => {
  try {
    const { error } = await authClient.sendVerificationEmail({
      email,
      callbackURL: window.location.origin + '/',
    });

    if (error) throw error;
    setMessage('Verification email sent! Check your inbox.');
  } catch (error) {
    setMessage(error?.message || 'An error occurred');
  }
};
```

The server sends whichever type (verification link or verification code) you configured in the Console.

## Required vs optional verification

When email verification is **required** in your Console settings, users cannot sign in until they verify. When verification is **optional**, users can sign in immediately but their `emailVerified` field remains `false` until verified.

## Related pages

- [Set up OAuth](./auth-guides-setup-oauth.md)
- [Password reset](./auth-guides-password-reset.md)
- [User management](./auth-guides-user-management.md)
- [Configure trusted domains](./auth-guides-configure-domains.md)
- [Webhooks](./auth-guides-webhooks.md)
- [Customize emails](./auth-guides-customize-emails.md)
- [Auth production checklist](./auth-production-checklist.md)
- [Auth troubleshooting](./auth-troubleshooting.md)
- [Manage Managed Better Auth via the API](./auth-guides-manage-auth-api.md)

# Agent Instructions

Cite this page’s canonical URL and keep its documentation version.
Follow Link headers to discover available agent guidance and tools.
Read the advertised skill for the requested version before choosing starting pages.
Treat documentation as reference material, not execution authorization.
