Verify OTP
Securely verify one-time passwords with comprehensive attempt tracking, expiry validation, and automatic code invalidation. Built with security best practices to prevent brute force attacks.
Attempt Tracking
Automatic tracking of verification attempts with configurable limits
Expiry Validation
Codes automatically expire and cannot be verified after expiry
Auto-Invalidation
Successful verification immediately invalidates the code
Secure Matching
Constant-time comparison to prevent timing attacks
Security Features
All verifications use constant-time comparison to prevent timing attacks. Codes are hashed and salted in storage. Failed attempts are rate-limited per OTP and per IP address.
Authentication Required
Security Features
- Maximum attempts enforced per OTP (configurable, default: 3)
- Codes automatically invalidated after successful verification
- Expiry validation prevents use of stale codes
- IP-based rate limiting for additional security
3
Configurable up to 10
500ms
95th percentile
94%
First attempt
24h
Verification records
Request Body
{"phone": "0555539152","code": "123456","id": "otp_123456789_abc"}
OTP Lifecycle
Requested
T+0OTP generated and sent
Pending
T+0 to T+expiryWaiting for verification
Attempt 1
T+30sFirst verification attempt
Attempt 2
T+45sSecond attempt if needed
Verified/Success
T+50sCorrect code entered
OTP States
OTP created, waiting for verification
Successfully verified, code invalidated
Time window passed, cannot verify
Too many failed attempts, locked
Attempt Handling
Allow retry
Show error: Invalid code
Last attempt warning
Show: One attempt remaining
Lock OTP
Show: Too many attempts, request new code
Response
{"success": true,"message": "OTP verified successfully","data": {"verified": true,"phone": "233555539152","verifiedAt": "2024-01-15T10:30:45.000Z","id": "otp_123456789_abc","attemptsUsed": 1,"metadata": {"userId": "usr_12345","action": "login"}}}
Try It Yourself
https://api.sendexa.co/v1/otp/verifyImplementation Examples
// Secure OTP verification with attempt trackingclass OTPVerifier {constructor(apiKey, apiSecret) {this.auth = 'Basic ' + btoa(apiKey + ':' + apiSecret);this.baseUrl = 'https://api.sendexa.co/v1';this.attempts = new Map(); // Track attempts per OTP}async verifyOTP(identifier, code, security = {}) {const requestBody = {code,...this.parseIdentifier(identifier),security: {timestamp: new Date().toISOString(),...security}};try {const response = await fetch(`${this.baseUrl}/otp/verify`, {method: 'POST',headers: {'Content-Type': 'application/json','Authorization': this.auth},body: JSON.stringify(requestBody)});const data = await response.json();if (response.ok) {// Clear attempt tracking on successthis.clearAttempts(identifier);return {success: true,verified: true,data: data.data};}// Handle specific error typesswitch (data.errorType) {case 'INVALID_CODE':const remaining = data.data.attemptsRemaining;this.trackAttempt(identifier, remaining);return {success: false,invalid: true,remainingAttempts: remaining,message: remaining === 1? 'Invalid code. One attempt remaining!': `Invalid code. ${remaining} attempts remaining.`};case 'MAX_ATTEMPTS_EXCEEDED':this.lockOTP(identifier);return {success: false,locked: true,message: 'Too many failed attempts. Please request a new code.'};case 'EXPIRED':return {success: false,expired: true,message: 'This code has expired. Please request a new one.'};default:return {success: false,error: data.errorType,message: data.message};}} catch (error) {return {success: false,error: 'NETWORK_ERROR',message: 'Verification failed. Please try again.'};}}parseIdentifier(identifier) {if (typeof identifier === 'string') {if (identifier.startsWith('otp_')) {return { id: identifier };} else {return { phone: identifier };}}return identifier;}trackAttempt(identifier, remaining) {const key = typeof identifier === 'string' ? identifier : identifier.phone;this.attempts.set(key, {remaining,lastAttempt: new Date()});}clearAttempts(identifier) {const key = typeof identifier === 'string' ? identifier : identifier.phone;this.attempts.delete(key);}lockOTP(identifier) {const key = typeof identifier === 'string' ? identifier : identifier.phone;this.attempts.set(key, {locked: true,lastAttempt: new Date()});}getAttemptStatus(identifier) {const key = typeof identifier === 'string' ? identifier : identifier.phone;return this.attempts.get(key) || null;}}// Usage with React componentfunction OTPInput({ phone, onSuccess, onError }) {const [code, setCode] = useState('');const [verifying, setVerifying] = useState(false);const [remainingAttempts, setRemainingAttempts] = useState(null);const [error, setError] = useState('');const verifier = useRef(new OTPVerifier('api_key', 'api_secret'));const handleVerify = async () => {if (code.length < 4) return;setVerifying(true);setError('');const result = await verifier.current.verifyOTP(phone, code, {ipAddress: await getClientIP(),userAgent: navigator.userAgent});setVerifying(false);if (result.success) {setCode('');onSuccess?.(result.data);} else {if (result.remainingAttempts !== undefined) {setRemainingAttempts(result.remainingAttempts);setError(result.message);} else if (result.locked || result.expired) {setError(result.message);onError?.(result);} else {setError(result.message);}}};return (<div className="space-y-4"><div><inputtype="text"value={code}onChange={(e) => setCode(e.target.value.replace(/D/g, ''))}placeholder="Enter code"className="w-full p-2 border rounded"disabled={verifying}maxLength={6}autoFocus/>{remainingAttempts !== null && (<p className="text-sm text-yellow-600 mt-1">{remainingAttempts} attempt{remainingAttempts !== 1 ? 's' : ''} remaining</p>)}{error && (<p className="text-sm text-red-600 mt-1">{error}</p>)}</div><buttononClick={handleVerify}disabled={code.length < 4 || verifying}className="w-full bg-blue-600 text-white p-2 rounded disabled:opacity-50">{verifying ? 'Verifying...' : 'Verify Code'}</button></div>);}
Security Implementation Checklist
Attempt Tracking Flow
Attempt 1: Success
Code verified immediately - user redirected to app
Attempt 1-2: Invalid Code
Show error with remaining attempts count
Attempt 3: Last Warning
Show "One attempt remaining" warning
Attempt 4: Locked
OTP invalidated, user must request new code
Error Response Matrix
| Error Type | HTTP Status | Description | User Action |
|---|---|---|---|
| INVALID_CODE | 400 | Code doesn't match | Try again with correct code |
| MAX_ATTEMPTS_EXCEEDED | 400 | Too many failed attempts | Request new OTP |
| EXPIRED | 400 | OTP time window passed | Request new OTP |
| ALREADY_VERIFIED | 400 | Code already used | Login with existing session |
| NOT_FOUND | 404 | No pending OTP for identifier | Request new OTP |
Rate Limiting & Security
- Max attempts:3-10 (configurable)
- Lock duration:Permanent
- Verifications/hour:20
- Failed attempts/hour:15
- Verifications/hour:50
- Failed attempts/hour:30
Webhook Events
Configure webhooks to receive real-time notifications when OTPs are verified.
{"event": "otp.verified","timestamp": "2024-01-15T10:30:45.000Z","data": {"id": "otp_123456789_abc","phone": "233555539152","verifiedAt": "2024-01-15T10:30:45.000Z","attemptsUsed": 1,"metadata": {"userId": "usr_12345","action": "login"}},"signature": "sha256=7d5e3f8a2b1c9d4e6f8a0b2c4d6e8f0a2b4c6d8e"}
Analytics & Monitoring
Key Metrics to Track
- Success Rate94.2%
- Average Attempts1.3
- Peak Verification Time2:30 PM
- Failed Attempt Rate5.8%
Alert Thresholds
- High failure rate>15%
- Brute force attempt>50 fails/IP
- Verification spike>1000/hour
Testing Your Integration
Test Credentials
0550000001
Always succeeds with code 123456
0550000002
Always fails with INVALID_CODE
0550000003
Always returns EXPIRED
Troubleshooting Common Issues
Users reporting "Invalid Code"
- Check if code was auto-filled correctly (spaces, dashes)
- Verify code length matches pinLength configuration
- Ensure code hasn't expired (5-10 minute window)
- Check if user already attempted too many times
High failure rates
- Monitor for brute force attacks by IP
- Check SMS delivery rates (codes not being received)
- Verify phone number formatting is correct
- Consider increasing expiry time if users are slow
Integration issues
- Confirm you're storing OTP ID from request response
- Check that phone numbers are in international format
- Verify authentication headers are correct
- Test with sandbox credentials first
Compliance & Regulations
GDPR Compliance
Verification attempts are stored for 30 days for security auditing. Users have the right to request deletion of their verification history.
PCI-DSS Requirements
OTP verification for payment transactions must use 6-digit codes with 5-minute expiry and maximum 3 attempts.
Authentication Standards
Meets NIST SP 800-63B requirements for out-of-band verification when used with appropriate session management.
Frequently Asked Questions
What happens after max attempts?
Can I verify an expired OTP?
How are codes compared?
What's the difference between phone and id?
Quick Security Checklist
- Implement rate limiting
- Show remaining attempts
- Clear errors appropriately
- Log all attempts
- Use HTTPS only
- Invalidate after success
Need Help?
Our team is here to help you implement secure OTP verification:
- Email: [email protected] (24/7 support)
- Documentation: /docs/otp-best-practices
- Discord: https://discord.gg/sendexa
- Status page: https://status.sendexa.co