This guide is for developers building WhatsApp OTP flows for Indian businesses. It covers the actual integration steps, real API formats, template requirements, delivery tracking, and fallback architecture — not a conceptual overview of why WhatsApp OTP exists.

If you need the business case first, how WhatsApp OTP service works in India covers pricing, DLT compliance, and channel comparison. Come back here when you're ready to build.
What You Need Before Starting
An approved WhatsApp Business Account (WABA) via a BSP — MessageBot or another official Meta Business Solution Provider
Your API token from the MessageBot dashboard
An approved WhatsApp Authentication template — not a utility or marketing template
The recipient's phone number in international format (91XXXXXXXXXX for India)
The authentication template has strict format requirements. It must have exactly one variable slot for the OTP code, an optional expiry line, and an optional one-tap autofill button. No promotional content, no hyperlinks, no emojis. Meta's review is typically fast for correctly formatted authentication templates — rejections almost always come from format violations.
How the Flow Works
User action → Backend generates OTP → Store in cache with TTL
→ API call to WhatsApp → Message delivered → User submits code
→ Backend validates → Session authenticated
OTP generation — do this right:
import secrets
import redis
import time
def generate_otp(phone_number: str, expiry_seconds: int = 300) -> str:
# Cryptographically secure random OTP
otp = str(secrets.randbelow(900000) + 100000) # 6-digit OTP
# Store in Redis with TTL — not in relational database
r = redis.Redis()
key = f"otp:{phone_number}"
r.setex(key, expiry_seconds, otp)
return otp
def verify_otp(phone_number: str, submitted_otp: str) -> bool:
r = redis.Redis()
key = f"otp:{phone_number}"
stored_otp = r.get(key)
if not stored_otp:
return False # Expired or never generated
if stored_otp.decode() == submitted_otp:
r.delete(key) # Invalidate immediately after use
return True
return FalseUse Redis or Memcached with a TTL — not a relational database. Storing OTPs in a database adds unnecessary latency and persistence risk. Invalidate the OTP immediately after successful verification, not after expiry.
Sending WhatsApp OTP via MessageBot API
MessageBot's WhatsApp API uses a REST endpoint. Here's the actual request format:
POST Request
POST https://api.messagebot.in/v1/whatsapp/message
Content-Type: application/json
Authorization: Bearer YOUR_API_TOKEN
{
"to": "919876543210",
"type": "template",
"template": {
"name": "otp_verification",
"language": {
"code": "en"
},
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "849201"
}
]
},
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [
{
"type": "text",
"text": "849201"
}
]
}
]
}
}The button component is the one-tap autofill button — it passes the OTP value to the app automatically when the user taps. Include it. It reduces manual entry errors and support tickets.
Successful response:
{
"success": true,
"messageId": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSM...",
"status": "sent",
"to": "919876543210"
}Store the messageId — you need it to poll or receive delivery status.
Code Examples
Python
import requests
import json
def send_whatsapp_otp(phone_number: str, otp: str, api_token: str) -> dict:
url = "https://api.messagebot.in/v1/whatsapp/message"
payload = {
"to": phone_number,
"type": "template",
"template": {
"name": "otp_verification",
"language": {"code": "en"},
"components": [
{
"type": "body",
"parameters": [{"type": "text", "text": otp}]
},
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [{"type": "text", "text": otp}]
}
]
}
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_token}"
}
response = requests.post(url, json=payload, headers=headers)
return response.json()
# Usage
otp = generate_otp("919876543210")
result = send_whatsapp_otp("919876543210", otp, "YOUR_API_TOKEN")
message_id = result.get("messageId")
Node.js
const axios = require('axios');
async function sendWhatsAppOTP(phoneNumber, otp, apiToken) {
const payload = {
to: phoneNumber,
type: 'template',
template: {
name: 'otp_verification',
language: { code: 'en' },
components: [
{
type: 'body',
parameters: [{ type: 'text', text: otp }]
},
{
type: 'button',
sub_type: 'url',
index: '0',
parameters: [{ type: 'text', text: otp }]
}
]
}
};
const response = await axios.post(
'https://api.messagebot.in/v1/whatsapp/message',
payload,
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiToken}`
}
}
);
return response.data;
}PHP
<?php
function sendWhatsAppOTP(string $phoneNumber, string $otp, string $apiToken): array {
$payload = [
'to' => $phoneNumber,
'type' => 'template',
'template' => [
'name' => 'otp_verification',
'language' => ['code' => 'en'],
'components' => [
[
'type' => 'body',
'parameters' => [['type' => 'text', 'text' => $otp]]
],
[
'type' => 'button',
'sub_type' => 'url',
'index' => '0',
'parameters' => [['type' => 'text', 'text' => $otp]]
]
]
]
];
$ch = curl_init('https://api.messagebot.in/v1/whatsapp/message');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
"Authorization: Bearer $apiToken"
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}Tracking Delivery Status
WhatsApp delivery status comes via webhooks — configure your endpoint in the MessageBot dashboard.
Webhook payload structure:
{
"messageId": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgS...",
"status": "delivered",
"timestamp": "2026-06-30T10:22:04Z",
"to": "919876543210"
}Status values:
sent — message left your system
delivered — confirmed on recipient's device
read — recipient opened the message
failed — delivery failed
delivered is the status that matters for OTP retry logic. If you don't receive a delivered webhook within 30 seconds, trigger your fallback channel.
Webhook handler example (Node.js):
app.post('/webhook/whatsapp', (req, res) => {
const { messageId, status, to } = req.body;
if (status === 'delivered') {
// Mark OTP as delivered in your session store
markOTPDelivered(messageId);
}
if (status === 'failed') {
// Trigger SMS fallback immediately
triggerSMSFallback(to);
}
res.status(200).send('OK');
});Building the Fallback
WhatsApp OTP will fail for some users — no internet, WhatsApp not installed, account issues. Build the fallback before you go live, not after users complain.
import asyncio
async def send_otp_with_fallback(phone_number: str, otp: str):
# Attempt 1: WhatsApp OTP
whatsapp_result = await send_whatsapp_otp(phone_number, otp)
if not whatsapp_result.get('success'):
# Immediate fallback to SMS if WhatsApp send fails
return await send_sms_otp(phone_number, otp)
# Wait 30 seconds for delivery confirmation via webhook
delivered = await wait_for_delivery(
whatsapp_result['messageId'],
timeout=30
)
if not delivered:
# WhatsApp sent but not delivered — trigger SMS fallback
await send_sms_otp(phone_number, otp)
return whatsapp_resultThe SMS fallback uses MessageBot's SMS API with messageType: 3 for the OTP route. How the SMS OTP API works in India — including the correct DLT parameters — covers the SMS side of this.
For high-security flows where both WhatsApp and SMS fail, Voice OTP is the final layer. The full omnichannel OTP architecture for India covers how to design the complete cascade.
Rate Limiting and Abuse Prevention
An OTP endpoint without rate limiting is a cost vector for bots and an account takeover risk. Minimum protections before going live:
from functools import wraps
import redis
r = redis.Redis()
def rate_limit_otp(max_requests: int = 3, window_seconds: int = 600):
def decorator(func):
@wraps(func)
def wrapper(phone_number: str, *args, **kwargs):
key = f"otp_rate:{phone_number}"
current = r.incr(key)
if current == 1:
r.expire(key, window_seconds)
if current > max_requests:
raise Exception(f"Rate limit exceeded. Try again in {r.ttl(key)} seconds.")
return func(phone_number, *args, **kwargs)
return wrapper
return decorator
@rate_limit_otp(max_requests=3, window_seconds=600)
def request_otp(phone_number: str):
otp = generate_otp(phone_number)
send_whatsapp_otp(phone_number, otp, API_TOKEN)3 requests per number per 10 minutes is a reasonable default. Add exponential backoff on failed verification attempts — lock the account after 5 incorrect OTP submissions.
Authentication Template: What Meta Actually Approves
The template name in your API call must match exactly what's approved in your WhatsApp Business Manager. The approved template must be category: Authentication — not Utility or Marketing.
A correctly formatted authentication template:
{{1}} is your verification code. For your security, do not share this code.
With an optional expiry line:
{{1}} is your verification code. This code expires in 10 minutes.
Variables: {{1}} = the OTP value. That's the only variable allowed. Adding a second variable, a URL, or promotional language will get the template rejected or reclassified.
If your template was approved as Utility and you're using it for OTP delivery, you're being charged ₹0.115/message — same as authentication. But if Meta's systems reclassify it as Marketing, you'll see ₹0.8631/message charges. Check your template category in WhatsApp Business Manager.
Pre-Launch Checklist
Authentication template approved in WhatsApp Business Manager
Template category confirmed as Authentication (not Utility or Marketing)
API token stored in environment variables — never in source code
OTPs generated with secrets module (Python) or crypto (Node.js) — not random or Math.random()
OTPs stored in Redis/Memcached with TTL — not relational database
OTP invalidated immediately after successful verification
Rate limiting active — max 3 OTP requests per number per 10 minutes
Failed attempt counter with lockout after 5 wrong submissions
Webhook endpoint configured for delivery status
SMS fallback configured with 30-second trigger on non-delivery
Tested on actual devices — not just API response confirmation
One-tap autofill button included in template components
Common Errors and What They Mean
Error | Likely Cause | Fix |
template_not_found | Template name mismatch | Verify exact template name in Business Manager |
invalid_parameter | Wrong variable count or type | Check component structure matches approved template |
recipient_not_on_whatsapp | User doesn't have WhatsApp | Trigger SMS fallback immediately |
message_undeliverable | User blocked your number or account issue | Fallback to SMS, investigate account quality |
rate_limit_exceeded | Too many messages in short window | Implement exponential backoff |
Contact MessageBot support for WhatsApp API credentials, template submission, and endpoint reference specific to your account.