Verify OTP
Verify one-time passwords submitted by users to complete authentication and verification workflows.
Endpoint
POST /api/v1/otp/verify
Headers
X-API-Key: sk_live_your_api_key
Content-Type: application/json
Why Use This Endpoint?
- 🔐 Secure Verification - OTPs are hashed and validated against stored values
- ✅ Multi-Channel Support - Verify one or all channels in a single request
- ⚡ Instant Validation - Real-time OTP verification with immediate feedback
- 🎯 Flexible Logic - Verify all channels or just specific ones
- 📊 Attempt Tracking - Automatic tracking of failed attempts
- 🔒 Session Security - Auto-locking after max attempts exceeded
- ⏱️ Expiry Handling - Automatic session expiration management
Core Concept
After sending an OTP, you receive a session token. The user enters the OTP code(s) they received, and you send both the token and the OTP code(s) to this endpoint for verification.
Verification Flow
- User receives OTP via Email/SMS/WhatsApp
- User enters OTP code in your application
- Your app sends token + OTP code to verify endpoint
- API validates OTP and returns verification result
- Your app proceeds based on verification status
Request Structure
Required Fields
| Field | Type | Description |
|---|---|---|
token | string | Session token received from send OTP endpoint |
otps | object | OTP codes to verify for each channel |
OTPs Object
The otps object contains OTP codes for the channels you want to verify. You can verify:
- Single channel - Just one OTP
- Multiple channels - Multiple OTPs simultaneously
- All channels - All channels that received OTPs
| Field | Type | Required When | Description |
|---|---|---|---|
otps.email | string | Verifying email | 6-digit OTP code for email |
otps.sms | string | Verifying SMS | 6-digit OTP code for SMS |
otps.whatsapp | string | Verifying WhatsApp | 6-digit OTP code for WhatsApp |
Use Case Examples
Example 1: Verify Email OTP
User received OTP via email and entered the code.
curl -X POST https://api.sendmator.com/api/v1/otp/verify \
-H "X-API-Key: sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"otps": {
"email": "123456"
}
}'
Success Response:
{
"verified": true,
"verification_results": {
"email": {
"success": true
}
},
"verified_channels": ["email"],
"session_verified": true,
"contact_added": false,
"contact_id": null,
"attempts_remaining": 3
}
Example 2: Verify Phone via WhatsApp
User received OTP via both SMS and WhatsApp but chose to verify using WhatsApp.
curl -X POST https://api.sendmator.com/api/v1/otp/verify \
-H "X-API-Key: sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"otps": {
"whatsapp": "654321"
}
}'
Success Response:
{
"verified": true,
"verification_results": {
"whatsapp": {
"success": true
}
},
"verified_channels": ["whatsapp"],
"session_verified": true,
"contact_added": false,
"contact_id": null,
"attempts_remaining": 3
}
Example 3: Verify Multiple Channels Simultaneously
Verify both email and phone in a single request.
curl -X POST https://api.sendmator.com/api/v1/otp/verify \
-H "X-API-Key: sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"otps": {
"email": "123456",
"sms": "654321"
}
}'
Success Response:
{
"verified": true,
"verification_results": {
"email": {
"success": true
},
"sms": {
"success": true
}
},
"verified_channels": ["email", "sms"],
"session_verified": true,
"contact_added": false,
"contact_id": null,
"attempts_remaining": 3
}
Example 4: Invalid OTP (Failed Verification)
User entered incorrect OTP code.
curl -X POST https://api.sendmator.com/api/v1/otp/verify \
-H "X-API-Key: sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"otps": {
"email": "999999"
}
}'
Error Response:
{
"statusCode": 400,
"message": "Invalid OTP for channel: email",
"error": "Bad Request",
"details": {
"attempts_remaining": 2,
"max_attempts": 3
}
}
Example 5: Expired Session
User tried to verify after session expired.
curl -X POST https://api.sendmator.com/api/v1/otp/verify \
-H "X-API-Key: sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"otps": {
"email": "123456"
}
}'
Error Response:
{
"statusCode": 400,
"message": "OTP session has expired",
"error": "Bad Request",
"details": {
"expired_at": "2024-01-15T10:45:00.000Z"
}
}
Example 6: Max Attempts Exceeded
User failed verification 3 times (max attempts).
curl -X POST https://api.sendmator.com/api/v1/otp/verify \
-H "X-API-Key: sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"otps": {
"sms": "000000"
}
}'
Error Response:
{
"statusCode": 400,
"message": "Maximum verification attempts exceeded",
"error": "Bad Request",
"details": {
"attempts_used": 3,
"max_attempts": 3,
"session_locked": true
}
}
Response Fields
Success Response
| Field | Type | Description |
|---|---|---|
verified | boolean | Always true for successful verification |
verification_results | object | Verification status for each channel |
verified_channels | array | List of channels successfully verified |
session_verified | boolean | true if session requirements are met |
contact_added | boolean | Whether contact was added (if enabled) |
contact_id | string/null | UUID of created contact (if added) |
attempts_remaining | number | Number of verification attempts remaining |
Error Response
{
"statusCode": 400,
"message": "Error description",
"error": "Bad Request",
"details": {
"attempts_remaining": 2,
"max_attempts": 3
}
}
Common Error Responses
| Status Code | Error Message | Cause | Solution |
|---|---|---|---|
| 400 | Invalid OTP | Wrong code entered | Retry with correct code |
| 400 | OTP session has expired | Time limit exceeded | Request new OTP |
| 400 | Maximum attempts exceeded | Too many failed attempts | Request new OTP |
| 400 | Invalid token | Token malformed or invalid | Check token format |
| 400 | Session not found | Token doesn't match any session | Request new OTP |
| 400 | Channel not found in session | Verifying channel that wasn't sent | Verify correct channel |
| 400 | Channel already verified | Channel was already verified | No action needed |
| 401 | Invalid API key | API key missing or incorrect | Check API key |
Verification Logic
Single Channel Verification
When verifying a single channel:
- OTP code is validated against hashed value
- Channel is marked as verified
- Attempt counter is reset for that channel
all_channels_verifiedistrueonly if all sent channels are now verified
Multi-Channel Verification
When verifying multiple channels:
- Each OTP is validated independently
- All valid channels are marked as verified
- If any OTP is invalid, entire request fails
- Failed attempts are counted only on failure
Progressive Verification (Verify Separately)
You can verify channels separately at different times within the expiry window!
This powerful feature allows:
- Send OTP to multiple channels (email + SMS + WhatsApp)
- User verifies via WhatsApp first
- Later, user can still verify email or SMS
- Each channel maintains its own verification state
- All verifications must happen before session expires
Example Flow:
# Step 1: Send OTP to all channels
POST /api/v1/otp/send
{
"channels": ["email", "sms", "whatsapp"],
"recipients": { ... }
}
# Step 2: User verifies WhatsApp first (5 minutes later)
POST /api/v1/otp/verify
{ "token": "...", "otps": { "whatsapp": "123456" } }
# Response:
{
"verified": true,
"verified_channels": ["whatsapp"],
"session_verified": true
}
# Step 3: User verifies email (2 minutes later)
POST /api/v1/otp/verify
{ "token": "...", "otps": { "email": "789012" } }
# Response:
{
"verified": true,
"verified_channels": ["whatsapp", "email"],
"session_verified": true
}
Session Status Progression:
PENDING→ No channels verifiedPARTIAL→ Some channels verified (but not all)VERIFIED→ Session fully verified (based on verification mode)
Use Cases:
- Fallback channels: Send to SMS + WhatsApp, user can verify via whichever arrives first
- Progressive KYC: Verify email immediately, verify phone number later
- Flexible UX: Let users choose their preferred verification method
Attempt Tracking
- Each wrong OTP increments the attempt counter
- Counter is per session, not per channel
- After max attempts (default 3), session is locked
- Successful verification resets the counter
Session Expiry
- Sessions expire after configured time (default 10 minutes)
- Expired sessions cannot be verified
- User must request a new OTP
Best Practices
1. User Experience
- Show attempts remaining - Display
attempts_remainingto user - Clear error messages - Explain what went wrong
- Resend option - Always provide "Resend OTP" button
- Timer display - Show countdown for session expiry
- Auto-submit - Auto-submit when all digits entered
- Paste support - Allow pasting OTP from clipboard
2. Security
- Rate limiting - Limit verification requests per IP/user
- Token storage - Store token securely (sessionStorage/memory)
- HTTPS only - Always use secure connections
- Log verification attempts - Track for security monitoring
- Clear after success - Remove token after successful verification
3. Error Handling
try {
const response = await fetch('/api/v1/otp/verify', {
method: 'POST',
headers: {
'X-API-Key': 'sk_live_your_api_key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
token: sessionToken,
otps: { email: userInput }
})
});
const data = await response.json();
if (response.ok) {
// Success - proceed with authenticated flow
console.log('Verified channels:', data.verified_channels);
onVerificationSuccess(data);
} else {
// Handle different error types
if (data.message.includes('expired')) {
showResendOption();
} else if (data.message.includes('Maximum attempts')) {
lockFormAndShowResend();
} else {
showError(data.message);
showAttemptsRemaining(data.details?.attempts_remaining);
}
}
} catch (error) {
console.error('Verification failed:', error);
showError('Network error. Please try again.');
}
4. Integration Patterns
Pattern 1: Verify and Proceed
// Verify OTP and continue to dashboard
const verifyOtp = async (token, otp) => {
const response = await verifyOtpRequest(token, { email: otp });
if (response.success) {
// Store verified session data
localStorage.setItem('verified_email', response.session_data.recipients.email);
// Redirect to dashboard
window.location.href = '/dashboard';
}
};
Pattern 2: Verify and Create Account
// Verify email OTP during registration
const completeRegistration = async (token, otp, userData) => {
const verification = await verifyOtpRequest(token, { email: otp });
if (verification.success) {
// Create account with verified email
const account = await createAccount({
...userData,
email: verification.session_data.recipients.email,
email_verified: true
});
return account;
}
};
Pattern 3: Multi-Channel Verification
// Verify both email and phone
const verifyAllChannels = async (token, emailOtp, smsOtp) => {
const response = await verifyOtpRequest(token, {
email: emailOtp,
sms: smsOtp
});
if (response.all_channels_verified) {
// Both channels verified
console.log('All channels verified:', response.verified_channels);
}
};
5. State Management
Track verification state in your application:
const [verificationState, setVerificationState] = useState({
token: null,
attempts: 0,
maxAttempts: 3,
expiresAt: null,
verified: false,
error: null
});
const handleVerify = async (otp) => {
try {
const response = await verifyOtpRequest(verificationState.token, { email: otp });
setVerificationState({
...verificationState,
verified: true,
sessionData: response.session_data
});
} catch (error) {
setVerificationState({
...verificationState,
attempts: error.details?.attempts_used || verificationState.attempts + 1,
error: error.message
});
}
};
Verification Scenarios
Scenario 1: Email-Only Verification
Use Case: Email verification during signup
# Send
POST /api/v1/otp/send
{ "channels": ["email"], "recipients": { "email": "user@example.com" } }
# Verify
POST /api/v1/otp/verify
{ "token": "...", "otps": { "email": "123456" } }
Result: Email is verified, user can proceed with registration.
Scenario 2: Phone Verification with Fallback
Use Case: Phone verification with SMS and WhatsApp as fallback
# Send to both
POST /api/v1/otp/send
{
"channels": ["sms", "whatsapp"],
"recipients": { "sms": "+1234567890", "whatsapp": "+1234567890" }
}
# Verify via WhatsApp (user didn't receive SMS)
POST /api/v1/otp/verify
{ "token": "...", "otps": { "whatsapp": "654321" } }
Result: Phone is verified via WhatsApp, one channel verification is sufficient.
Scenario 3: Two-Factor Authentication
Use Case: 2FA verification after password login
# Send
POST /api/v1/otp/send
{
"channels": ["sms"],
"recipients": { "sms": "+1234567890" },
"config": { "expiry_minutes": 5 }
}
# Verify
POST /api/v1/otp/verify
{ "token": "...", "otps": { "sms": "789012" } }
Result: 2FA verified, user gains access to account.
Scenario 4: Account Recovery
Use Case: Verify ownership via email before password reset
# Send
POST /api/v1/otp/send
{
"channels": ["email"],
"recipients": { "email": "user@example.com" },
"metadata": { "purpose": "password_reset" }
}
# Verify
POST /api/v1/otp/verify
{ "token": "...", "otps": { "email": "345678" } }
Result: Identity verified, user can proceed to reset password.
Rate Limits
Verification endpoint limits:
- 10 verification attempts per minute per session
- 20 verification requests per minute per IP
- 3 failed attempts per session (configurable)
Next Steps
- Send OTP - Learn how to send OTPs
- OTP Overview - Understand OTP concepts
- Authentication - Set up API keys