Skip to main content

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

  1. User receives OTP via Email/SMS/WhatsApp
  2. User enters OTP code in your application
  3. Your app sends token + OTP code to verify endpoint
  4. API validates OTP and returns verification result
  5. Your app proceeds based on verification status

Request Structure

Required Fields

FieldTypeDescription
tokenstringSession token received from send OTP endpoint
otpsobjectOTP 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
FieldTypeRequired WhenDescription
otps.emailstringVerifying email6-digit OTP code for email
otps.smsstringVerifying SMS6-digit OTP code for SMS
otps.whatsappstringVerifying WhatsApp6-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

FieldTypeDescription
verifiedbooleanAlways true for successful verification
verification_resultsobjectVerification status for each channel
verified_channelsarrayList of channels successfully verified
session_verifiedbooleantrue if session requirements are met
contact_addedbooleanWhether contact was added (if enabled)
contact_idstring/nullUUID of created contact (if added)
attempts_remainingnumberNumber 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 CodeError MessageCauseSolution
400Invalid OTPWrong code enteredRetry with correct code
400OTP session has expiredTime limit exceededRequest new OTP
400Maximum attempts exceededToo many failed attemptsRequest new OTP
400Invalid tokenToken malformed or invalidCheck token format
400Session not foundToken doesn't match any sessionRequest new OTP
400Channel not found in sessionVerifying channel that wasn't sentVerify correct channel
400Channel already verifiedChannel was already verifiedNo action needed
401Invalid API keyAPI key missing or incorrectCheck API key

Verification Logic

Single Channel Verification

When verifying a single channel:

  1. OTP code is validated against hashed value
  2. Channel is marked as verified
  3. Attempt counter is reset for that channel
  4. all_channels_verified is true only if all sent channels are now verified

Multi-Channel Verification

When verifying multiple channels:

  1. Each OTP is validated independently
  2. All valid channels are marked as verified
  3. If any OTP is invalid, entire request fails
  4. 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 verified
  • PARTIAL → 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_remaining to 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