Skip to main content
POST
/
otp
curl -X POST "http://localhost:8080/otp" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+1234567890",
    "create_user": true
  }'
{
  "message_id": "msg_1234567890abcdef",
  "phone": "+1***-***-7890"
}
Send a one-time password (OTP) via SMS for phone number verification or passwordless authentication. This endpoint supports both user registration and login flows.
This endpoint does not require authentication and can create new users if create_user is set to true.
curl -X POST "http://localhost:8080/otp" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+1234567890",
    "create_user": true
  }'

Request Body

phone
string
required
Phone number in E.164 format (e.g., +1234567890) to send the OTP to.
create_user
boolean
Whether to create a new user if the phone number doesn’t exist. Defaults to false.
data
object
Additional user metadata to store if creating a new user.
captcha_token
string
Captcha token for verification if captcha is enabled.
channel
string
Delivery channel for the OTP. Defaults to “sms”.Options:
  • sms - Send via SMS text message
  • whatsapp - Send via WhatsApp (if configured)

Response

message_id
string
Unique identifier for the sent OTP message
phone
string
Masked phone number that the OTP was sent to (e.g., “+1***-***-7890”)
{
  "message_id": "msg_1234567890abcdef",
  "phone": "+1***-***-7890"
}

Error Responses

{
  "code": 400,
  "msg": "Invalid phone number format",
  "details": "Phone number must be in E.164 format (e.g., +1234567890)"
}

OTP Authentication Flow

1

Request OTP

User enters their phone number and requests an OTP
2

SMS Sent

A 6-digit OTP is sent to the user’s phone via SMS
3

User Enters Code

User enters the OTP code in your application
4

Verification

Verify the OTP using the verify endpoint to complete authentication

Implementation Examples

React OTP Request Form

import { useState } from 'react';

function OTPRequestForm({ onOTPSent }) {
  const [phone, setPhone] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const [createUser, setCreateUser] = useState(true);

  const formatPhoneNumber = (value) => {
    // Remove all non-digits
    const digits = value.replace(/\D/g, '');
    
    // Format as +1 (XXX) XXX-XXXX for US numbers
    if (digits.length >= 10) {
      const formatted = `+1 (${digits.slice(1, 4)}) ${digits.slice(4, 7)}-${digits.slice(7, 11)}`;
      return formatted;
    }
    
    return value;
  };

  const handlePhoneChange = (e) => {
    const formatted = formatPhoneNumber(e.target.value);
    setPhone(formatted);
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError('');

    // Convert formatted phone to E.164
    const e164Phone = '+1' + phone.replace(/\D/g, '').slice(1);

    try {
      const response = await fetch('/api/auth/otp', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          phone: e164Phone,
          create_user: createUser
        }),
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.msg || 'Failed to send OTP');
      }

      const result = await response.json();
      onOTPSent?.(e164Phone, result.message_id);
      
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="otp-request-form">
      <h2>Sign in with Phone</h2>
      <p>
        Enter your phone number and we'll send you a verification code.
      </p>
      
      {error && (
        <div className="error-message">
          {error}
        </div>
      )}

      <div className="form-group">
        <label htmlFor="phone">Phone Number</label>
        <input
          type="tel"
          id="phone"
          value={phone}
          onChange={handlePhoneChange}
          placeholder="+1 (555) 123-4567"
          required
        />
        <small>We'll send a 6-digit verification code</small>
      </div>

      <div className="form-group">
        <label className="checkbox-label">
          <input
            type="checkbox"
            checked={createUser}
            onChange={(e) => setCreateUser(e.target.checked)}
          />
          Create account if it doesn't exist
        </label>
      </div>

      <button type="submit" disabled={loading || phone.length < 14}>
        {loading ? 'Sending...' : 'Send Verification Code'}
      </button>

      <p>
        Prefer email? <a href="/login">Sign in with email</a>
      </p>
    </form>
  );
}

export default OTPRequestForm;

React OTP Verification Form

import { useState, useEffect, useRef } from 'react';

function OTPVerificationForm({ phone, messageId, onVerified, onResend }) {
  const [otp, setOtp] = useState(['', '', '', '', '', '']);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const [timeLeft, setTimeLeft] = useState(300); // 5 minutes
  const inputRefs = useRef([]);

  useEffect(() => {
    // Countdown timer
    const timer = setInterval(() => {
      setTimeLeft(prev => {
        if (prev <= 1) {
          clearInterval(timer);
          return 0;
        }
        return prev - 1;
      });
    }, 1000);

    return () => clearInterval(timer);
  }, []);

  const handleOtpChange = (index, value) => {
    if (value.length > 1) return; // Prevent multiple characters
    
    const newOtp = [...otp];
    newOtp[index] = value;
    setOtp(newOtp);

    // Auto-focus next input
    if (value && index < 5) {
      inputRefs.current[index + 1]?.focus();
    }

    // Auto-submit when all fields are filled
    if (newOtp.every(digit => digit) && newOtp.join('').length === 6) {
      handleVerify(newOtp.join(''));
    }
  };

  const handleKeyDown = (index, e) => {
    // Handle backspace
    if (e.key === 'Backspace' && !otp[index] && index > 0) {
      inputRefs.current[index - 1]?.focus();
    }
  };

  const handleVerify = async (otpCode = otp.join('')) => {
    if (otpCode.length !== 6) {
      setError('Please enter all 6 digits');
      return;
    }

    setLoading(true);
    setError('');

    try {
      const response = await fetch('/api/auth/verify', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          type: 'sms',
          phone: phone,
          token: otpCode
        }),
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.msg || 'Invalid verification code');
      }

      const authData = await response.json();
      onVerified?.(authData);
      
    } catch (err) {
      setError(err.message);
      // Clear OTP on error
      setOtp(['', '', '', '', '', '']);
      inputRefs.current[0]?.focus();
    } finally {
      setLoading(false);
    }
  };

  const handleResend = async () => {
    try {
      await onResend?.();
      setTimeLeft(300); // Reset timer
      setOtp(['', '', '', '', '', '']);
      setError('');
      inputRefs.current[0]?.focus();
    } catch (err) {
      setError('Failed to resend code');
    }
  };

  const formatTime = (seconds) => {
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    return `${mins}:${secs.toString().padStart(2, '0')}`;
  };

  const maskedPhone = phone.replace(/(\+1)(\d{3})(\d{3})(\d{4})/, '$1 (***) ***-$4');

  return (
    <div className="otp-verification-form">
      <h2>Enter Verification Code</h2>
      <p>
        We sent a 6-digit code to <strong>{maskedPhone}</strong>
      </p>
      
      {error && (
        <div className="error-message">
          {error}
        </div>
      )}

      <div className="otp-inputs">
        {otp.map((digit, index) => (
          <input
            key={index}
            ref={el => inputRefs.current[index] = el}
            type="text"
            inputMode="numeric"
            pattern="[0-9]"
            maxLength={1}
            value={digit}
            onChange={(e) => handleOtpChange(index, e.target.value)}
            onKeyDown={(e) => handleKeyDown(index, e)}
            className="otp-input"
            disabled={loading}
            autoFocus={index === 0}
          />
        ))}
      </div>

      <button 
        onClick={() => handleVerify()} 
        disabled={loading || otp.some(digit => !digit)}
        className="verify-button"
      >
        {loading ? 'Verifying...' : 'Verify Code'}
      </button>

      <div className="resend-section">
        {timeLeft > 0 ? (
          <p>
            Resend code in <strong>{formatTime(timeLeft)}</strong>
          </p>
        ) : (
          <button onClick={handleResend} className="resend-button">
            Resend Code
          </button>
        )}
      </div>

      <p>
        <small>
          Didn't receive the code? Check your messages or try again.
        </small>
      </p>
    </div>
  );
}

export default OTPVerificationForm;

Complete Phone Authentication Component

import { useState } from 'react';
import OTPRequestForm from './OTPRequestForm';
import OTPVerificationForm from './OTPVerificationForm';

function PhoneAuth() {
  const [step, setStep] = useState('request'); // 'request' | 'verify'
  const [phone, setPhone] = useState('');
  const [messageId, setMessageId] = useState('');

  const handleOTPSent = (phoneNumber, msgId) => {
    setPhone(phoneNumber);
    setMessageId(msgId);
    setStep('verify');
  };

  const handleVerified = (authData) => {
    // Store tokens
    localStorage.setItem('access_token', authData.access_token);
    localStorage.setItem('refresh_token', authData.refresh_token);
    
    // Redirect to dashboard
    window.location.href = '/dashboard';
  };

  const handleResend = async () => {
    const response = await fetch('/api/auth/resend', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        type: 'sms',
        phone: phone
      }),
    });

    if (!response.ok) {
      throw new Error('Failed to resend code');
    }

    const result = await response.json();
    setMessageId(result.message_id);
  };

  const handleBack = () => {
    setStep('request');
    setPhone('');
    setMessageId('');
  };

  return (
    <div className="phone-auth">
      {step === 'request' ? (
        <OTPRequestForm onOTPSent={handleOTPSent} />
      ) : (
        <div>
          <button onClick={handleBack} className="back-button">
            ← Change Phone Number
          </button>
          <OTPVerificationForm
            phone={phone}
            messageId={messageId}
            onVerified={handleVerified}
            onResend={handleResend}
          />
        </div>
      )}
    </div>
  );
}

export default PhoneAuth;

Node.js Backend Handler

const express = require('express');
const { body, validationResult } = require('express-validator');

const router = express.Router();

// Phone number validation regex (E.164 format)
const phoneRegex = /^\+[1-9]\d{1,14}$/;

router.post('/otp', [
  body('phone').matches(phoneRegex).withMessage('Invalid phone number format'),
  body('create_user').optional().isBoolean(),
  body('data').optional().isObject()
], async (req, res) => {
  try {
    // Check validation errors
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        code: 400,
        msg: 'Invalid phone number format',
        details: 'Phone number must be in E.164 format (e.g., +1234567890)'
      });
    }

    const { phone, create_user, data, captcha_token, channel } = req.body;

    // Call Strike Auth Service
    const response = await fetch(`${process.env.AUTH_SERVICE_URL}/otp`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        phone,
        create_user: create_user || false,
        data,
        captcha_token,
        channel: channel || 'sms'
      }),
    });

    const result = await response.json();

    if (!response.ok) {
      return res.status(response.status).json(result);
    }

    // Log OTP request
    console.log(`OTP sent to: ${phone}, create_user: ${create_user}`);

    res.json(result);

  } catch (error) {
    console.error('OTP error:', error);
    res.status(500).json({
      code: 500,
      msg: 'Internal server error',
      details: 'Please try again later'
    });
  }
});

module.exports = router;

Phone Number Formats

Supported Formats

The API accepts phone numbers in E.164 format:
CountryFormatExample
United States+1XXXXXXXXXX+12345678901
United Kingdom+44XXXXXXXXX+447123456789
Canada+1XXXXXXXXXX+12345678901
Australia+61XXXXXXXXX+61412345678
Germany+49XXXXXXXXX+4915123456789

Format Validation

function validatePhoneNumber(phone) {
  // E.164 format: + followed by country code and number
  const e164Regex = /^\+[1-9]\d{1,14}$/;
  return e164Regex.test(phone);
}

function formatPhoneForDisplay(phone) {
  // Mask middle digits for privacy
  if (phone.startsWith('+1') && phone.length === 12) {
    return phone.replace(/(\+1)(\d{3})(\d{3})(\d{4})/, '$1 (***) ***-$4');
  }
  
  // Generic masking for other countries
  const visibleStart = phone.slice(0, 3);
  const visibleEnd = phone.slice(-4);
  const masked = '*'.repeat(phone.length - 7);
  return visibleStart + masked + visibleEnd;
}

Security Features

  • Rate Limiting: Prevents SMS spam and abuse
  • OTP Expiration: Codes expire after 5 minutes
  • Single Use: Each OTP can only be used once
  • Phone Verification: Inherent phone number verification
  • Secure Generation: Cryptographically secure random codes

Rate Limiting

This endpoint is rate limited to prevent abuse:
Limit TypeLimitWindow
Per Phone3 requests5 minutes
Per IP10 requests10 minutes

Best Practices

  • Provide clear phone number formatting guidance
  • Show masked phone number during verification
  • Implement auto-advancing OTP input fields
  • Include countdown timer for resend functionality
  • Offer alternative authentication methods
  • Use 6-digit codes with sufficient entropy
  • Implement proper rate limiting
  • Set reasonable OTP expiration (5 minutes)
  • Log OTP requests for security monitoring
  • Validate phone number formats server-side
  • Use reliable SMS providers with good delivery rates
  • Include clear sender identification
  • Monitor delivery rates and costs
  • Respect opt-out requests and regulations
  • Consider international SMS costs and restrictions

Testing

Unit Tests

describe('POST /otp', () => {
  test('should send OTP for valid phone number', async () => {
    const response = await request(app)
      .post('/otp')
      .send({
        phone: '+12345678901',
        create_user: false
      })
      .expect(200);

    expect(response.body.message_id).toBeDefined();
    expect(response.body.phone).toContain('***');
  });

  test('should create user and send OTP', async () => {
    const response = await request(app)
      .post('/otp')
      .send({
        phone: '+19876543210',
        create_user: true,
        data: {
          first_name: 'Test',
          last_name: 'User'
        }
      })
      .expect(200);

    expect(response.body.message_id).toBeDefined();
  });

  test('should reject invalid phone format', async () => {
    await request(app)
      .post('/otp')
      .send({
        phone: '1234567890', // Missing +
        create_user: true
      })
      .expect(400);
  });

  test('should return 404 for non-existent user when create_user is false', async () => {
    await request(app)
      .post('/otp')
      .send({
        phone: '+15555555555',
        create_user: false
      })
      .expect(404);
  });
});
I