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:
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 CodeErrorDescription
400Invalid OTP codeCode is incorrect or expired
404Phone not foundPhone number is not registered
409Phone already existsPhone number is already registered
429Too many requestsRate limit exceeded
503SMS service unavailableSMS 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>
  );
}