Auto Web OTP in React: An Honest Developer's Guide
Tired of users fumbling with OTP codes? Learn to implement the Web OTP API in React for a seamless, one-tap login. An honest developer's guide to the code & gotchas.
Alex Carter
Senior Frontend Engineer specializing in React, performance, and modern web APIs.
Seamless Logins: A Developer's Guide to Auto Web OTP in React
Let's be real: nobody enjoys fumbling between their SMS app and their browser to copy a one-time password (OTP). It's a clunky, error-prone dance we force on users in the name of security. But what if we could make it disappear? What if the browser could just... grab the code for us?
That's exactly what the Web OTP API promises. It’s a simple, elegant solution to a common UX problem. In this guide, we'll cut through the fluff and get straight to implementing it in a React application. We'll cover the basics, build a reusable hook, and—most importantly—talk honestly about the gotchas and limitations you'll face in the real world.
What is the Web OTP API, Really?
The Web OTP API is a browser standard that allows your web application to programmatically read an OTP from an SMS message, with the user's permission. The goal is to streamline the verification process by removing the need for manual copy-pasting.
Here’s the flow:
- Your React app calls the API, telling the browser it's expecting an OTP via SMS.
- The browser starts listening for incoming SMS messages that follow a specific format.
- When a correctly formatted SMS arrives, the browser shows a prompt asking the user for permission to let your app read it.
- If the user clicks "Allow," the browser hands the OTP code directly to your JavaScript code.
The result? A one-tap verification flow. It’s a massive user experience win.
The Core Requirements (The "Catch")
This is where the "honest developer" part comes in. The magic doesn't work just anywhere. You need to meet a few strict conditions:
- HTTPS Only: Like most modern web APIs that handle sensitive info, this only works on secure origins (
https://). - A Specifically Formatted SMS: This is the big one. Your backend service can't just send any old text message. The SMS must include your website's domain, preceded by
@and followed by the OTP code, preceded by#.
The SMS body must contain a line that looks like this:
Your verification code is 123456.
@your-app.com #123456
Breaking it down:
@your-app.com: This binds the message to your website's domain. The browser won't read it for any other site.#123456: This is the OTP itself. The API looks for the alphanumeric code immediately following the hash symbol.
If your SMS doesn't follow this format, the API will do nothing. It will fail silently. So, get friendly with your backend team!
A Simple Implementation in a React Component
Let's get our hands dirty. The easiest way to start is inside a useEffect hook in your verification component.
First, you'll want to check if the API is even available in the user's browser.
if ('OTPCredential' in window) {
// API is available!
}
Now, let's put it all together in a component. We'll use an AbortController to clean up the API call if the component unmounts, which is a crucial best practice in React.
import React, { useEffect, useState } from 'react';
function OtpForm() {
const [otp, setOtp] = useState('');
useEffect(() => {
// Check if the Web OTP API is supported
if (!('OTPCredential' in window)) {
console.log('Web OTP API is not supported.');
return;
}
console.log('Web OTP API is supported, listening for OTP...');
// Create a new AbortController instance for this effect
const abortController = new AbortController();
const listenForOtp = async () => {
try {
const otpCredential = await navigator.credentials.get({
otp: { transport: ['sms'] },
signal: abortController.signal,
});
console.log('OTP received:', otpCredential.code);
setOtp(otpCredential.code);
// You would typically auto-submit the form here
// form.submit();
} catch (err) {
// The most common error is AbortError, which is fine
if (err.name !== 'AbortError') {
console.error(err);
}
}
};
listenForOtp();
// Cleanup function to abort the credential request
return () => {
abortController.abort();
};
}, []); // Empty dependency array means this runs once on mount
return (
<form>
<label htmlFor="otp-input">Enter your OTP code:</label>
<input
id="otp-input"
type="text"
inputMode="numeric"
autoComplete="one-time-code"
value={otp}
onChange={(e) => setOtp(e.target.value)}
/>
<button type="submit">Verify</button>
</form>
);
}
export default OtpForm;
Notice the autoComplete="one-time-code" attribute on the input. This is a related but separate feature that helps browsers (especially Safari on iOS) suggest OTPs from messages, even without the Web OTP API. It's a good fallback to have!
Level Up: Building a Reusable useWebOTP Hook
The useEffect approach works, but it's not very reusable. Let's abstract this logic into a custom hook, useWebOTP. This is the React way.
Our hook will take a callback function to execute when an OTP is successfully received.
import { useEffect } from 'react';
export const useWebOTP = (onOtpReceived) => {
useEffect(() => {
if (!('OTPCredential' in window)) {
return;
}
const abortController = new AbortController();
const getOtp = async () => {
try {
const otp = await navigator.credentials.get({
otp: { transport: ['sms'] },
signal: abortController.signal,
});
if (otp?.code) {
onOtpReceived(otp.code);
}
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Error getting OTP:', error);
}
}
};
getOtp();
return () => {
abortController.abort();
};
// Rerun if the callback function changes
}, [onOtpReceived]);
};
Now, using it in our component becomes beautifully simple:
import React, { useState, useCallback } from 'react';
import { useWebOTP } from './useWebOTP'; // Import our new hook
function OtpForm() {
const [otp, setOtp] = useState('');
const handleOtpReceived = useCallback((receivedOtp) => {
console.log('OTP received via hook:', receivedOtp);
setOtp(receivedOtp);
// Maybe auto-submit here
}, []);
// Use the hook
useWebOTP(handleOtpReceived);
return (
<form>
{/* ... same form as before ... */}
<input
value={otp}
onChange={(e) => setOtp(e.target.value)}
autoComplete="one-time-code"
/* ... other attributes */
/>
</form>
);
}
Much cleaner! This encapsulates the API logic and makes our components focused solely on the UI.
The "Honest" Part: Gotchas, Caveats, and Testing
Here's what most tutorials don't tell you.
Browser Support is Limited
As of late 2024, the Web OTP API is primarily supported by Chrome on Android. It's not available on iOS, Firefox, or desktop Chrome. This means it's a progressive enhancement, not a core feature you can rely on for all users. Your standard OTP input form must still be fully functional.
The SMS Format is Brittle
I can't stress this enough. If the @domain.com #otp format is even slightly off, it will fail without any warning in your console. Common mistakes include:
- Forgetting the
@or#. - Having a space between
#and the code. - Using a different domain than the one the user is on (e.g., a URL shortener).
You must coordinate perfectly with your backend or SMS provider.
Testing Can Be Awkward
You can't exactly ask your company to send you dozens of SMS messages for testing. So how do you do it?
The best way is with a real Android device and Chrome's remote debugging tools. Connect your phone to your computer via USB, enable debugging, and you can test your local development server directly on your phone. When the API call is active, you can send an SMS to your device (from another phone) with the correctly formatted string to trigger the flow.
Some versions of Chrome DevTools have experimental features for emulating this, but the real-device method is the most reliable way to confirm it works end-to-end.
The Final Verdict: Is It Worth It?
Absolutely.
Despite the limitations, implementing the Web OTP API is a low-effort, high-reward task. For the millions of users on Chrome for Android, you're removing a significant point of friction. It makes your application feel modern and thoughtful.
Think of it as a bonus feature. It won't break anything for users on unsupported browsers, but it will delight the ones on supported platforms. By building a simple useWebOTP hook, you can add this delightful experience to your React app in minutes. Just remember to treat it as the progressive enhancement it is.