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.
To use SMS OTP as an authentication strategy, you first need to enable it in the Clerk Dashboard.
Navigate to the Clerk Dashboard .
In the navigation sidebar, go to User & Authentication > Email, phone, username .
Ensure that only Phone number is required. If Email address or Username are enabled, ensure they are not required. Use the settings icon to check if a setting is required or optional. If you would like to require Username , you must collect the username and pass it to the create()
method in your custom flow. For this guide, if you would like to use email OTP instead, require the Email address option instead of the Phone number option.
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 instead, enable the Email verification code strategy instead of the SMS verification code strategy.
To sign up a user using an OTP, you must:
Initiate the sign-up process by collecting the user's identifier, which for this example is a phone number.
Prepare the verification, which sends a one-time code to the given identifier.
Attempt to complete the verification with the code the user provides.
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.
app /sign-up /[[...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 ()
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 >
</>
)
}
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 >
main.js import { Clerk } from '@clerk/clerk-js'
const pubKey = import . meta . env . VITE_CLERK_PUBLISHABLE_KEY
const clerk = new Clerk (pubKey)
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)
}
})
}
SMSOTPSignUpView.swift import SwiftUI
import ClerkSDK
struct SMSOTPSignUpView : View {
@State private var phoneNumber = ""
@State private var code = ""
@State private var isVerifying = false
var body: some View {
if isVerifying {
TextField ( "Enter your verification code" , text : $code )
Button ( "Verify" ) {
Task { await verify ( code : code ) }
}
} else {
TextField ( "Enter phone number" , text : $phoneNumber )
Button ( "Continue" ) {
Task { await submit ( phoneNumber : phoneNumber ) }
}
}
}
}
extension SMSOTPSignUpView {
func submit ( phoneNumber : String ) async {
do {
// Start the sign-up process using the phone number method.
let signUp = try await SignUp. create ( strategy : .standard ( phoneNumber : phoneNumber ))
// Start the verification - a SMS message will be sent to the
// number with a one-time code.
try await signUp. prepareVerification ( strategy : .phoneCode )
// Set isVerifying to true to display second form and capture the OTP code.
isVerifying = true
} catch {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
dump ( error )
}
}
func verify ( code : String ) async {
do {
// Access the in progress sign up stored on the client object.
guard let inProgressSignUp = Clerk.shared.client ? .signUp else { return }
// Use the code provided by the user and attempt verification.
let signUp = try await inProgressSignUp. attemptVerification ( .phoneCode ( code : code ))
switch signUp.status {
case .complete :
// If verification was completed, navigate the user as needed.
dump ( Clerk.shared.session )
default:
// If the status is not complete, check why. User may need to
// complete further steps.
dump ( signUp.status )
}
} catch {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
dump ( error )
}
}
}
To create a sign-up flow for email OTP, use the prepareEmailAddressVerification
and attemptEmailAddressVerification
. These helpers work the same way as their phone number counterparts do in the previous example. You can find all available methods in the SignUp
object documentation.
To authenticate a user with an OTP, you must:
Initiate the sign-in process by creating a SignIn
using the identifier provided, which for this example is a phone number.
Prepare the first factor verification.
Attempt verification with the code the user provides.
If the attempt 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.
app /sign-in /[[...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 >
</>
)
}
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 >
main.js import { Clerk } from '@clerk/clerk-js'
const pubKey = import . meta . env . VITE_CLERK_PUBLISHABLE_KEY
const clerk = new Clerk (pubKey)
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)
}
})
}
SMSOTPSignInView.swift import SwiftUI
import ClerkSDK
struct SMSOTPSignInView : View {
@State private var phoneNumber = ""
@State private var code = ""
@State private var isVerifying = false
var body: some View {
if isVerifying {
TextField ( "Enter your verification code" , text : $code )
Button ( "Verify" ) {
Task { await verify ( code : code ) }
}
} else {
TextField ( "Enter phone number" , text : $phoneNumber )
Button ( "Continue" ) {
Task { await submit ( phoneNumber : phoneNumber ) }
}
}
}
}
extension SMSOTPSignInView {
func submit ( phoneNumber : String ) async {
do {
// Start the sign-in process using the phone number method.
let signIn = try await SignIn. create ( strategy : .identifier ( phoneNumber ))
// Send the OTP code to the user.
try await signIn. prepareFirstFactor ( for : .phoneCode )
// Set isVerifying to true to display second form
// and capture the OTP code.
isVerifying = true
} catch {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
dump ( error )
}
}
func verify ( code : String ) async {
do {
// Access the in progress sign in stored on the client object.
guard let inProgressSignIn = Clerk.shared.client ? .signIn else { return }
// Use the code provided by the user and attempt verification.
let signIn = try await inProgressSignIn. attemptFirstFactor ( for : .phoneCode ( code : code ))
switch signIn.status {
case .complete :
// If verification was completed, navigate the user as needed.
dump ( Clerk.shared.session )
default:
// If the status is not complete, check why. User may need to
// complete further steps.
dump ( signIn.status )
}
} catch {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
dump ( error )
}
}
}
To create a sign-in flow for email OTP, pass the value email_code
as the first factor strategy. You can find all available methods in the SignIn
object documentation.