Overview
The SMS OTP system provides phone number verification and authentication using one-time passwords sent via SMS. It supports registration, login, phone verification, and password recovery flows.
Check SMS OTP Status
Check if SMS OTP is enabled for the system.
Endpoint: GET /api/auth/sms-otp/status
Response: 200 OK
{
"code" : 200 ,
"message" : "SMS OTP status retrieved successfully" ,
"data" : {
"enabled" : true ,
"provider" : "twilio"
}
}
Example:
async function checkSMSOTPStatus () {
const response = await fetch ( 'https://api.rcoinx.com/api/auth/sms-otp/status' , {
method: 'GET' ,
headers: {
'Content-Type' : 'application/json'
}
});
const data = await response . json ();
return data . data ;
}
Send SMS OTP
Send a verification code to a phone number.
Endpoint: POST /api/auth/sms-otp/send
Request:
{
"phone" : "1234567890" ,
"country_code" : "1" ,
"purpose" : "verification"
}
Parameters:
phone: Required, minimum 10 digits, numeric only
country_code: Required, 1-3 digits
purpose: Optional, one of: verification, login, registration, password_reset
Response: 200 OK
{
"code" : 200 ,
"message" : "OTP sent successfully" ,
"data" : {
"phone" : "1234567890" ,
"country_code" : "1" ,
"expires_in" : 300 ,
"sent_at" : "2025-10-13T12:00:00Z"
}
}
OTP codes typically expire after 5 minutes (300 seconds). The code is 6 digits.
Example:
JavaScript
TypeScript
Flutter
async function sendSMSOTP ( phone , countryCode , purpose = 'verification' ) {
const response = await fetch ( 'https://api.rcoinx.com/api/auth/sms-otp/send' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
phone ,
country_code: countryCode ,
purpose
})
});
const data = await response . json ();
if ( ! response . ok ) {
throw new Error ( data . message );
}
return data . data ;
}
// Usage
try {
await sendSMSOTP ( '1234567890' , '1' , 'verification' );
console . log ( 'OTP sent successfully' );
} catch ( error ) {
console . error ( 'Failed to send OTP:' , error );
}
SMS sending is rate-limited. Excessive requests may result in temporary blocking.
Verify SMS OTP
Verify an OTP code sent to a phone number.
Endpoint: POST /api/auth/sms-otp/verify
Request:
{
"phone" : "1234567890" ,
"country_code" : "1" ,
"code" : "123456"
}
Parameters:
phone: Required, the phone number the OTP was sent to
country_code: Required, country code
code: Required, 6-digit OTP code
Response: 200 OK
{
"code" : 200 ,
"message" : "OTP verified successfully" ,
"data" : {
"verified" : true ,
"phone" : "1234567890" ,
"country_code" : "1"
}
}
Error Response: 400 Bad Request
{
"code" : 400 ,
"message" : "Invalid OTP code" ,
"error" : "The code you entered is incorrect or expired"
}
Example:
async function verifySMSOTP ( phone , countryCode , code ) {
const response = await fetch ( 'https://api.rcoinx.com/api/auth/sms-otp/verify' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
phone ,
country_code: countryCode ,
code
})
});
const data = await response . json ();
if ( ! response . ok ) {
throw new Error ( data . message );
}
return data . data ;
}
Register with SMS OTP
Register a new user using SMS OTP verification.
Endpoint: POST /api/auth/register/sms-otp
Request:
{
"phone" : "1234567890" ,
"country_code" : "1" ,
"username" : "johndoe" ,
"password" : "SecurePass123!" ,
"password_confirmation" : "SecurePass123!" ,
"otp_code" : "123456"
}
Parameters:
phone: Required, phone number
country_code: Required, country code
username: Required, 3-50 characters, unique
password: Required, minimum 8 characters
password_confirmation: Required, must match password
otp_code: Required, 6-digit OTP code (must be verified first)
Response: 200 OK
{
"code" : 200 ,
"message" : "Registration successful" ,
"data" : {
"access_token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ,
"refresh_token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ,
"access_token_expires_at" : "2025-10-13T12:30:00Z" ,
"refresh_token_expires_at" : "2025-10-20T12:00:00Z" ,
"user" : {
"id" : 123 ,
"username" : "johndoe" ,
"phone" : {
"phone" : "1234567890" ,
"country_code" : "1" ,
"verified" : true
}
}
}
}
Example:
async function registerWithSMSOTP ( phone , countryCode , username , password , otpCode ) {
const response = await fetch ( 'https://api.rcoinx.com/api/auth/register/sms-otp' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
phone ,
country_code: countryCode ,
username ,
password ,
password_confirmation: password ,
otp_code: otpCode
})
});
const data = await response . json ();
if ( ! response . ok ) {
throw new Error ( data . message );
}
return data . data ;
}
Login with SMS OTP
Login using phone number and OTP (passwordless login).
Endpoint: POST /api/auth/login/sms-otp
Request:
{
"phone" : "1234567890" ,
"country_code" : "1" ,
"otp_code" : "123456" ,
"remember" : false
}
Parameters:
phone: Required, registered phone number
country_code: Required, country code
otp_code: Required, 6-digit OTP code
remember: Optional, extends token lifetime
Response: 200 OK
{
"code" : 200 ,
"message" : "Login successful" ,
"data" : {
"access_token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ,
"refresh_token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ,
"access_token_expires_at" : "2025-10-13T12:30:00Z" ,
"refresh_token_expires_at" : "2025-10-20T12:00:00Z" ,
"user" : {
"id" : 123 ,
"username" : "johndoe" ,
"phone" : {
"phone" : "1234567890" ,
"country_code" : "1" ,
"verified" : true
}
}
}
}
Example:
async function loginWithSMSOTP ( phone , countryCode , otpCode , remember = false ) {
const response = await fetch ( 'https://api.rcoinx.com/api/auth/login/sms-otp' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
phone ,
country_code: countryCode ,
otp_code: otpCode ,
remember
})
});
const data = await response . json ();
if ( ! response . ok ) {
throw new Error ( data . message );
}
// Store tokens
localStorage . setItem ( 'access_token' , data . data . access_token );
localStorage . setItem ( 'refresh_token' , data . data . refresh_token );
return data . data ;
}
Forget Password with SMS OTP
Reset password using SMS OTP verification.
Endpoint: POST /api/auth/sms-otp/forget-password
Request:
{
"phone" : "1234567890" ,
"country_code" : "1" ,
"otp_code" : "123456" ,
"new_password" : "NewSecurePass123!" ,
"new_password_confirmation" : "NewSecurePass123!"
}
Parameters:
phone: Required, registered phone number
country_code: Required, country code
otp_code: Required, 6-digit OTP code
new_password: Required, minimum 8 characters
new_password_confirmation: Required, must match new_password
Response: 200 OK
{
"code" : 200 ,
"message" : "Password reset successful" ,
"data" : {
"success" : true
}
}
Example:
async function resetPasswordWithSMSOTP ( phone , countryCode , otpCode , newPassword ) {
const response = await fetch ( 'https://api.rcoinx.com/api/auth/sms-otp/forget-password' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
phone ,
country_code: countryCode ,
otp_code: otpCode ,
new_password: newPassword ,
new_password_confirmation: newPassword
})
});
const data = await response . json ();
if ( ! response . ok ) {
throw new Error ( data . message );
}
return data . data ;
}
Add Phone (Authenticated Users)
Add a new phone number to an authenticated user’s account.
Endpoint: POST /api/user/sms-otp/add-phone
Headers:
Authorization : Bearer <access_token>
Request:
{
"phone" : "9876543210" ,
"country_code" : "1"
}
Response: 200 OK
{
"code" : 200 ,
"message" : "Phone added successfully. Please verify." ,
"data" : {
"phone" : "9876543210" ,
"country_code" : "1" ,
"verified" : false ,
"otp_sent" : true
}
}
Example:
async function addPhone ( phone , countryCode ) {
const accessToken = localStorage . getItem ( 'access_token' );
const response = await fetch ( 'https://api.rcoinx.com/api/user/sms-otp/add-phone' , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ accessToken } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
phone ,
country_code: countryCode
})
});
const data = await response . json ();
if ( ! response . ok ) {
throw new Error ( data . message );
}
return data . data ;
}
Verify Phone (Authenticated Users)
Verify a phone number added to an authenticated user’s account.
Endpoint: POST /api/user/sms-otp/verify-phone
Headers:
Authorization : Bearer <access_token>
Request:
{
"phone" : "9876543210" ,
"country_code" : "1" ,
"otp_code" : "123456"
}
Response: 200 OK
{
"code" : 200 ,
"message" : "Phone verified successfully" ,
"data" : {
"phone" : "9876543210" ,
"country_code" : "1" ,
"verified" : true
}
}
Example:
async function verifyPhone ( phone , countryCode , otpCode ) {
const accessToken = localStorage . getItem ( 'access_token' );
const response = await fetch ( 'https://api.rcoinx.com/api/user/sms-otp/verify-phone' , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ accessToken } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
phone ,
country_code: countryCode ,
otp_code: otpCode
})
});
const data = await response . json ();
if ( ! response . ok ) {
throw new Error ( data . message );
}
return data . data ;
}
Complete SMS OTP Flow Example
Here’s a complete example of implementing SMS OTP login:
import React , { useState } from 'react' ;
function SMSOTPLogin () {
const [ step , setStep ] = useState ( 'phone' ); // 'phone' or 'code'
const [ phone , setPhone ] = useState ( '' );
const [ countryCode , setCountryCode ] = useState ( '1' );
const [ code , setCode ] = useState ( '' );
const [ loading , setLoading ] = useState ( false );
const [ error , setError ] = useState ( null );
const [ timeLeft , setTimeLeft ] = useState ( 0 );
const handleSendOTP = async ( e ) => {
e . preventDefault ();
setLoading ( true );
setError ( null );
try {
await sendSMSOTP ( phone , countryCode , 'login' );
setStep ( 'code' );
setTimeLeft ( 300 ); // 5 minutes
// Start countdown
const interval = setInterval (() => {
setTimeLeft ( prev => {
if ( prev <= 1 ) {
clearInterval ( interval );
return 0 ;
}
return prev - 1 ;
});
}, 1000 );
} catch ( err ) {
setError ( err . message );
} finally {
setLoading ( false );
}
};
const handleVerifyOTP = async ( e ) => {
e . preventDefault ();
setLoading ( true );
setError ( null );
try {
const result = await loginWithSMSOTP ( phone , countryCode , code );
// Login successful
console . log ( 'Logged in:' , result . user );
window . location . href = '/dashboard' ;
} catch ( err ) {
setError ( err . message );
} finally {
setLoading ( false );
}
};
const handleResend = async () => {
setLoading ( true );
setError ( null );
try {
await sendSMSOTP ( phone , countryCode , 'login' );
setTimeLeft ( 300 );
alert ( 'New code sent!' );
} catch ( err ) {
setError ( err . message );
} finally {
setLoading ( false );
}
};
const formatTime = ( seconds ) => {
const mins = Math . floor ( seconds / 60 );
const secs = seconds % 60 ;
return ` ${ mins } : ${ secs . toString (). padStart ( 2 , '0' ) } ` ;
};
return (
< div className = "sms-otp-login" >
< h2 > Login with Phone </ h2 >
{ error && < div className = "error" > { error } </ div > }
{ step === 'phone' ? (
< form onSubmit = { handleSendOTP } >
< div >
< label > Country Code: </ label >
< input
type = "text"
value = { countryCode }
onChange = { ( e ) => setCountryCode ( e . target . value ) }
placeholder = "1"
required
/>
</ div >
< div >
< label > Phone Number: </ label >
< input
type = "tel"
value = { phone }
onChange = { ( e ) => setPhone ( e . target . value ) }
placeholder = "1234567890"
required
/>
</ div >
< button type = "submit" disabled = { loading } >
{ loading ? 'Sending...' : 'Send Code' }
</ button >
</ form >
) : (
< form onSubmit = { handleVerifyOTP } >
< p > Enter the code sent to + { countryCode } { phone } </ p >
< div >
< label > Verification Code: </ label >
< input
type = "text"
value = { code }
onChange = { ( e ) => setCode ( e . target . value ) }
placeholder = "123456"
maxLength = { 6 }
required
/>
</ div >
< div className = "timer" >
Code expires in: { formatTime ( timeLeft ) }
</ div >
< button type = "submit" disabled = { loading || ! code } >
{ loading ? 'Verifying...' : 'Verify & Login' }
</ button >
< button
type = "button"
onClick = { handleResend }
disabled = { loading || timeLeft > 240 }
>
Resend Code
</ button >
< button type = "button" onClick = { () => setStep ( 'phone' ) } >
Change Phone Number
</ button >
</ form >
) }
</ div >
);
}
export default SMSOTPLogin ;
Error Handling
Common Errors
Status Code Error Description 400 Invalid OTP code Code is incorrect or expired 404 Phone not found Phone number is not registered 409 Phone already exists Phone number is already registered 429 Too many requests Rate limit exceeded 503 SMS service unavailable SMS provider is down
Best Practices
1. Implement Rate Limiting UI
const RATE_LIMIT = {
maxAttempts: 5 ,
windowMs: 60000 // 1 minute
};
class SMSOTPRateLimiter {
constructor () {
this . attempts = [];
}
canSend () {
const now = Date . now ();
this . attempts = this . attempts . filter (
time => now - time < RATE_LIMIT . windowMs
);
return this . attempts . length < RATE_LIMIT . maxAttempts ;
}
recordAttempt () {
this . attempts . push ( Date . now ());
}
getTimeUntilReset () {
if ( this . attempts . length === 0 ) return 0 ;
const oldest = Math . min ( ... this . attempts );
const timeElapsed = Date . now () - oldest ;
return Math . max ( 0 , RATE_LIMIT . windowMs - timeElapsed );
}
}
2. Validate Phone Numbers
function validatePhoneNumber ( phone , countryCode ) {
// Remove any non-numeric characters
const cleanPhone = phone . replace ( / \D / g , '' );
// Check minimum length
if ( cleanPhone . length < 10 ) {
throw new Error ( 'Phone number must be at least 10 digits' );
}
// Validate country code
const cleanCode = countryCode . replace ( / \D / g , '' );
if ( cleanCode . length < 1 || cleanCode . length > 3 ) {
throw new Error ( 'Invalid country code' );
}
return { phone: cleanPhone , countryCode: cleanCode };
}
3. Auto-Submit Code
function OTPInput ({ length = 6 , onComplete }) {
const [ code , setCode ] = useState ( Array ( length ). fill ( '' ));
const inputs = useRef ([]);
const handleChange = ( index , value ) => {
if ( ! / ^ \d * $ / . test ( value )) return ;
const newCode = [ ... code ];
newCode [ index ] = value ;
setCode ( newCode );
// Auto-focus next input
if ( value && index < length - 1 ) {
inputs . current [ index + 1 ]?. focus ();
}
// Auto-submit when complete
if ( newCode . every ( digit => digit ) && onComplete ) {
onComplete ( newCode . join ( '' ));
}
};
return (
< div className = "otp-input" >
{ code . map (( digit , index ) => (
< input
key = { index }
ref = { el => inputs . current [ index ] = el }
type = "text"
maxLength = { 1 }
value = { digit }
onChange = { ( e ) => handleChange ( index , e . target . value ) }
onKeyDown = { ( e ) => {
if ( e . key === 'Backspace' && ! digit && index > 0 ) {
inputs . current [ index - 1 ]?. focus ();
}
} }
/>
)) }
</ div >
);
}