OTP
OTP Integration Examples
Updated: 2026-02-22 - Added customMessageTemplate request examples
This page provides practical code examples for integrating with the OTP service.
JavaScript/TypeScript
Send OTP
Codeasync function sendOtp(phoneNumber: string, purpose: string): Promise<string> { const response = await fetch('/api/v2/otp/send', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ phoneNumber: phoneNumber, purpose: purpose, expiryMinutes: 10 }) }); if (response.status === 429) { const error = await response.json(); throw new Error(`Rate limited. Retry after ${error.extensions?.retry_after_seconds} seconds`); } if (!response.ok) { throw new Error(`Failed to send OTP: ${response.statusText}`); } const data = await response.json(); return data.otpRequestId; // Store this for verification }
Send OTP with Custom Message Template
Codeasync function sendOtpWithCustomTemplate(phoneNumber: string): Promise<string> { const response = await fetch('/api/v2/otp/send', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ phoneNumber, purpose: 'Login', expiryMinutes: 5, senderId: 'MYAPP', customMessageTemplate: 'Use code {code} to sign in. It expires in {minutes} minutes.' }) }); if (!response.ok) { throw new Error(`Failed to send OTP: ${response.statusText}`); } const data = await response.json(); return data.otpRequestId; }
Supported placeholders in customMessageTemplate:
{code}{minutes}
Verify OTP
Codeasync function verifyOtp( phoneNumber: string, code: string, otpRequestId: string ): Promise<boolean> { const response = await fetch('/api/v2/otp/verify', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ phoneNumber: phoneNumber, code: code, purpose: 'Login' }) }); if (!response.ok) { throw new Error(`Failed to verify OTP: ${response.statusText}`); } const data = await response.json(); if (!data.isValid) { console.log(`Verification failed: ${data.message}`); console.log(`Remaining attempts: ${3 - (data.attemptCount || 0)}`); } return data.isValid; }
React Hook Example
Codeimport { useState, useCallback } from 'react'; interface UseOtpReturn { otpRequestId: string | null; isSending: boolean; isVerifying: boolean; error: string | null; sendOtp: (phoneNumber: string) => Promise<void>; verifyOtp: (code: string) => Promise<boolean>; remainingAttempts: number; } export function useOtp(purpose: string = 'Login'): UseOtpReturn { const [otpRequestId, setOtpRequestId] = useState<string | null>(null); const [isSending, setIsSending] = useState(false); const [isVerifying, setIsVerifying] = useState(false); const [error, setError] = useState<string | null>(null); const [remainingAttempts, setRemainingAttempts] = useState(3); const sendOtp = useCallback(async (phoneNumber: string) => { setIsSending(true); setError(null); try { const response = await fetch('/api/v2/otp/send', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ phoneNumber, purpose }) }); if (response.status === 429) { const err = await response.json(); throw new Error(`Too many requests. Retry in ${Math.ceil(err.extensions?.retry_after_seconds / 60)} minutes.`); } if (!response.ok) { throw new Error('Failed to send OTP'); } const data = await response.json(); setOtpRequestId(data.otpRequestId); setRemainingAttempts(3); } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error'); } finally { setIsSending(false); } }, [purpose]); const verifyOtp = useCallback(async (code: string) => { if (!otpRequestId) return false; setIsVerifying(true); setError(null); try { const response = await fetch('/api/v2/otp/verify', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ phoneNumber: phoneNumber, code: code, purpose: purpose }) }); const data = await response.json(); if (data.isValid) { return true; } else { setRemainingAttempts(prev => prev - 1); setError(data.message); return false; } } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error'); return false; } finally { setIsVerifying(false); } }, [otpRequestId, purpose]); return { otpRequestId, isSending, isVerifying, error, sendOtp, verifyOtp, remainingAttempts }; }
C# / .NET
Using HttpClient
Codepublic class OtpClient { private readonly HttpClient _httpClient; private readonly string _baseUrl; private readonly string _accessToken; public OtpClient(HttpClient httpClient, string baseUrl, string accessToken) { _httpClient = httpClient; _baseUrl = baseUrl; _accessToken = accessToken; } public async Task<Guid> SendOtpAsync(string phoneNumber, OtpPurpose purpose) { var request = new { PhoneNumber = phoneNumber, Purpose = purpose, ExpiryMinutes = 10 }; var json = JsonSerializer.Serialize(request); var content = new StringContent(json, Encoding.UTF8, "application/json"); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken); var response = await _httpClient.PostAsync($"{_baseUrl}/api/v2/otp/send", content); if (response.StatusCode == HttpStatusCode.TooManyRequests) { var error = await response.Content.ReadAsStringAsync(); throw new OtpRateLimitException("Rate limit exceeded", error); } response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync<OtpGenerationResult>(); return result!.OtpRequestId; } public async Task<bool> VerifyOtpAsync(string phoneNumber, string code, OtpPurpose purpose) { var request = new { PhoneNumber = phoneNumber, Code = code, Purpose = purpose }; var json = JsonSerializer.Serialize(request); var content = new StringContent(json, Encoding.UTF8, "application/json"); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken); var response = await _httpClient.PostAsync($"{_baseUrl}/api/v2/otp/verify", content); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync<OtpVerificationResult>(); return result!.IsValid; } public async Task<Guid> SendOtpWithCustomTemplateAsync(string phoneNumber) { var request = new { PhoneNumber = phoneNumber, Purpose = OtpPurpose.Login, ExpiryMinutes = 5, SenderId = "MYAPP", CustomMessageTemplate = "Use code {code} to sign in. It expires in {minutes} minutes." }; var json = JsonSerializer.Serialize(request); var content = new StringContent(json, Encoding.UTF8, "application/json"); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken); var response = await _httpClient.PostAsync($"{_baseUrl}/api/v2/otp/send", content); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync<OtpGenerationResult>(); return result!.OtpRequestId; } }
cURL Examples
Send OTP
Codecurl -X POST https://api.tech231apps.net/api/v2/otp/send \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "phoneNumber": "+231770123456", "purpose": "Login", "expiryMinutes": 10 }'
Send OTP (Custom Template)
Codecurl -X POST https://api.tech231apps.net/api/v2/otp/send \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "phoneNumber": "+231770123456", "purpose": "Login", "expiryMinutes": 5, "senderId": "MYAPP", "customMessageTemplate": "Use code {code} to sign in. It expires in {minutes} minutes." }'
Response:
Code{ "otpRequestId": "550e8400-e29b-41d4-a716-446655440000", "phoneNumber": "+231770123456", "expiresAt": "2025-02-21T22:30:00Z", "status": "Pending" }
Verify OTP
Codecurl -X POST https://api.tech231apps.net/api/v2/otp/verify \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "phoneNumber": "+231770123456", "code": "123456", "purpose": "Login" }'
Response:
Code{ "isValid": true, "status": "Verified", "message": "Code verified successfully" }
Python
Using requests
Codeimport requests from typing import Optional class OtpClient: def __init__(self, base_url: str, access_token: str): self.base_url = base_url self.access_token = access_token self.headers = { 'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json' } def send_otp(self, phone_number: str, purpose: str = 'Login') -> str: response = requests.post( f'{self.base_url}/api/v2/otp/send', headers=self.headers, json={ 'phoneNumber': phone_number, 'purpose': purpose, 'expiryMinutes': 10 } ) if response.status_code == 429: error_data = response.json() retry_after = error_data.get('extensions', {}).get('retry_after_seconds', 600) raise RateLimitError(f"Rate limited. Retry after {retry_after} seconds") response.raise_for_status() data = response.json() return data['otpRequestId'] def verify_otp(self, phone_number: str, code: str, purpose: str = 'Login') -> bool: response = requests.post( f'{self.base_url}/api/v2/otp/verify', headers=self.headers, json={ 'phoneNumber': phone_number, 'code': code, 'purpose': purpose } ) response.raise_for_status() data = response.json() return data['isValid'] class RateLimitError(Exception): pass
Error Handling
Handle Rate Limits
Codeasync function sendOtpWithRetry(phoneNumber: string): Promise<string> { try { return await sendOtp(phoneNumber, 'Login'); } catch (error) { if (error.message.includes('Rate limited')) { // Show user a message about retry time const match = error.message.match(/Retry after (\d+) seconds/); const seconds = match ? parseInt(match[1]) : 600; const minutes = Math.ceil(seconds / 60); throw new Error(`Too many attempts. Please try again in ${minutes} minutes.`); } throw error; } }
Handle Verification Failures
Codeasync function verifyOtpWithAttempts( phoneNumber: string, code: string, maxAttempts: number = 3 ): Promise<boolean> { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { const isValid = await verifyOtp(phoneNumber, code); if (isValid) { return true; } // If not valid and not last attempt, prompt for retry if (attempt < maxAttempts) { code = await promptUserForCode( `Invalid code. ${maxAttempts - attempt} attempts remaining.` ); } } catch (error) { if (error.message.includes('expired')) { throw new Error('OTP has expired. Please request a new one.'); } if (error.message.includes('maximum')) { throw new Error('Maximum attempts exceeded. Please request a new OTP.'); } throw error; } } return false; }
UI Implementation Tips
Countdown Timer
Codefunction OtpCountdown({ expiresAt }: { expiresAt: Date }) { const [secondsLeft, setSecondsLeft] = useState( Math.max(0, Math.floor((expiresAt.getTime() - Date.now()) / 1000)) ); useEffect(() => { const interval = setInterval(() => { const remaining = Math.max(0, Math.floor((expiresAt.getTime() - Date.now()) / 1000)); setSecondsLeft(remaining); if (remaining === 0) { clearInterval(interval); } }, 1000); return () => clearInterval(interval); }, [expiresAt]); const minutes = Math.floor(secondsLeft / 60); const seconds = secondsLeft % 60; return ( <div className={secondsLeft < 60 ? 'text-red-500' : 'text-gray-600'}> Expires in: {minutes}:{seconds.toString().padStart(2, '0')} </div> ); }
OTP Input Component
Codefunction OtpInput({ onComplete }: { onComplete: (code: string) => void }) { const [code, setCode] = useState(['', '', '', '', '', '']); const inputs = useRef<(HTMLInputElement | null)[]>([]); const handleChange = (index: number, value: string) => { if (!/^\d*$/.test(value)) return; const newCode = [...code]; newCode[index] = value.slice(0, 1); setCode(newCode); // Auto-focus next input if (value && index < 5) { inputs.current[index + 1]?.focus(); } // Check if complete const fullCode = newCode.join(''); if (fullCode.length === 6) { onComplete(fullCode); } }; const handleKeyDown = (index: number, e: React.KeyboardEvent) => { if (e.key === 'Backspace' && !code[index] && index > 0) { inputs.current[index - 1]?.focus(); } }; return ( <div className="flex gap-2"> {code.map((digit, index) => ( <input key={index} ref={el => inputs.current[index] = el} type="text" inputMode="numeric" maxLength={1} value={digit} onChange={e => handleChange(index, e.target.value)} onKeyDown={e => handleKeyDown(index, e)} className="w-12 h-14 text-center text-2xl border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none" /> ))} </div> ); }
Best Practices
- Store OTP Request ID - Keep it for verification and status checks
- Show expiration timer - Let users know how much time they have
- Handle rate limits gracefully - Show retry time, not just error
- Allow resend with delay - Implement 60+ second delay between resends
- Mask phone numbers - Show only last 4 digits in UI
- Clear inputs on error - Don't leave partial codes visible
- Auto-submit on complete - When 6 digits entered, auto-verify
Next Steps
- See OTP Service Integration for API reference
- See OTP Security Guide for security details
Last modified on