Resend OTP
Resend an OTP code while preserving the original configuration. Perfect for handling cases where users didn't receive the code or need a fresh one with the same settings.
Preserve Configuration
Resend uses the same PIN length, type, and expiry as original request
Reset Expiry
New OTP gets fresh expiry window, old OTP is automatically invalidated
Cooldown Enforcement
Respects same rate limits as initial request to prevent abuse
Attempt Tracking
Resets verification attempt counter, maintains original max attempts
Authentication Required
/verify call, not on resend itself.Resend Behavior
phoneis required;idis optional and targets a specific pending OTP for that phone- Reuses the original PIN length, type, and expiry duration
- 30-second cooldown between resends for the same phone/id
- Verification attempt counter resets for the new code
- Counts toward the same 3-per-hour phone rate limit as fresh requests
One Endpoint, Any Channel
POST /v1/otp/resend takes the same optional channel field as request — a single value, an array for fallback, or omit it to resend on SMS as before. The channel-specific URLs — /resend/voice, /resend/whatsapp, /resend/email — still work exactly as before (email takes the address in recipient.value or legacy phone).30s
Between resends
3
Per hour per phone
Fresh
New timer starts
Reset
Counter starts fresh
Request Body
{"phone": "0555539152","id": "otp_123456789_abc","channel": "sms"}
recipient: { "value": "0555539152" } also works in place of phone. channel is optional — omit it and it resends on whatever channel the original OTP would default to (SMS, unless you hit one of the channel-specific URLs).
Response
{"success": true,"message": "OTP resent successfully","data": {"id": "otp_123456790_xyz","originalId": "otp_123456789_abc","phone": "233555539152","pinLength": 6,"pinType": "NUMERIC","expiry": {"amount": 10,"duration": "minutes","expiresAt": "2024-01-15T10:45:00.000Z"},"maxValidationAttempts": 3,"attemptsUsed": 0,"createdAt": "2024-01-15T10:35:00.000Z"}}
Common Resend Scenarios
User didn't receive code
Action
Resend with phone only
Result
New code sent, old invalidated
Code expired
Action
Resend with phone or ID
Result
Fresh code with new expiry
Too many wrong attempts
Action
Resend required
Result
Reset attempt counter, new code
Cooldown Rules
| Condition | Cooldown | Hourly Limit |
|---|---|---|
| Initial request | None | 3 |
| First resend | 30 seconds | 3 |
| Subsequent resends | 30 seconds | 3 |
Try It Yourself
https://api.sendexa.co/v1/otp/resendImplementation Examples
// Resend OTP with countdown and error handlingclass OTPResendManager {constructor(apiKey, apiSecret) {this.auth = 'Basic ' + btoa(apiKey + ':' + apiSecret);this.baseUrl = 'https://api.sendexa.co/v1';this.cooldownTimer = null;}// phone is always required by the API; id is optional and only narrows// which pending OTP for that phone gets resent.async resendOTP(phone, id, options = {}) {try {const response = await fetch(`${this.baseUrl}/otp/resend`, {method: 'POST',headers: {'Content-Type': 'application/json','Authorization': this.auth},body: JSON.stringify({ phone, id })});const data = await response.json();if (!response.ok) {// data.error is a flat string code — no nested details object,// no retryAfter. The 30s cooldown is a fixed, known constant.if (data.error === 'RESEND_COOLDOWN') {this.startCooldown(30, () => {if (options.onCooldownEnd) options.onCooldownEnd();});return {success: false,cooldown: true,message: data.message};}return {success: false,error: data.error || 'UNKNOWN_ERROR',message: data.message};}// Success - new OTP createdreturn {success: true,otpId: data.data.id,expiresAt: data.data.expiry.expiresAt,data: data.data};} catch (error) {return {success: false,error: 'NETWORK_ERROR',message: error.message};}}startCooldown(seconds, onEnd) {if (this.cooldownTimer) {clearInterval(this.cooldownTimer);}let remaining = seconds;this.cooldownTimer = setInterval(() => {remaining--;if (remaining <= 0) {clearInterval(this.cooldownTimer);if (onEnd) onEnd();}// Dispatch event for UI updateswindow.dispatchEvent(new CustomEvent('otpCooldown', {detail: { remaining }}));}, 1000);}}// Usage with React componentfunction ResendButton({ phone, onResend }) {const [cooldown, setCooldown] = useState(0);const [loading, setLoading] = useState(false);const manager = useRef(new OTPResendManager('api_key', 'api_secret'));useEffect(() => {const handleCooldown = (e) => {setCooldown(e.detail.remaining);};window.addEventListener('otpCooldown', handleCooldown);return () => window.removeEventListener('otpCooldown', handleCooldown);}, []);const handleResend = async () => {setLoading(true);const result = await manager.current.resendOTP(phone, undefined, {onCooldownEnd: () => {setCooldown(0);}});setLoading(false);if (result.success) {onResend?.(result.otpId);toast.success('New code sent!');} else if (result.cooldown) {toast.info(result.message);} else {toast.error(result.message);}};return (<buttononClick={handleResend}disabled={loading || cooldown > 0}className="text-blue-600 disabled:opacity-50">{loading ? 'Sending...' :cooldown > 0 ? `Resend in ${cooldown}s` :'Resend Code'}</button>);}
Resend Best Practices
Do's
- ✓ Show cooldown timer in UI
- ✓ Disable resend button during cooldown
- ✓ Clear error messages on successful resend
- ✓ Reset verification input field
Don'ts
- ✗ Allow unlimited resend attempts
- ✗ Hide cooldown information from users
- ✗ Resend without invalidating old code
- ✗ Ignore the 3-per-hour phone rate limit
Error Handling Tips
- RESEND_COOLDOWN: Show a fixed 30s countdown to the user (no retryAfter is returned)
- OTP_ALREADY_VERIFIED / OTP_BLOCKED / OTP_NOT_FOUND: Redirect to request a new OTP
- RATE_LIMIT_EXCEEDED: Stop resending — the phone has hit 3 requests/hour
- NETWORK_ERROR: Retry with increasing delays