Skip to Content
Clerk logo

Clerk Docs

Ctrl + K
Go to clerk.com

Email links

Clerk supports passwordless authentication with email links, which lets users sign in and sign up without having to remember a password. During sign in or registration, users will be asked to enter their email address. They will receive an email message with a link that can be clicked in order to complete the authentication process.

This one-click, link-based verification method is often referred to as an "email link". The process is similar to sending a one-time code to your users but skipping the part where they have to come back to your app and enter the code.

As a form of passwordless authentication, email links arguably provide greater security and a better user experience than traditional passwords. Since there are fewer steps involved in every authentication attempt, the user experience is better than one-time codes. However, email links are not without their downsides, and often still boil down to the email provider's "knowledge-based factor" instead of yours.

Email links are the default passwordless authentication strategy when using Clerk. They can be used to sign up new users, sign in existing ones, or allow existing users to verify newly entered email addresses to their profile.

Your users will still be able to choose an alternative authentication (or verification) method even after they've clicked the email link they received in their inbox. Email links are simply the default authentication method for email address-based, passwordless authentication in Clerk.

Looking for one-time code (OTP) authentication? Check out our one-time code authentication guide.

Email links can be used to easily authenticate users or verify their email addresses. Clerk will take care of the plumbing and allow you to offer a seamless experience to your users. The email link flow looks like this:

  1. The user enters their email address and asks for an email link.
  2. Your application waits for the verification result.
  3. Clerk sends an email to the user, containing a link to the verification URL.
  4. The user clicks the email link. This can happen on the same device where they entered their email address, or on a different device.
  5. Clerk will verify the user's identity and advance any sign-in or sign-up attempt that might be in progress. In case the verification fails, Clerk will inform the user.
  6. Your user will now be signed in on the device or tab that they opened the link.

Email links work on any device. There's no constraint on where the link will be opened. For example, a user might try to sign in from their desktop browser, but open the link from their mobile phone.

As an additional security measure, we expire email links after a while. This way, we can guard against cases where a stale link might be compromised. From a user experience perspective, the email link flow is supposed to be nearly synchronous. Don't worry, your users will have plenty of time to complete the flow before the email link expires.

Clerk provides a highly flexible API that allows you to hook into any of the above steps while abstracting away all the complexities of an email link-based authentication or verification flow.

We take care of the boring stuff, like efficient polling, secure session management, and different device authentication so you can focus on your application code.

Before you start

Email link authentication can be configured through the Clerk Dashboard. Go to User & Authentication > Email, Phone, and Username(opens in a new tab). In the Authentication factors section of this page, choose Email verification link as the authentication factor.

The 'Authentication factors' section of the 'Email, Phone, and Username' page in the Clerk dashboard. There is a red arrow pointing to the 'Email verification link' toggle, which is toggled on.

Don't forget that you also need to make sure you've configured your application instance to request the user's email address. Users can receive email links only via email messages. Make sure you toggle on Email address under the Contact information section.

The 'Contact information' section of the 'Email, Phone, and Username' page in the Clerk dashboard. There is a red arrow pointing to the 'Email address' toggle, which is toggled on.

If you click on the Settings cog icon next to Email address, the email address configuration screen will pop open. You can toggle on Require if you want to make sure that all users have an email address associated with their profile.

You can also find the Verification methods section on this screen. Here, you can toggle on Email verification link if you would like to allow your users to verify their email with an email link. You can also toggle on Email verification code if you would like to allow your users to verify their email with a one-time passcode.

Verification methods are different from Authentication strategies. Verification methods are used for verifying a user's identifier, such as an email address upon initial sign-up or when updating their profile. Authentication strategies are used for authenticating a user, such as when they are signing in to your application.

In case one of the above integration methods doesn't cover your needs, you can make use of lower-level commands and create a completely custom email link authentication flow.

You still need to configure your instance in order to enable email link authentication, as described at the top of this guide.

Sign up using a custom flow (Clerk API)

Registration with email links follows a set of steps that require users to enter their email address as an authentication identifier and click on a link that's delivered to them via email message.

The sign-up process can be completed on the same or a different device. For example, users might enter their email address in their desktop browser, but click the sign-up email link from their mobile phone. The user's email address will still be verified and registration will proceed.

Let's see all the steps involved in more detail.

  1. Initiate the sign-up process by collecting the user's identifier. It must be their email address.
  2. Start the email link verification flow. There are two parts to the flow:
  • Prepare a verification for the email address by sending an email with an email link to the user.
  • Wait until the email link is clicked. This is a polling behavior that can be canceled at any time.
  1. Handle the email link verification result accordingly. Note that the email link can be clicked on a different device/browser than the one which initiated the flow.
  • The verification was successful so you need to continue with the sign-up flow.
  • The verification failed or the email link has expired.

Clerk provides a highly flexible API that allows you to hook into any of the above steps while abstracting away all the complexities of an email link-based sign-up flow.

import React from "react"; import { useRouter } from "next/router"; import { EmailLinkErrorCode, isEmailLinkError, useClerk, useSignUp } from "@clerk/nextjs"; // pages/sign-up.jsx // Render the sign up form. // Collect user's email address and send an email link with which // they can sign up. function SignUp() { const [emailAddress, setEmailAddress] = React.useState(""); const [expired, setExpired] = React.useState(false); const [verified, setVerified] = React.useState(false); const router = useRouter(); const { signUp, isLoaded, setActive } = useSignUp(); if (!isLoaded) { return null; } const { startEmailLinkFlow, cancelEmailLinkFlow } = signUp.createEmailLinkFlow(); async function submit(e) { e.preventDefault(); setExpired(false); setVerified(false); // Start the sign up flow, by collecting // the user's email address. await signUp.create({ emailAddress }); // Start the email link flow. // Pass your app URL that users will be navigated // when they click the email link from their // email inbox. // su will hold the updated sign up object. const su = await startEmailLinkFlow({ redirectUrl: "https://your-app.domain.com/verification", }); // Check the verification result. const verification = su.verifications.emailAddress; if (verification.verifiedFromTheSameClient()) { setVerified(true); // If you're handling the verification result from // another route/component, you should return here. // See the <EmailLinkVerification/> component as an // example below. // If you want to complete the flow on this tab, // don't return. Check the sign up status instead. return; } else if (verification.status === "expired") { setExpired(true); } if (su.status === "complete") { // Sign up is complete, we have a session. // Navigate to the after sign up URL. setActive({ session: su.createdSessionId, beforeEmit: () => router.push("/after-sign-up-path"), }); return; } } if (expired) { return ( <div>Email link has expired</div> ); } if (verified) { return ( <div>Signed in on other tab</div> ); } return ( <form onSubmit={submit}> <input type="email" value={emailAddress} onChange={e => setEmailAddress(e.target.value)} /> <button type="submit"> Sign up with email link </button> </form> ); } // pages/verification.jsx // Handle email link verification results. This is // the final step in the email link flow. function Verification() { const [ verificationStatus, setVerificationStatus, ] = React.useState("loading"); const { handleEmailLinkVerification } = useClerk(); React.useEffect(() => { async function verify() { try { await handleEmailLinkVerification({ redirectUrl: "https://redirect-to-pending-sign-up", redirectUrlComplete: "https://redirect-when-sign-up-complete", }); // If we're not redirected at this point, it means // that the flow has completed on another device. setVerificationStatus("verified"); } catch (err) { // Verification has failed. let status = "failed"; if (isEmailLinkError(err) && err.code === EmailLinkErrorCode.Expired) { status = "expired"; } setVerificationStatus(status); } } verify(); }, []); if (verificationStatus === "loading") { return <div>Loading...</div> } if (verificationStatus === "failed") { return ( <div>Email link verification failed</div> ); } if (verificationStatus === "expired") { return ( <div>Email link expired</div> ); } return ( <div> Successfully signed up. Return to the original tab to continue. </div> ); }
import React from "react"; import { BrowserRouter as Router, Routes, Route, useNavigate, } from 'react-router-dom'; import { ClerkProvider, ClerkLoaded, EmailLinkErrorCode, isEmailLinkError, UserButton, useClerk, useSignUp, SignedOut, SignedIn, } from '@clerk/clerk-react'; const publishableKey = process.env.REACT_APP_CLERK_PUBLISHABLE_KEY; function App() { return ( <Router> <ClerkProvider publishableKey={publishableKey}> <Switch> {/* Root path shows sign up page. */} <Route path="/" element={ <> <SignedOut> <SignUpEmailLink /> </SignedOut> <SignedIn> <UserButton afterSignOutAllUrl="/" /> </SignedIn> </> } /> {/* Define a /verification route that handles email link result */} <Route path="/verification" element={ <ClerkLoaded> <EmailLinkVerification /> </ClerkLoaded> } /> </Routes> </ClerkProvider> </Router> ); } // Render the sign up form. // Collect user's email address and send an email link with which // they can sign up. function SignUpEmailLink() { const [emailAddress, setEmailAddress] = React.useState(""); const [expired, setExpired] = React.useState(false); const [verified, setVerified] = React.useState(false); const navigate = useNavigate(); const { signUp, isLoaded, setActive } = useSignUp(); if (!isLoaded) { return null; } const { startEmailLinkFlow, cancelEmailLinkFlow } = signUp.createEmailLinkFlow(); async function submit(e) { e.preventDefault(); setExpired(false); setVerified(false); // Start the sign up flow, by collecting // the user's email address. await signUp.create({ emailAddress }); // Start the email link flow. // Pass your app URL that users will be navigated // when they click the email link from their // email inbox. // su will hold the updated sign up object. const su = await startEmailLinkFlow({ redirectUrl: "https://your-app.domain.com/verification", }); // Check the verification result. const verification = su.verifications.emailAddress; if (verification.verifiedFromTheSameClient()) { setVerified(true); // If you're handling the verification result from // another route/component, you should return here. // See the <EmailLinkVerification/> component as an // example below. // If you want to complete the flow on this tab, // don't return. Check the sign up status instead. return; } else if (verification.status === "expired") { setExpired(true); } if (su.status === "complete") { // Sign up is complete, we have a session. // Navigate to the after sign up URL. setActive({ session: su.createdSessionId, beforeEmit: () => navigate("/after-sign-up-path"), }); return; } } if (expired) { return ( <div>Email link has expired</div> ); } if (verified) { return ( <div>Signed in on other tab</div> ); } return ( <form onSubmit={submit}> <input type="email" value={emailAddress} onChange={e => setEmailAddress(e.target.value)} /> <button type="submit"> Sign up with email link </button> </form> ); } // Handle email link verification results. This is // the final step in the email link flow. function EmailLinkVerification() { const [ verificationStatus, setVerificationStatus, ] = React.useState("loading"); const { handleEmailLinkVerification } = useClerk(); React.useEffect(() => { async function verify() { try { await handleEmailLinkVerification({ redirectUrl: "https://redirect-to-pending-sign-up", redirectUrlComplete: "https://redirect-when-sign-up-complete", }); // If we're not redirected at this point, it means // that the flow has completed on another device. setVerificationStatus("verified"); } catch (err) { // Verification has failed. let status = "failed"; if (isEmailLinkError(err) && err.code === EmailLinkErrorCode.Expired) { status = "expired"; } setVerificationStatus(status); } } verify(); }, []); if (verificationStatus === "loading") { return <div>Loading...</div> } if (verificationStatus === "failed") { return ( <div>Email link verification failed</div> ); } if (verificationStatus === "expired") { return ( <div>Email link expired</div> ); } return ( <div> Successfully signed up. Return to the original tab to continue. </div> ); } export default App;
const signUp = window.Clerk.client.signUp; const { startEmailLinkFlow, cancelEmailLinkFlow, } = signUp.createEmailLinkFlow(); const res = await startEmailLinkFlow({ // Pass your app URL that users will be navigated // when they click the email link from their // email inbox. redirectUrl: "https://redirect-from-email-email-link" }); if (res.status === "completed") { // sign up completed } else { // sign up still pending } // Cleanup cancelEmailLinkFlow();

Sign in using a custom flow (Clerk API)

Signing users into your application is probably the most popular use case for email links. Users enter their email address and then click on a link that's delivered to them via email message in order to sign in.

The sign-in process can be completed on the same or a different device. For example, users might enter their email address in their desktop browser, but click the sign-in email link from their mobile phone. The user's email address will still be verified and authentication will proceed.

Let's see all the steps involved in more detail.

  1. Initiate the sign-in process, by collecting the user's authentication identifier. It must be their email address.
  2. Start the email link verification flow. There are two parts to the flow:
  • Prepare a verification for the email address by sending an email with an email link to the user.
  • Wait until the email link is clicked. This is a polling behavior that can be canceled at any time.
  1. Handle the email link verification result accordingly. Note that the email link can be clicked on a different device/browser than the one which initiated the flow.
  • The verification was successful so you need to continue with the sign-in flow.
  • The verification failed or the email link has expired.

Clerk provides a highly flexible API that allows you to hook into any of the above steps, while abstracting away all the complexities of an email link based sign-in flow.

import React from "react"; import { useRouter } from "next/router"; import { EmailLinkErrorCode, isEmailLinkError, useClerk, useSignIn, } from "@clerk/nextjs"; // pages/sign-in.jsx // Render the sign in form. // Collect user's email address and send an email link with which // they can sign in. function SignIn() { const [emailAddress, setEmailAddress] = React.useState(""); const [expired, setExpired] = React.useState(false); const [verified, setVerified] = React.useState(false); const router = useRouter(); const { signIn, isLoaded, setActive } = useSignIn(); if (!isLoaded) { return null; } const { startEmailLinkFlow, cancelEmailLinkFlow } = signIn.createEmailLinkFlow(); async function submit(e) { e.preventDefault(); setExpired(false); setVerified(false); // Start the sign in flow, by collecting // the user's email address. const si = await signIn.create({ identifier: emailAddress }); const { emailAddressId } = si.supportedFirstFactors.find( ff => ff.strategy === "email_link" && ff.safeIdentifier === emailAddress ); // Start the email link flow. // Pass your app URL that users will be navigated // res will hold the updated sign in object. const res = await startEmailLinkFlow({ emailAddressId: emailAddressId, redirectUrl: "https://your-app.domain.com/verification", }); // Check the verification result. const verification = res.firstFactorVerification; if (verification.verifiedFromTheSameClient()) { setVerified(true); // If you're handling the verification result from // another route/component, you should return here. // See the <Verification/> component as an // example below. // If you want to complete the flow on this tab, // don't return. Simply check the sign in status. return; } else if (verification.status === "expired") { setExpired(true); } if (res.status === "complete") { setActive({ session: res.createdSessionId }) //Handle redirect return; } } if (expired) { return ( <div>Email link has expired</div> ); } if (verified) { return ( <div>Signed in on other tab</div> ); } return ( <form onSubmit={submit}> <input type="email" value={emailAddress} onChange={e => setEmailAddress(e.target.value)} /> <button type="submit"> Sign in with email link </button> </form> ); } // pages/verification.jsx // Handle email link verification results. This is // the final step in the email link flow. function Verification() { const [ verificationStatus, setVerificationStatus, ] = React.useState("loading"); const { handleEmailLinkVerification } = useClerk(); React.useEffect(() => { async function verify() { try { await handleEmailLinkVerification({ redirectUrl: "https://redirect-to-pending-sign-in-like-2fa", redirectUrlComplete: "https://redirect-when-sign-in-complete", }); // If we're not redirected at this point, it means // that the flow has completed on another device. setVerificationStatus("verified"); } catch (err) { // Verification has failed. let status = "failed"; if (isEmailLinkError(err) && err.code === EmailLinkErrorCode.Expired) { status = "expired"; } setVerificationStatus(status); } } verify(); }, []); if (verificationStatus === "loading") { return <div>Loading...</div> } if (verificationStatus === "failed") { return ( <div>Email link verification failed</div> ); } if (verificationStatus === "expired") { return ( <div>Email link expired</div> ); } return ( <div> Successfully signed in. Return to the original tab to continue. </div> ); }
import React from "react"; import { BrowserRouter as Router, Routes, Route, useNavigate, } from "react-router-dom"; import { ClerkProvider, ClerkLoaded, EmailLinkErrorCode, isEmailLinkError, UserButton, useClerk, useSignIn, } from "@clerk/clerk-react"; const publishableKey = process.env.REACT_APP_CLERK_PUBLISHABLE_KEY; function App() { return ( <Router> <ClerkProvider publishableKey={publishableKey}> <Routes> {/* Root path shows sign in page. */} <Route path="/" element={ <> <SignedOut> <SignInEmailLink /> </SignedOut> <SignedIn> <UserButton afterSignOutAllUrl="/" /> </SignedIn> </> } /> {/* Define a /verification route that handles email link result */} <Route path="/verification" element={ <ClerkLoaded> <EmailLinkVerification /> </ClerkLoaded> } /> </Routes> </ClerkProvider> </Router> ); } // Render the sign in form. // Collect user's email address and send an email link with which // they can sign in. function SignInEmailLink() { const [emailAddress, setEmailAddress] = React.useState(""); const [expired, setExpired] = React.useState(false); const [verified, setVerified] = React.useState(false); const navigate = useNavigate(); const { signIn, isLoaded, setActive } = useSignIn(); if (!isLoaded) { return null; } const { startEmailLinkFlow, cancelEmailLinkFlow } = signIn.createEmailLinkFlow(); async function submit(e) { e.preventDefault(); setExpired(false); setVerified(false); // Start the sign in flow, by collecting // the user's email address. const si = await signIn.create({ identifier: emailAddress }); const { emailAddressId } = si.supportedFirstFactors.find( ff => ff.strategy === "email_link" && ff.safeIdentifier === emailAddress ); // Start the email link flow. // Pass your app URL that users will be navigated // res will hold the updated sign in object. const res = await startEmailLinkFlow({ emailAddressId: emailAddressId, redirectUrl: "https://your-app.domain.com/verification", }); // Check the verification result. const verification = res.firstFactorVerification; if (verification.verifiedFromTheSameClient()) { setVerified(true); // If you're handling the verification result from // another route/component, you should return here. // See the <EmailLinkVerification/> component as an // example below. // If you want to complete the flow on this tab, // don't return. Simply check the sign in status. return; } else if (verification.status === "expired") { setExpired(true); } if (res.status === "complete") { // Sign in is complete, we have a session. // Navigate to the after sign in URL. setActive({ session: res.createdSessionId, beforeEmit: () => navigate("/after-sign-in-path"), }); return; } } if (expired) { return ( <div>Email link has expired</div> ); } if (verified) { return ( <div>Signed in on other tab</div> ); } return ( <form onSubmit={submit}> <input type="email" value={emailAddress} onChange={e => setEmailAddress(e.target.value)} /> <button type="submit"> Sign in with email link </button> </form> ); } // Handle email link verification results. This is // the final step in the email link flow. function EmailLinkVerification() { const [ verificationStatus, setVerificationStatus, ] = React.useState("loading"); const { handleEmailLinkVerification } = useClerk(); React.useEffect(() => { async function verify() { try { await handleEmailLinkVerification({ redirectUrl: "https://redirect-to-pending-sign-in-like-2fa", redirectUrlComplete: "https://redirect-when-sign-in-complete", }); // If we're not redirected at this point, it means // that the flow has completed on another device. setVerificationStatus("verified"); } catch (err) { // Verification has failed. let status = "failed"; if (isEmailLinkError(err) && err.code === EmailLinkErrorCode.Expired) { status = "expired"; } setVerificationStatus(status); } } verify(); }, []); if (verificationStatus === "loading") { return <div>Loading...</div> } if (verificationStatus === "failed") { return ( <div>Email link verification failed</div> ); } if (verificationStatus === "expired") { return ( <div>Email link expired</div> ); } return ( <div> Successfully signed in. Return to the original tab to continue. </div> ); } export default App;
const signIn = window.Clerk.client.signIn; const { startEmailLinkFlow, cancelEmailLinkFlow, } = signIn.createEmailLinkFlow(); const { email_address_id } = signIn.supportedFirstFactors.find( ff => ff.strategy === "email_link" && ff.safe_identifier === "your-users-email" ); // Pass your app URL that users will be navigated // when they click the email link from their // email inbox. const res = await startEmailLinkFlow({ emailAddressId, redirectUrl: "https://redirect-from-email-email-link", }); if (res.status === "completed") { // sign in completed } else { // sign in still pending } // Cleanup cancelEmailLinkFlow();

Set up email address verification using a custom flow (Clerk API)

Email links can also provide a nice user experience for verifying email addresses that users add when updating their profiles. The flow is similar to one-time code verification, but users need only click on the email link; there's no need to return to your app.

  1. Collect the user's email address.
  2. Start the email link verification flow. There are two parts to the flow:
  • Prepare a verification for the email address by sending an email with an email link to the user.
  • Wait until the email link is clicked. This is a polling behavior that can be canceled at any time.
  1. Handle the email link verification result accordingly. Note that the email link can be clicked on a different device/browser than the one which initiated the flow.
  • The verification was successful.
  • The verification failed or the email link has expired.

Clerk provides a highly flexible API that allows you to hook into any of the above steps while abstracting away all the complexities of an email link-based email address verification.

import React from "react"; import { useUser, useEmailLink } from "@clerk/nextjs"; // A page where users can add a new email address. function NewEmailPage() { const [email, setEmail] = React.useState(''); const [emailAddress, setEmailAddress] = React.useState(null); const [verified, setVerified] = React.useState(false); const { user } = useUser(); async function submit(e) { e.preventDefault(); const res = await user.createEmailAddress({ email }); setEmailAddress(res); } if (emailAddress && !verified) { return ( <VerifyWithEmailLink emailAddress={emailAddress} onVerify={() => setVerified(true)} /> ); } return ( <form onSubmit={submit}> <input type="email" value={email} onChange={e => setEmail(e.target.value)} /> </form> ); } // A page which verifies email addresses with email links. function VerifyWithEmailLink({ emailAddress, onVerify, }) { const { startEmailLinkFlow } = useEmailLink(emailAddress); React.useEffect(() => { verify(); }, []); async function verify() { // Start the email link flow. // Pass your app URL that users will be navigated // when they click the email link from their // email inbox. const res = await startEmailLinkFlow({ redirectUrl: "https://redirect-from-email-email-link", }); // res will hold the updated EmailAddress object. if (res.verification.status === "verified") { onVerify(); } else { // act accordingly } } return ( <div> Waiting for verification... </div> ); }
import React from "react"; import { useUser, useEmailLink } from "@clerk/clerk-react"; // A page where users can add a new email address. function NewEmailPage() { const [email, setEmail] = React.useState(''); const [emailAddress, setEmailAddress] = React.useState(null); const [verified, setVerified] = React.useState(false); const { user } = useUser(); async function submit(e) { e.preventDefault(); const res = await user.createEmailAddress({ email }); setEmailAddress(res); } if (emailAddress && !verified) { return ( <VerifyWithEmailLink emailAddress={emailAddress} onVerify={() => setVerified(true)} /> ); } return ( <form onSubmit={submit}> <input type="email" value={email} onChange={e => setEmail(e.target.value)} /> </form> ); } // A page which verifies email addresses with email links. function VerifyWithEmailLink({ emailAddress, onVerify, }) { const { startEmailLinkFlow } = useEmailLink(emailAddress); React.useEffect(() => { verify(); }, []); async function verify() { // Start the email link flow. // Pass your app URL that users will be navigated // when they click the email link from their // email inbox. const res = await startEmailLinkFlow({ redirectUrl: "https://redirect-from-email-email-link", }); // res will hold the updated EmailAddress object. if (res.verification.status === "verified") { onVerify(); } else { // act accordingly } } return ( <div> Waiting for verification... </div> ); }
const user = window.Clerk.user; const emailAddress = user.emailAddresses[0]; const { startEmailLinkFlow, cancelEmailLinkFlow, } = emailAddress.createEmailLinkFlow(); // Pass your app URL that users will be navigated // when they click the email link from their // email inbox. const res = await startEmailLinkFlow({ redirectUrl: "https://redirect-from-email-email-link", }); if (res.verification.status === "verified") { // email address was verified } else { // email address wasn't verified } // Cleanup cancelEmailLinkFlow();

Last updated on April 19, 2024

What did you think of this content?

Clerk © 2024