Request OTP

Generate and send secure one-time passwords with configurable length, type, expiry, and automatic rate limiting. Perfect for authentication, transaction verification, and account recovery flows.

Multiple PIN Types

NUMERIC, ALPHANUMERIC, or ALPHABETIC codes, 4-10 characters long

Flexible Expiry

Custom expiry from 1 minute up to 24 hours with automatic invalidation

Rate Limiting

Phone-based and IP-based limits to prevent OTP spam and abuse

Metadata Support

Attach custom data (userId, sessionId) for seamless integration

High Security
HIGH

Advanced security measures for sensitive operations

  • Encrypted at rest (AES-256-GCM)
  • Automatic code expiration
  • Brute force protection
  • Idempotency-Key support
Default Expiry

10 min

Configurable 1-1440 (minutes or hours)

Max Attempts

3

Per OTP by default

Rate Limit

3 / hr

Per phone number

Code Length

4-10

Chars, any PIN type

POST
/v1/otp/request
Enhanced
Idempotent

Request Body

application/json
JSON
{
"phone": "0555539152",
"from": "YourBrand",
"message": "Your verification code is {code}. Valid for {amount} {duration}.",
"pinLength": 6,
"pinType": "NUMERIC",
"expiry": {
"amount": 10,
"duration": "minutes"
},
"maxAmountOfValidationRetries": 3,
"metadata": {
"userId": "usr_12345",
"action": "login"
},
"ipAddress": "192.168.1.1"
}

PIN Type Comparison

TypeLengthSecurityUse CaseExample
NUMERIC
4-10 digits
Medium
General purpose, easiest to enter123456
ALPHANUMERIC
4-10 chars
High
Financial transactions, high-securityA7B9X2K4
ALPHABETIC
4-10 letters
Low-Medium
Voice-based verificationABCDEF

Expiry Configuration

minutes
Default: 10

Range: 1-1440 minutes

Quick verifications

hours
Default:

Range: 1-1440 hours

Extended sessions (pass duration: "hours")

Message Template Variables

{code}

Generated OTP Code

Required placeholder - will be replaced with actual PIN

{amount}

Expiry Amount

Numeric value from expiry.amount

{duration}

Expiry Duration

"minutes" or "hours" from expiry.duration

Rate Limits & Security

3 requests

per phone per hour

Counts requests + resends together

30 seconds

resend cooldown

Applies to /resend, not the first request

1-10 attempts

per OTP

Configurable, defaults to 3

Response

JSON
{
"success": true,
"message": "OTP sent successfully",
"data": {
"id": "otp_123456789_abc",
"phone": "233555539152",
"pinLength": 6,
"pinType": "NUMERIC",
"expiry": {
"amount": 10,
"duration": "minutes",
"expiresAt": "2024-01-15T10:40:00.000Z"
},
"maxValidationAttempts": 3,
"createdAt": "2024-01-15T10:30:00.000Z",
"metadata": {
"userId": "usr_12345"
}
}
}

Try It Yourself

POST
https://api.sendexa.co/v1/otp/request

Implementation Examples

JavaScript
// Complete OTP request flow with error handling
class OTPManager {
constructor(apiKey, apiSecret) {
this.auth = 'Basic ' + btoa(apiKey + ':' + apiSecret);
this.baseUrl = 'https://api.sendexa.co/v1';
}
async requestOTP(phone, options = {}) {
const {
from = 'YourBrand',
message = 'Your verification code is {code}',
pinLength = 6,
pinType = 'NUMERIC',
expiry = { amount: 10, duration: 'minutes' },
maxAttempts = 3,
metadata = {}
} = options;
try {
const response = await fetch(`${this.baseUrl}/otp/request`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': this.auth
},
body: JSON.stringify({
phone: this.formatPhone(phone),
from,
message,
pinLength,
pinType,
expiry,
maxAmountOfValidationRetries: maxAttempts,
metadata
})
});
const data = await response.json();
if (!response.ok) {
// data.error is a flat string code, e.g. "RATE_LIMIT_EXCEEDED" — no nested details object.
throw new OTPError(data.message, data.error);
}
return {
success: true,
otpId: data.data.id,
expiresAt: data.data.expiry.expiresAt,
...data.data
};
} catch (error) {
if (error.code === 'RATE_LIMIT_EXCEEDED' || error.code === 'IP_RATE_LIMIT_EXCEEDED') {
// Fixed rolling-hour window — no retryAfter/resetAt is returned, so
// just surface the message and let the caller retry later.
return {
success: false,
rateLimited: true,
message: error.message
};
}
// Note: there's no "active OTP already exists" error — a new request
// silently invalidates any still-pending OTP for that phone.
throw error;
}
}
formatPhone(phone) {
// Remove any non-digits
const cleaned = phone.replace(/D/g, '');
// Convert to international format if needed
if (cleaned.startsWith('0')) {
return '233' + cleaned.substring(1);
}
return cleaned;
}
}
class OTPError extends Error {
constructor(message, code) {
super(message);
this.code = code;
}
}
// Usage with retry logic
async function initiateLogin(phone) {
const otpManager = new OTPManager('api_key', 'api_secret');
const result = await otpManager.requestOTP(phone, {
from: 'MyApp',
pinLength: 6,
expiry: { amount: 5, duration: 'minutes' },
metadata: { action: 'login' }
});
if (result.success) {
// Store OTP ID for verification
sessionStorage.setItem('otpId', result.otpId);
sessionStorage.setItem('expiresAt', result.expiresAt);
// Start countdown timer
startOTPTimer(result.expiresAt);
return { success: true };
} else if (result.rateLimited) {
return {
success: false,
message: result.message
};
}
}

Security Best Practices

Rate Limiting

Implement exponential backoff and never allow more than 3 requests per hour

Short Expiry

Use 5-10 minute expiry for most use cases. Shorter is more secure.

Attempt Limiting

Set max attempts to 3-5 to prevent brute force attacks

Metadata Tracking

Store userId, IP, and device info in metadata for audit trails

Error Handling Guide

RATE_LIMIT_EXCEEDED

Phone Rate Limit Hit

3 requests/hour per phone number, no retryAfter is returned — surface the message and let the user retry later.

IP_RATE_LIMIT_EXCEEDED

IP Abuse Signal

The same IP requested OTPs for 10+ distinct phone numbers in the last hour. Only triggers if you pass ipAddress in the request.

WHATSAPP_NOT_CONFIGURED

WhatsApp channel unavailable

Returned by /request/whatsapp when the business has no active WABA + approved authentication template. Fall back to SMS/Voice/Email.