Build a custom flow for handling email links
Email links can be used to sign up new users, sign in existing users, or allow existing users to verify newly added email addresses to their user profiles.
The email link flow works as follows:
- The user enters their email address and asks for an email link.
- Clerk sends an email to the user, containing a link to the verification URL.
- The user visits the email link, either on the same device where they entered their email address or on a different device, depending on the settings in the Clerk Dashboard.
- Clerk verifies the user's identity and advances any sign-up or sign-in attempt that might be in progress.
- If the verification is successful, the user is authenticated or their email address is verified, depending on the reason for the email link.
This guide demonstrates how to use Clerk's API to build a custom flow for handling email links. It covers the following scenarios:
Enable email link authentication
To allow your users to sign up or sign in using email links, you must first configure the appropriate settings in the Clerk Dashboard.
- In the Clerk Dashboard, navigate to the Email, phone, username page.
- For this guide, ensure that only Email address is required. If Phone number, Username, or Name are enabled, ensure they are not required. Use the settings icon next to each option to verify if it's required or optional. If you would like to require any of these, you'll need to combine their custom flows with the email link custom flow.
- In the Authentication strategies section, ensure only Email verification link is enabled. By default, Require the same device and browser is enabled, which means that email links are required to be verified from the same device and browser on which the sign-up or sign-in was initiated. For this guide, leave this setting enabled.
- Keep this page open to enable email link verification in the next step.
Enable email link verification
Verification methods are different from authentication strategies. Authentication strategies are used for authenticating a user, such as when they are signing in to your application. Verification methods are used for verifying a user's identifier, such as an email address upon initial sign-up or when adding a new email to their account.
To allow your users to verify their email addresses using email links, configure the following settings:
- On the Email, phone, username page of the Clerk Dashboard, next to Email address, select the settings icon. A modal will open.
- Under Verification methods, enable the Email verification link option. By default, Require the same device and browser is enabled, which means that email links are required to be verified from the same device and browser on which the sign-up or sign-in was initiated. For this guide, leave this setting enabled.
- Because this guide focuses on email links, uncheck the box for Email verification code.
- Select Continue to save your changes.
Sign-up flow
- The
useSignUp()
hook is used to get theSignUp
object. - The
SignUp
object is used to access thecreateEmailLinkFlow()
method. - The
createEmailLinkFlow()
method is used to access thestartEmailLinkFlow()
method. - The
startEmailLinkFlow()
method is called with theredirectUrl
parameter set to/sign-up/verify
. It sends an email with a verification link to the user. When the user visits the link, they are redirected to the URL that was provided. - On the
/sign-up/verify
page, theuseClerk()
hook is used to get thehandleEmailLinkVerification()
method. - The
handleEmailLinkVerification()
method is called to verify the email address. Error handling is included to handle any errors that occur during the verification process.
'use client'
import * as React from 'react'
import { useSignUp } from '@clerk/nextjs'
export default function SignInPage() {
const [emailAddress, setEmailAddress] = React.useState('')
const [verified, setVerified] = React.useState(false)
const [verifying, setVerifying] = React.useState(false)
const [error, setError] = React.useState('')
const { signUp, isLoaded } = useSignUp()
if (!isLoaded) return null
const { startEmailLinkFlow } = signUp.createEmailLinkFlow()
async function submit(e: React.FormEvent) {
e.preventDefault()
// Reset states in case user resubmits form mid sign-up
setVerified(false)
setError('')
setVerifying(true)
if (!isLoaded && !signUp) return null
// Start the sign-up process using the email provided
try {
await signUp.create({
emailAddress,
})
// Dynamically set the host domain for dev and prod
// You could instead use an environment variable or other source for the host domain
const protocol = window.location.protocol
const host = window.location.host
// Send the user an email with the email link
const signUpAttempt = await startEmailLinkFlow({
// URL to navigate to after the user visits the link in their email
redirectUrl: `${protocol}//${host}/sign-up/verify`,
})
// Check the verification result
const verification = signUpAttempt.verifications.emailAddress
// Handle if user visited the link and completed sign-up from /sign-up/verify
if (verification.verifiedFromTheSameClient()) {
setVerifying(false)
setVerified(true)
}
} catch (err: any) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(err, null, 2))
if (err.errors?.[0]?.longMessage) {
console.log('Clerk error:', err.errors[0].longMessage)
setError(err.errors[0].longMessage)
} else {
setError('An error occurred.')
}
}
}
async function reset(e: React.FormEvent) {
e.preventDefault()
setVerifying(false)
}
if (error) {
return (
<div>
<p>Error: {error}</p>
<button onClick={() => setError('')}>Try again</button>
</div>
)
}
if (verifying) {
return (
<div>
<p>Check your email and visit the link that was sent to you.</p>
<form onSubmit={reset}>
<button type="submit">Restart</button>
</form>
</div>
)
}
if (verified) {
return <div>Signed up successfully!</div>
}
return (
<div>
<h1>Sign up</h1>
<form onSubmit={submit}>
<input
type="email"
placeholder="Enter email address"
value={emailAddress}
onChange={(e) => setEmailAddress(e.target.value)}
/>
<button type="submit">Continue</button>
</form>
</div>
)
}
'use client'
import * as React from 'react'
import { useClerk } from '@clerk/nextjs'
import { EmailLinkErrorCodeStatus, isEmailLinkError } from '@clerk/nextjs/errors'
import Link from 'next/link'
export default function VerifyEmailLink() {
const [verificationStatus, setVerificationStatus] = React.useState('loading')
const { handleEmailLinkVerification, loaded } = useClerk()
async function verify() {
try {
// Dynamically set the host domain for dev and prod
// You could instead use an environment variable or other source for the host domain
const protocol = window.location.protocol
const host = window.location.host
await handleEmailLinkVerification({
// URL to navigate to if sign-up flow needs more requirements, such as MFA
redirectUrl: `${protocol}//${host}/sign-up`,
})
// If not redirected at this point,
// the flow has completed
setVerificationStatus('verified')
} catch (err: any) {
let status = 'failed'
if (isEmailLinkError(err)) {
// If link expired, set status to expired
if (err.code === EmailLinkErrorCodeStatus.Expired) {
status = 'expired'
} else if (err.code === EmailLinkErrorCodeStatus.ClientMismatch) {
// OPTIONAL: This check is only required if you have
// the 'Require the same device and browser' setting
// enabled in the Clerk Dashboard
status = 'client_mismatch'
}
}
setVerificationStatus(status)
}
}
React.useEffect(() => {
if (!loaded) return
verify()
}, [handleEmailLinkVerification, loaded])
if (verificationStatus === 'loading') {
return <div>Loading...</div>
}
if (verificationStatus === 'failed') {
return (
<div>
<h1>Verify your email</h1>
<p>The email link verification failed.</p>
<Link href="/sign-up">Sign up</Link>
</div>
)
}
if (verificationStatus === 'expired') {
return (
<div>
<h1>Verify your email</h1>
<p>The email link has expired.</p>
<Link href="/sign-up">Sign up</Link>
</div>
)
}
// OPTIONAL: This check is only required if you have
// the 'Require the same device and browser' setting
// enabled in the Clerk Dashboard
if (verificationStatus === 'client_mismatch') {
return (
<div>
<h1>Verify your email</h1>
<p>
You must complete the email link sign-up on the same device and browser that you started
it on.
</p>
<Link href="/sign-up">Sign up</Link>
</div>
)
}
return (
<div>
<h1>Verify your email</h1>
<p>Successfully signed up. Return to the original tab to continue.</p>
</div>
)
}
Sign-in flow
- The
useSignIn()
hook is used to get theSignIn
object. - The
SignIn
object is used to access thecreateEmailLinkFlow()
method. - The
createEmailLinkFlow()
method is used to access thestartEmailLinkFlow()
method. - The
startEmailLinkFlow()
method is called with theredirectUrl
parameter set to/sign-in/verify
. It sends an email with a verification link to the user. When the user visits the link, they are redirected to the URL that was provided. - On the
/sign-in/verify
page, theuseClerk()
hook is used to get thehandleEmailLinkVerification()
method. - The
handleEmailLinkVerification()
method is called to verify the email address. Error handling is included to handle any errors that occur during the verification process.
'use client'
import * as React from 'react'
import { useSignIn } from '@clerk/nextjs'
import { EmailLinkFactor, SignInFirstFactor } from '@clerk/types'
export default function SignInPage() {
const [emailAddress, setEmailAddress] = React.useState('')
const [verified, setVerified] = React.useState(false)
const [verifying, setVerifying] = React.useState(false)
const [error, setError] = React.useState('')
const { signIn, isLoaded } = useSignIn()
if (!isLoaded) return null
const { startEmailLinkFlow } = signIn.createEmailLinkFlow()
async function submit(e: React.FormEvent) {
e.preventDefault()
// Reset states in case user resubmits form mid sign-in
setVerified(false)
setError('')
if (!isLoaded && !signIn) return null
// Start the sign-in process using the email provided
try {
const { supportedFirstFactors } = await signIn.create({
identifier: emailAddress,
})
setVerifying(true)
// Filter the returned array to find the 'email_link' entry
const isEmailLinkFactor = (factor: SignInFirstFactor): factor is EmailLinkFactor => {
return factor.strategy === 'email_link'
}
const emailLinkFactor = supportedFirstFactors?.find(isEmailLinkFactor)
if (!emailLinkFactor) {
setError('Email link factor not found')
return
}
const { emailAddressId } = emailLinkFactor
// Dynamically set the host domain for dev and prod
// You could instead use an environment variable or other source for the host domain
const protocol = window.location.protocol
const host = window.location.host
// Send the user an email with the email link
const signInAttempt = await startEmailLinkFlow({
emailAddressId,
redirectUrl: `${protocol}//${host}/sign-in/verify`,
})
// Check the verification result
const verification = signInAttempt.firstFactorVerification
// Handle if verification expired
if (verification.status === 'expired') {
setError('The email link has expired.')
}
// Handle if user visited the link and completed sign-in from /sign-in/verify
if (verification.verifiedFromTheSameClient()) {
setVerifying(false)
setVerified(true)
}
} catch (err: any) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(err, null, 2))
setError('An error occurred.')
}
}
async function reset(e: React.FormEvent) {
e.preventDefault()
setVerifying(false)
}
if (error) {
return (
<div>
<p>Error: {error}</p>
<button onClick={() => setError('')}>Try again</button>
</div>
)
}
if (verifying) {
return (
<div>
<p>Check your email and visit the link that was sent to you.</p>
<form onSubmit={reset}>
<button type="submit">Restart</button>
</form>
</div>
)
}
if (verified) {
return <div>Signed in successfully!</div>
}
return (
<div>
<h1>Sign in</h1>
<form onSubmit={submit}>
<input
type="email"
placeholder="Enter email address"
value={emailAddress}
onChange={(e) => setEmailAddress(e.target.value)}
/>
<button type="submit">Continue</button>
</form>
</div>
)
}
'use client'
import * as React from 'react'
import { useClerk } from '@clerk/nextjs'
import { EmailLinkErrorCodeStatus, isEmailLinkError } from '@clerk/nextjs/errors'
import Link from 'next/link'
export default function VerifyEmailLink() {
const [verificationStatus, setVerificationStatus] = React.useState('loading')
const { handleEmailLinkVerification, loaded } = useClerk()
async function verify() {
try {
// Dynamically set the host domain for dev and prod
// You could instead use an environment variable or other source for the host domain
const protocol = window.location.protocol
const host = window.location.host
await handleEmailLinkVerification({
// URL to navigate to if sign-in flow needs more requirements, such as MFA
redirectUrl: `${protocol}//${host}/sign-in`,
})
// If not redirected at this point,
// the flow has completed
setVerificationStatus('verified')
} catch (err: any) {
let status = 'failed'
if (isEmailLinkError(err)) {
// If link expired, set status to expired
if (err.code === EmailLinkErrorCodeStatus.Expired) {
status = 'expired'
} else if (err.code === EmailLinkErrorCodeStatus.ClientMismatch) {
// OPTIONAL: This check is only required if you have
// the 'Require the same device and browser' setting
// enabled in the Clerk Dashboard
status = 'client_mismatch'
}
}
setVerificationStatus(status)
return
}
}
React.useEffect(() => {
if (!loaded) return
verify()
}, [handleEmailLinkVerification, loaded])
if (verificationStatus === 'loading') {
return <div>Loading...</div>
}
if (verificationStatus === 'failed') {
return (
<div>
<h1>Verify your email</h1>
<p>The email link verification failed.</p>
<Link href="/sign-in">Sign in</Link>
</div>
)
}
if (verificationStatus === 'expired') {
return (
<div>
<h1>Verify your email</h1>
<p>The email link has expired.</p>
<Link href="/sign-in">Sign in</Link>
</div>
)
}
// OPTIONAL: This check is only required if you have
// the 'Require the same device and browser' setting
// enabled in the Clerk Dashboard
if (verificationStatus === 'client_mismatch') {
return (
<div>
<h1>Verify your email</h1>
<p>
You must complete the email link sign-in on the same device and browser as you started it
on.
</p>
<Link href="/sign-in">Sign in</Link>
</div>
)
}
return (
<div>
<h1>Verify your email</h1>
<p>Successfully signed in. Return to the original tab to continue.</p>
</div>
)
}
Add new email flow
When a user adds an email address to their account, you can use email links to verify the email address.
- Every user has a
User
object that represents their account. TheUser
object has aemailAddresses
property that contains all the email addresses associated with the user. TheuseUser()
hook is used to get theUser
object. - The
User.createEmailAddress()
method is used to add the email address to the user's account. A newEmailAddress
object is created and stored inUser.emailAddresses
. - The newly created
EmailAddress
object is used to access thecreateEmailLinkFlow()
method. - The
createEmailLinkFlow()
method is used to access thestartEmailLinkFlow()
method. - The
startEmailLinkFlow()
method is called with theredirectUrl
parameter set to/account/add-email/verify
. It sends an email with a verification link to the user. When the user visits the link, they are redirected to the URL that was provided. - On the
/account/add-email/verify
page, theuseClerk()
hook is used to get thehandleEmailLinkVerification()
method. - The
handleEmailLinkVerification()
method is called to verify the email address. Error handling is included to handle any errors that occur during the verification process.
'use client'
import * as React from 'react'
import { useUser } from '@clerk/nextjs'
export default function Page() {
const { isLoaded, user } = useUser()
const [email, setEmail] = React.useState('')
const [verifying, setVerifying] = React.useState(false)
const [error, setError] = React.useState('')
if (!isLoaded) return null
if (isLoaded && !user?.id) {
return <p>You must be signed in to access this page</p>
}
// Handle addition of the email address
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
setVerifying(true)
// Add an unverified email address to user
const res = await user.createEmailAddress({ email })
// Reload user to get updated User object
await user.reload()
// Find the email address that was just added
const emailAddress = user.emailAddresses.find((a) => a.id === res.id)
if (!emailAddress) {
setError('Email address not found')
return
}
const { startEmailLinkFlow } = emailAddress.createEmailLinkFlow()
// Dynamically set the host domain for dev and prod
// You could instead use an environment variable or other source for the host domain
const protocol = window.location.protocol
const host = window.location.host
// Send the user an email with the verification link
startEmailLinkFlow({ redirectUrl: `${protocol}//${host}/account/add-email/verify` })
} catch (err) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(err, null, 2))
setError('An error occurred.')
}
}
async function reset(e: React.FormEvent) {
e.preventDefault()
setVerifying(false)
}
if (error) {
return (
<div>
<p>Error: {error}</p>
<button onClick={() => setError('')}>Try again</button>
</div>
)
}
if (verifying) {
return (
<div>
<p>Check your email and visit the link that was sent to you.</p>
<form onSubmit={reset}>
<button type="submit">Restart</button>
</form>
</div>
)
}
// Display the initial form to capture the email address
return (
<>
<h1>Add email</h1>
<div>
<form onSubmit={(e) => handleSubmit(e)}>
<input
placeholder="Enter email address"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Continue</button>
</form>
</div>
</>
)
}
'use client'
import * as React from 'react'
import { useClerk } from '@clerk/nextjs'
import { EmailLinkErrorCode, isEmailLinkError } from '@clerk/nextjs/errors'
import Link from 'next/link'
export type VerificationStatus =
| 'expired'
| 'failed'
| 'loading'
| 'verified'
| 'verified_switch_tab'
| 'client_mismatch'
export default function VerifyEmailLink() {
const [verificationStatus, setVerificationStatus] = React.useState('loading')
const { handleEmailLinkVerification, loaded } = useClerk()
async function verify() {
try {
await handleEmailLinkVerification({})
setVerificationStatus('verified')
} catch (err: any) {
let status: VerificationStatus = 'failed'
if (isEmailLinkError(err)) {
// If link expired, set status to expired
if (err.code === EmailLinkErrorCode.Expired) {
status = 'expired'
} else if (err.code === EmailLinkErrorCode.ClientMismatch) {
// OPTIONAL: This check is only required if you have
// the 'Require the same device and browser' setting
// enabled in the Clerk Dashboard
status = 'client_mismatch'
}
}
setVerificationStatus(status)
return
}
}
React.useEffect(() => {
if (!loaded) return
verify()
}, [handleEmailLinkVerification, loaded])
if (verificationStatus === 'loading') {
return <div>Loading...</div>
}
if (verificationStatus === 'failed') {
return (
<div>
<h1>Verify your email</h1>
<p>The email link verification failed.</p>
<Link href="/account/add-email">Return to add email</Link>
</div>
)
}
if (verificationStatus === 'expired') {
return (
<div>
<h1>Verify your email</h1>
<p>The email link has expired.</p>
<Link href="/account/add-email">Return to add email</Link>
</div>
)
}
// OPTIONAL: This check is only required if you have
// the 'Require the same device and browser' setting
// enabled in the Clerk Dashboard
if (verificationStatus === 'client_mismatch') {
return (
<div>
<h1>Verify your email</h1>
<p>
You must complete the email link verification on the same device and browser as you
started it on.
</p>
<Link href="/account/add-email">Return to add email</Link>
</div>
)
}
return (
<div>
<h1>Verify your email</h1>
<p>Successfully added email!</p>
</div>
)
}
Feedback
Last updated on