Docs

Build a custom email or SMS OTP authentication flow

Warning

This guide is for users who want to build a custom user interface using the Clerk API. To authenticate users with one-time passwords using a prebuilt UI, you should use Clerk's Account Portal pages or prebuilt components.

Clerk supports passwordless authentication, which lets users sign in and sign up without having to remember a password. Instead, users receive a one-time password (OTP), also known as a one-time code, via email or SMS, which they can use to authenticate themselves.

This guide will walk you through how to build a custom SMS OTP sign-up and sign-in flow. The process for using email OTP is similar, and the differences will be highlighted throughout.

Enable SMS OTP

To use SMS OTP as an authentication strategy, you first need to enable it in the Clerk Dashboard.

  1. Navigate to the Clerk Dashboard.
  2. Go to User & Authentication > Email, Phone, Username in the sidebar menu.
  3. In the Contact information section, ensure only Phone number is required. Use the settings cog icon to check if a setting is required or optional. If you would like to use email OTP instead, you can enable the Email address option instead of the phone number option.
  4. In the Username section, ensure that Username is not required, or else the create() method will require a username to be passed in the params. If you would like to use usernames, you must handle collecting the username in your custom flow.
  5. In the Authentication strategies section of this page, ensure SMS verification code is enabled. Ensure Password is toggled off, as you are prioritizing passwordless, SMS OTP-only authentication in this guide. If you would like to use email OTP, you can enable the Email verification code strategy instead of the SMS strategy.

Create the OTP sign-up flow

To sign up a user using an OTP, you must:

  1. Initiate the sign-up process by collecting the user's identifier, which for this example is a phone number.
  2. Prepare the verification, which sends a one-time code to the given identifier.
  3. Attempt to complete the verification by supplying the one-time code.
  4. If the verification is successful, set the newly created session as the active session.

This example is written for Next.js App Router but can be adapted to any React meta framework, such as Remix or Gatsby.

app/sign-up/page.tsx
'use client';

import * as React from 'react';
import { useSignUp } from '@clerk/nextjs';
import { useRouter } from 'next/navigation';

export default function Page() {
  const { isLoaded, signUp, setActive } = useSignUp();
  const [verifying, setVerifying] = React.useState(false);
  const [phone, setPhone] = React.useState('');
  const [code, setCode] = React.useState('');
  const router = useRouter();

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();

    if (!isLoaded && !signUp) return null;

    try {
      // Start the sign-up process using the phone number method
      await signUp.create({
        phoneNumber: phone,
      });

      // Start the verification - a SMS message will be sent to the
      // number with a one-time code
      await signUp.preparePhoneNumberVerification();

      // Set verifying to true to display second form and capture the OTP code
      setVerifying(true);
    } catch (err) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error('Error:', JSON.stringify(err, null, 2));
    }
  }

  async function handleVerification(e: React.FormEvent) {
    e.preventDefault();
    setError('');

    if (!isLoaded && !signUp) return null;

    try {
      // Use the code provided by the user and attempt verification
      const signInAttempt = await signUp.attemptPhoneNumberVerification({
        code,
      });

      // If verification was completed, set the session to active
      // and redirect the user
      if (signInAttempt.status === 'complete') {
        await setActive({ session: signInAttempt.createdSessionId });

        router.push('/');
      } else {
        // If the status is not complete, check why. User may need to
        // complete further steps.
        console.error(signInAttempt);
      }
    } catch (err) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error('Error:', JSON.stringify(err, null, 2));
    }
  }

  if (verifying) {
    return (
      <>
        <h1>Verify your phone number</h1>
        <form onSubmit={handleVerification}>
          <label htmlFor="code">Enter your verification code</label>
          <input
            value={code}
            id="code"
            name="code"
            onChange={(e) => setCode(e.target.value)}
          />
          <button type="submit">Verify</button>
        </form>
      </>
    );
  }

  return (
    <>
      <h1>Sign up</h1>
      <form onSubmit={handleSubmit}>
        <label htmlFor="phone">Enter phone number</label>
        <input
          value={phone}
          id="phone"
          name="phone"
          type="tel"
          onChange={(e) => setPhone(e.target.value)}
        />
        <button type="submit">Continue</button>
      </form>
    </>
  );
}

For the following example, your HTML file should look like this:

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Clerk + JavaScript App</title>
  </head>
  <body>
    <div id="signed-in"></div>

    <div id="sign-up">
      <h2>Sign up</h2>
      <form id="sign-up-form">
        <label for="phone">Enter phone number</label>
        <input type="tel" name="phone" id="sign-up-phone" />
        <button type="submit">Continue</button>
      </form>
    </div>

    <form id="verifying" hidden>
      <h2>Verify your phone number</h2>
      <label for="code">Enter your verification code</label>
      <input id="code" name="code" />
      <button type="submit" id="verify-button">Verify</button>
    </form>

    <script
      type="module"
      src="/src/main.js"
      async
      crossorigin="anonymous"
    ></script>
  </body>
</html>

And your JavaScript file should look like this:

main.js
import { Clerk } from '@clerk/clerk-js';

// Initialize Clerk with your Clerk publishable key
const clerk = new Clerk('YOUR_PUBLISHABLE_KEY');
await clerk.load();

if (clerk.user) {
  // Mount user button component
  document.getElementById("signed-in").innerHTML = `
    <div id="user-button"></div>
  `;

  const userbuttonDiv = document.getElementById("user-button");

  clerk.mountUserButton(userbuttonDiv);
} else {
  // Handle the sign-up form
  document.getElementById("sign-up-form")
    .addEventListener("submit", async (e) => {
      e.preventDefault();

      const formData = new FormData(e.target);
      const phoneNumber = formData.get("phone");

      try {
        // Start the sign-up process using the phone number method
        await clerk.client.signUp.create({ phoneNumber });
        await clerk.client.signUp.preparePhoneNumberVerification();
        // Hide sign-up form
        document.getElementById("sign-up").setAttribute("hidden", "");
        // Show verification form
        document.getElementById("verifying").removeAttribute("hidden");
      } catch (error) {
        // See https://clerk.com/docs/custom-flows/error-handling
        // for more info on error handling
        console.error(error);
      }
    });

  // Handle the verification form
  document.getElementById("verifying").addEventListener("submit", async (e) => {
    const formData = new FormData(e.target);
    const code = formData.get("code");

    try {
      // Verify the phone number
      const verify = await clerk.client.signUp.attemptPhoneNumberVerification({
        code,
      });

      // Now that the user is created, set the session to active.
      await clerk.setActive({ session: verify.createdSessionId });
    } catch (error) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error(error);
    }
  });
}
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Clerk + JavaScript App</title>
  </head>
  <body>
    <div id="signed-in"></div>

    <div id="sign-up">
      <h2>Sign up</h2>
      <form id="sign-up-form">
        <label for="phone">Enter phone number</label>
        <input type="tel" name="phone" id="sign-up-phone" />
        <button type="submit">Continue</button>
      </form>
    </div>

    <form id="verifying" hidden>
      <h2>Verify your phone number</h2>
      <label for="code">Enter your verification code</label>
      <input id="code" name="code" />
      <button type="submit" id="verify-button">Verify</button>
    </form>

    <script
      async
      crossorigin="anonymous"
      data-clerk-publishable-key="YOUR_PUBLISHABLE_KEY"
      src="https://YOUR_FRONTEND_API_URL/npm/@clerk/clerk-js@latest/dist/clerk.browser.js"
      type="text/javascript"
    ></script>

    <script>
      window.addEventListener("load", async function () {
        await Clerk.load();

        if (Clerk.user) {
          // Mount user button component
          document.getElementById("signed-in").innerHTML = `
          <div id="user-button"></div>
        `;

          const userbuttonDiv = document.getElementById("user-button");

          Clerk.mountUserButton(userbuttonDiv);
        } else {
          // Handle the sign-up form
          document
            .getElementById("sign-up-form")
            .addEventListener("submit", async (e) => {
              e.preventDefault();

              const formData = new FormData(e.target);
              const phoneNumber = formData.get("phone");

              try {
                // Start the sign-up process using the phone number method
                await Clerk.client.signUp.create({ phoneNumber });
                await Clerk.client.signUp.preparePhoneNumberVerification();
                // Hide sign-up form
                document.getElementById("sign-up").setAttribute("hidden", "");
                // Show verification form
                document.getElementById("verifying").removeAttribute("hidden");
              } catch (error) {
                // See https://clerk.com/docs/custom-flows/error-handling
                // for more info on error handling
                console.error(error);
              }
            });

          // Handle the verification form
          document
            .getElementById("verifying")
            .addEventListener("submit", async (e) => {
              const formData = new FormData(e.target);
              const code = formData.get("code");

              try {
                // Verify the phone number
                const verify =
                  await Clerk.client.signUp.attemptPhoneNumberVerification({
                    code,
                  });

                // Now that the user is created, set the session to active.
                await Clerk.setActive({ session: verify.createdSessionId });
              } catch (error) {
                // See https://clerk.com/docs/custom-flows/error-handling
                // for more info on error handling
                console.error(error);
              }
            });
        }
      });
    </script>
  </body>
</html>

To create a sign-up flow for email OTP, you can use the prepareEmailAddressVerification and attemptEmailAddressVerification. These helpers work the same way as their phone number counterparts do in the above example. You can find all available methods in the SignUp object documentation.

Create the OTP sign-in flow

To authenticate a user with an OTP, you must:

  1. Initiate the sign-in process by collecting the user's authentication identifier, which for this example is a phone number.
  2. Prepare the first factor verification.
  3. Attempt to complete the first factor verification.
  4. If the verification is successful, set the newly created session as the active session.

This example is written for Next.js App Router but can be adapted to any React meta framework, such as Remix or Gatsby.

app/sign-in/page.tsx
'use client';

import * as React from 'react';
import { useSignIn } from '@clerk/nextjs';
import { PhoneCodeFactor, SignInFirstFactor } from '@clerk/types';
import { useRouter } from 'next/navigation';

export default function Page() {
  const { isLoaded, signIn, setActive } = useSignIn();
  const [verifying, setVerifying] = React.useState(false);
  const [phone, setPhone] = React.useState('');
  const [code, setCode] = React.useState('');
  const router = useRouter();

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();

    if (!isLoaded && !signIn) return null;

    try {
      // Start the sign-in process using the phone number method
      const { supportedFirstFactors } = await signIn.create({
        identifier: phone,
      });

      // Filter the returned array to find the 'phone_code' entry
      const isPhoneCodeFactor = (
        factor: SignInFirstFactor
      ): factor is PhoneCodeFactor => {
        return factor.strategy === 'phone_code';
      };
      const phoneCodeFactor = supportedFirstFactors?.find(isPhoneCodeFactor);

      if (phoneCodeFactor) {
        // Grab the phoneNumberId
        const { phoneNumberId } = phoneCodeFactor;

        // Send the OTP code to the user
        await signIn.prepareFirstFactor({
          strategy: 'phone_code',
          phoneNumberId,
        });

        // Set verifying to true to display second form
        // and capture the OTP code
        setVerifying(true);
      }
    } catch (err) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error('Error:', JSON.stringify(err, null, 2));
    }
  }

  async function handleVerification(e: React.FormEvent) {
    e.preventDefault();

    if (!isLoaded && !signIn) return null;

    try {
      // Use the code provided by the user and attempt verification
      const signInAttempt = await signIn.attemptFirstFactor({
        strategy: 'phone_code',
        code,
      });

      // If verification was completed, set the session to active
      // and redirect the user
      if (signInAttempt.status === 'complete') {
        await setActive({ session: signInAttempt.createdSessionId });

        router.push('/');
      } else {
        // If the status is not complete, check why. User may need to
        // complete further steps.
        console.error(signInAttempt);
      }
    } catch (err) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error('Error:', JSON.stringify(err, null, 2));
    }
  }

  if (verifying) {
    return (
      <>
        <h1>Verify your phone number</h1>
        <form onSubmit={handleVerification}>
          <label htmlFor="code">Enter your verification code</label>
          <input
            value={code}
            id="code"
            name="code"
            onChange={(e) => setCode(e.target.value)}
          />
          <button type="submit">Verify</button>
        </form>
      </>
    );
  }

  return (
    <>
      <h1>Sign in</h1>
      <form onSubmit={handleSubmit}>
        <label htmlFor="phone">Enter phone number</label>
        <input
          value={phone}
          id="phone"
          name="phone"
          type="tel"
          onChange={(e) => setPhone(e.target.value)}
        />
        <button type="submit">Continue</button>
      </form>
    </>
  );
}

For the following example, your HTML file should look like this:

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Clerk + JavaScript App</title>
  </head>
  <body>
    <div id="signed-in"></div>

    <div id="sign-in">
      <h2>Sign in</h2>
      <form id="sign-in-form">
        <label for="phone">Enter phone number</label>
        <input type="tel" name="phone" id="sign-in-phone" />
        <button type="submit">Continue</button>
      </form>
    </div>

    <form id="verifying" hidden>
      <h2>Verify your phone number</h2>
      <label for="code">Enter your verification code</label>
      <input id="code" name="code" />
      <button type="submit" id="verify-button">Verify</button>
    </form>

    <script
      type="module"
      src="/src/main.js"
      async
      crossorigin="anonymous"
    ></script>
  </body>
</html>

And your JavaScript file should look like this:

main.js
import { Clerk } from '@clerk/clerk-js';

// Initialize Clerk with your Clerk publishable key
const clerk = new Clerk('YOUR_PUBLISHABLE_KEY');
await clerk.load();

if (clerk.user) {
  // Mount user button component
  document.getElementById('signed-in').innerHTML = `
    <div id="user-button"></div>
  `;

  const userbuttonDiv = document.getElementById('user-button');

  clerk.mountUserButton(userbuttonDiv);
} else {
  // Handle the sign-in form
  document
    .getElementById('sign-in-form')
    .addEventListener('submit', async (e) => {
      e.preventDefault();

      const formData = new FormData(e.target);
      const phone = formData.get('phone');

      try {
        // Start the sign-in process using the user's identifier.
        // In this case, it's their phone number.
        const { supportedFirstFactors } = await clerk.client.signIn.create({
          identifier: phone,
        });

        // Find the phoneNumberId from all the available first factors for the current sign-in
        const firstPhoneFactor = supportedFirstFactors.find((factor) => {
          return factor.strategy === 'phone_code';
        });

        const { phoneNumberId } = firstPhoneFactor;

        // Prepare first factor verification, specifying
        // the phone code strategy.
        await clerk.client.signIn.prepareFirstFactor({
          strategy: 'phone_code',
          phoneNumberId,
        });

        // Hide sign-in form
        document.getElementById('sign-in').setAttribute('hidden', '');
        // Show verification form
        document.getElementById('verifying').removeAttribute('hidden');
      } catch (error) {
        // See https://clerk.com/docs/custom-flows/error-handling
        // for more info on error handling
        console.error(error);
      }
    });

  // Handle the verification form
  document.getElementById('verifying').addEventListener('submit', async (e) => {
    const formData = new FormData(e.target);
    const code = formData.get('code');

    try {
      // Verify the phone number
      const verify = await clerk.client.signIn.attemptFirstFactor({
        strategy: 'phone_code',
        code,
      });

      // Now that the user is created, set the session to active.
      await clerk.setActive({ session: verify.createdSessionId });
    } catch (error) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error(error);
    }
  });
}
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Clerk + JavaScript App</title>
  </head>
  <body>
    <div id="signed-in"></div>

    <div id="sign-in">
      <h2>Sign in</h2>
      <form id="sign-in-form">
        <label for="phone">Enter phone number</label>
        <input type="tel" name="phone" id="sign-in-phone" />
        <button type="submit">Continue</button>
      </form>
    </div>

    <form id="verifying" hidden>
      <h2>Verify your phone number</h2>
      <label for="code">Enter your verification code</label>
      <input id="code" name="code" />
      <button type="submit" id="verify-button">Verify</button>
    </form>

    <script
      async
      crossorigin="anonymous"
      data-clerk-publishable-key="YOUR_PUBLISHABLE_KEY"
      src="https://YOUR_FRONTEND_API_URL/npm/@clerk/clerk-js@latest/dist/clerk.browser.js"
      type="text/javascript"
    ></script>

    <script>
      window.addEventListener('load', async function () {
        await Clerk.load();

        if (Clerk.user) {
          // Mount user button component
          document.getElementById('signed-in').innerHTML = `
            <div id="user-button"></div>
          `;

          const userbuttonDiv = document.getElementById('user-button');

          Clerk.mountUserButton(userbuttonDiv);
        } else {
          // Handle the sign-in form
          document.getElementById('sign-in-form')
            .addEventListener('submit', async (e) => {
              e.preventDefault();

              const formData = new FormData(e.target);
              const phone = formData.get('phone');

              try {
                // Start the sign-in process using the user's identifier.
                // In this case, it's their phone number.
                const { supportedFirstFactors } =
                  await Clerk.client.signIn.create({
                    identifier: phone,
                  });

                // Find the phoneNumberId from all the available first factors for the current sign-in
                const firstPhoneFactor = supportedFirstFactors.find(
                  (factor) => {
                    return factor.strategy === 'phone_code';
                  }
                );

                const { phoneNumberId } = firstPhoneFactor;

                // Prepare first factor verification, specifying
                // the phone code strategy.
                await Clerk.client.signIn.prepareFirstFactor({
                  strategy: 'phone_code',
                  phoneNumberId,
                });

                // Hide sign-in form
                document.getElementById('sign-in').setAttribute('hidden', '');
                // Show verification form
                document.getElementById('verifying').removeAttribute('hidden');

              } catch (error) {
                // See https://clerk.com/docs/custom-flows/error-handling
                // for more info on error handling
                console.error(error);
              }
            });

          // Handle the verification form
          document.getElementById('verifying')
            .addEventListener('submit', async (e) => {
              const formData = new FormData(e.target);
              const code = formData.get('code');

              try {
                // Verify the phone number
                const verify = await Clerk.client.signIn.attemptFirstFactor({
                  strategy: 'phone_code',
                  code,
                });

                // Now that the user is created, set the session to active.
                await Clerk.setActive({ session: verify.createdSessionId });
              } catch (error) {
                // See https://clerk.com/docs/custom-flows/error-handling
                // for more info on error handling
                console.error(error);
              }
            });
        }
      });
    </script>
  </body>
</html>

You can also achieve passwordless sign-ins with an email address. Simply pass the value email_code as the first factor strategy. Just make sure you've collected the user's email address first. You can find all available methods in the SignIn object documentation.

Feedback

What did you think of this content?