SMS OTP form best practices

It's common to ask a user to provide a one-time password (OTP) to confirm their identity by sending an SMS. Some use cases for SMS OTP include:

  • Two-factor authentication. In addition to username and password, SMS OTP can be used as a strong signal that the account is owned by the person who received the SMS OTP.
  • Phone number verification. Some services use a phone number as the user's primary identifier. In such services, users can enter their phone number and the OTP received with SMS to prove their identity. Sometimes it's combined with a PIN to constitute a two-factor authentication.
  • Account recovery. When a user loses access to their account, there needs to be a way to recover it. Sending an email to their registered email address or an SMS OTP to their phone number are common account recovery methods.
  • Payment confirmation In payment systems, some banks or credit card issuers request additional authentication from the payer for security reasons. SMS OTP is commonly used for that purpose.

Keep reading for best practices on building SMS OTP forms for these use cases.

Checklist

To provide the best user experience with the SMS OTP, follow these steps:

  • Use the <input> element with:
    • type="text"
    • inputmode="numeric"
    • autocomplete="one-time-code"
  • Use @BOUND_DOMAIN #OTP_CODE as the last line of the OTP SMS message.
  • Use the WebOTP API.

Use the <input> element

Using a form with an <input> element is the most important best practice you can follow because it works in all browsers. Even if other suggestions from this post don't work in some browser, the user will still be able to enter and submit the OTP manually.

<form action="/verify-otp" method="POST">
  <input type="text"
      inputmode="numeric"
      autocomplete="one-time-code"
      pattern="\d{6}"
      required>
</form>

The following are a few ideas to ensure an input field gets the best out of browser functionality.

type="text"

Since OTPs are usually five or six digit numbers, using type="number" for an input field might seem intuitive because it changes the mobile keyboard to numbers only. This is not recommended because the browser expects an input field to be a countable number rather than a sequence of multiple numbers, which can cause unexpected behavior. Using type="number" causes up and down buttons to be displayed beside the input field; pressing these buttons increments or decrements the number and may remove preceding zeros.

Use type="text" instead. This won't turn the mobile keyboard into numbers only, but that is fine because the next tip for using inputmode="numeric" does that job.

inputmode="numeric"

Use inputmode="numeric" to change the mobile keyboard to numbers only.

Some websites use type="tel" for OTP input fields since it also turns the mobile keyboard to numbers only (including * and #) when focused. This hack was used in the past when inputmode="numeric" wasn't widely supported. Since Firefox started supporting inputmode="numeric", there's no need to use the semantically incorrect type="tel" hack.

autocomplete="one-time-code"

autocomplete attribute lets developers specify what permission the browser has to provide autocomplete assistance and informs the browser about the type of information expected in the field.

With autocomplete="one-time-code" whenever a user receives an SMS message while a form is open, the operating system will parse the OTP in the SMS heuristically and the keyboard will suggest the OTP for the user to enter. It works only on Safari 12 and later on iOS, iPadOS, and macOS, but we strongly recommend using it, because it's better way to improve the SMS OTP experience on those platforms.

autocomplete="one-time-code" in action.

autocomplete="one-time-code" improves the user experience, but there's more you can do by ensuring that the SMS message complies with the origin-bound message format.

Optional attributes

Optional attributes include:

Read our sign-in form best practices for more advice.

Format the SMS text

Enhance the user experience of entering an OTP by aligning with the origin-bound one-time codes delivered by SMS specification.

At its core, the format rule is as follows: Finish the SMS message with the receiver domain preceded with @, and the OTP preceded with #.

For example:

Your OTP is 123456

@web-otp.glitch.me #123456

The standard format for OTP messages makes extraction easier and more reliable. Associating OTP codes with websites makes it harder to trick users into providing a code to malicious sites.

Precise formatting rules

The precise rules are:

  • The message begins with (optional) human-readable text that contains a four to ten character alphanumeric string with at least one number, leaving the last line for the URL and the OTP.
  • The domain part of the URL of the website that invoked the API must be preceded by @.
  • The URL must contain a #, followed by the OTP. The number of characters must be 140 or less.

Using this format provides a couple of benefits:

  • The OTP will be bound to the domain. If the user is on domains other than the one specified in the SMS message, the OTP suggestion won't appear. This also mitigates the risk of phishing attacks and potential account hijacks.
  • Browser will now be able to reliably extract the OTP without depending on mysterious and flaky heuristics.

When a website uses autocomplete="one-time-code", Safari with iOS 14 or later will suggest the OTP following these rules.

This SMS message format also benefits browsers other than Safari. Chrome, Opera, and Vivaldi on Android also support the origin-bound one-time codes rule with the WebOTP API, though not through autocomplete="one-time-code".

Use the WebOTP API

The WebOTP API provides access to the OTP received in an SMS message. By calling navigator.credentials.get() with otp type (OTPCredential) where transport includes sms, the website will wait for an SMS that complies with the origin-bound one-time codes to be delivered and granted access by the user. Once the OTP is passed to JavaScript, the website can use it in a form or POST it directly to the server.

navigator.credentials.get({
  otp: {transport:['sms']}
})
.then(otp => input.value = otp.code);
WebOTP API in action.

Learn how to use the WebOTP API in detail in Verify phone numbers on the web with the WebOTP API or copy and paste the following snippet. Make sure to set an action and method attribute in your <form>.

// Feature detection
if ('OTPCredential' in window) {
  window.addEventListener('DOMContentLoaded', e => {
    const input = document.querySelector('input[autocomplete="one-time-code"]');
    if (!input) return;
    // Cancel the WebOTP API if the form is submitted manually.
    const ac = new AbortController();
    const form = input.closest('form');
    if (form) {
      form.addEventListener('submit', e => {
        // Cancel the WebOTP API.
        ac.abort();
      });
    }
    // Invoke the WebOTP API
    navigator.credentials.get({
      otp: { transport:['sms'] },
      signal: ac.signal
    }).then(otp => {
      input.value = otp.code;
      // Automatically submit the form when an OTP is obtained.
      if (form) form.submit();
    }).catch(err => {
      console.log(err);
    });
  });
}