How to Send WhatsApp OTP in Node.js Without Getting Your Number Banned

SMS delivery in Pakistan and India is increasingly broken. Between strict Do-Not-Disturb registries, fluctuating operator routes, and high per-message costs, your users often wait minutes for a critical login OTP. Many times, the message never arrives at all. Moving your verification codes to WhatsApp solves this reliability issue, but if you try to build this using web-scraping libraries or unofficial APIs, Meta will ban your virtual or physical SIM card within hours.
To send OTPs reliably at scale, you must use the official WhatsApp Business Platform Cloud API. This guide walks you through setting up the official API, structuring your Node.js backend, handling the strict template rules imposed by Meta, and dealing with real-world API errors.
The Reality of WhatsApp OTPs: Costs and Templates
Before writing code, you need to understand how Meta structures and charges for OTP messages. You cannot send a plain text message like "Your OTP is 1234" to a user who has not messaged you first. Meta blocks this to prevent spam.
Instead, you must use an approved Authentication Template. These templates are specifically designed for verification codes. They require a distinct format, usually featuring a "Copy Code" button or a one-tap autofill button for mobile users. Meta reviews these templates automatically, a process that usually takes under two minutes.
Pricing is based on 24-hour conversation windows. When you send an OTP, you open a "Utility" conversation. You pay a flat rate for that 24-hour window, during which you can send multiple messages to that user if needed. In India, a utility conversation costs roughly 0.11 INR. In Pakistan, it is about 0.35 PKR. These rates fluctuate based on Meta's official exchange rate sheets, but they are often comparable to or cheaper than high-priority transactional SMS routes.
What You Need Before Writing Node.js Code
To interact with the official API, you must complete a few administrative steps. Skipping these will prevent your code from working outside of a restricted developer sandbox.
- A Meta Developer Account: Register at developers.facebook.com.
- A WhatsApp Business App: Created within your Meta developer console.
- A Dedicated Phone Number: This number must not be active on a personal or business WhatsApp mobile app. If it is, you must delete the account from your phone first.
- A Payment Method: Meta requires a credit card on file in your Business Portfolio to pay for conversation charges, even during testing.
Step 1: Creating Your OTP Template
Navigate to your WhatsApp Manager within the Meta Business Suite. Under the Message Templates section, create a new template with the following configurations:
- Category: Authentication
- Template Name:
otp_verification_code - Language: English (or your preferred local language)
- Button Type: Copy Code
Meta will automatically generate the template body for you. It typically reads: "Your verification code is {{1}}. For your security, do not share this code." The double curly braces represent the variable where your Node.js application will inject the dynamic one-time password.
Step 2: The API Payload Structure
Because we cannot use standard markdown code blocks here, the table below outlines the exact JSON payload structure your Node.js application must send to the Meta Graph API endpoint. The API endpoint URL is: https://graph.facebook.com/v18.0/YOUR_PHONE_NUMBER_ID/messages.
| JSON Key Path | Type | Value / Example | Purpose |
|---|---|---|---|
| messaging_product | String | "whatsapp" | Identifies the Meta product. Always set to "whatsapp". |
| to | String | "923001234567" | Recipient number with country code, no leading zeros or + sign. |
| type | String | "template" | Specifies that we are sending a pre-approved template. |
| template.name | String | "otp_verification_code" | The exact name of your approved Meta template. |
| template.language.code | String | "en_US" | The language code matching your approved template. |
| template.components[0].type | String | "body" | Targeting the body section of the template. |
| template.components[0].parameters[0].type | String | "text" | The type of dynamic variable we are injecting. |
| template.components[0].parameters[0].text | String | "582910" | The actual dynamic OTP generated by your server. |
| template.components[1].type | String | "button" | Targeting the button component of the template. |
| template.components[1].sub_type | String | "url" | Specifies the button triggers a URL action (copy code mechanism). |
| template.components[1].index | String | "0" | The position index of the button (first button is 0). |
| template.components[1].parameters[0].type | String | "text" | Injects the OTP directly into the copy-to-clipboard action. |
| template.components[1].parameters[0].text | String | "582910" | Must match the OTP code sent in the body. |
Step 3: Writing the Node.js Code
We will use Node's native https module to make the API call. This eliminates external package dependencies like Axios, reducing your production bundle size and security vulnerabilities. Save the following logic inside a file named sendOtp.js.
Define the Request Options:
Set up your authorization headers. You need a permanent System User Token from your Meta Business Suite. Do not use the temporary 24-hour token from the developer dashboard in production, as your backend will stop working the next day.
Construct the Payload:
Build an object matching the table structure above. Ensure your recipient's phone number is sanitized to contain only numbers.
Send the Request:
Write the HTTPS POST request, handle the response stream, and catch potential network failures.
Here is the functional Node.js implementation:
const https = require('https');
function sendWhatsAppOtp(recipientPhone, otpCode) {
const phoneNumberId = 'YOUR_META_PHONE_NUMBER_ID';
const accessToken = 'YOUR_META_PERMANENT_ACCESS_TOKEN';
const payload = JSON.stringify({
messaging_product: 'whatsapp',
to: recipientPhone,
type: 'template',
template: {
name: 'otp_verification_code',
language: { code: 'en_US' },
components: [
{
type: 'body',
parameters: [{ type: 'text', text: otpCode }]
},
{
type: 'button',
sub_type: 'url',
index: '0',
parameters: [{ type: 'text', text: otpCode }]
}
]
}
});
const options = {
hostname: 'graph.facebook.com',
path: `/v18.0/${phoneNumberId}/messages`,
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload)
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
const response = JSON.parse(data);
if (res.statusCode === 200) {
console.log('OTP sent successfully:', response.messages[0].id);
} else {
console.error('Meta API Error:', response.error.message);
}
});
});
req.on('error', (error) => {
console.error('Network request failed:', error.message);
});
req.write(payload);
req.end();
}
// Example execution: sendWhatsAppOtp('923001234567', '482019');
What Will Not Work (And What to Avoid)
When building production auth systems, developers often make assumptions that lead to failed audits or blocked accounts. Here is what you should avoid:
1. Web-Scraping Libraries
Libraries like whatsapp-web.js or baileys run a headless browser instance in the background to control a real WhatsApp Web session. This is fine for home automation, but a disaster for production OTPs. Meta continuously updates its web client code. When they do, these libraries break instantly. More importantly, sending automated, unsolicited messages via these libraries triggers Meta's anti-spam algorithms, resulting in a permanent hardware and number ban within hours.
2. Sandbox Numbers for Production Launch
When you set up a Meta developer account, they assign you a test phone number. This number can only send messages to up to 5 verified developer phone numbers. If you attempt to run a production signup flow with this test number, your users will receive nothing, and your API calls will fail with a 131030 error.
3. Unverified Business Portfolios
While Meta allows you to send messages immediately after adding a phone number, your account will be in a limited tier. You can only initiate conversations with 250 unique customers in a rolling 24-hour period. To scale beyond this, you must complete business verification in your Meta Business Suite by uploading registration documents matching your company's physical address and legal name.
4. Using a Personal Number
You cannot use a number that is currently active on your personal WhatsApp app on your phone. If you register that number to the Cloud API, the mobile app will log you out, and you will lose your chat history. The number becomes strictly an API-controlled asset.
Handling Common API Error Codes
Your Node.js backend must gracefully handle errors returned by Meta's API. If you do not catch these, uncaught exceptions will crash your Node process, taking down your entire login flow.
- Error 100 (Invalid Parameter): This usually means your recipient phone number is formatted incorrectly. Ensure you strip out spaces, dashes, and leading zeros. The number must start directly with the country code (e.g., 92 for Pakistan, 91 for India).
- Error 131030 (Recipient Not in Sandbox Allowlist): You are trying to send an OTP to a real user using a Meta developer test number. You must register a real, clean phone number in your console and add a payment method to resolve this.
- Error 131026 (Message Undeliverable): The destination number does not have an active WhatsApp account, or the user has blocked your business number. When this happens, your Node.js application should immediately fall back to a traditional SMS gateway to ensure the user is not locked out.
How WA Link Simplifies This Setup
Configuring Meta Business Managers, managing credit card limits for micro-transactions across borders, and setting up permanent system user tokens can be a massive administrative burden, especially for businesses in Pakistan. At WA Link, we simplify this process. We provide direct API access, local billing options, and guided template approval to bypass the complex developer console setups. However, we do not bypass Meta's platform rules: you still cannot use unofficial scraping tools, and you must use approved authentication templates to send verification codes.
Frequently Asked Questions
Do I get free messages for OTPs?
No. Meta previously offered 1,000 free service conversations per month, but this free tier no longer applies to Utility (OTP) or Marketing conversations. Every single OTP conversation you initiate will be charged according to Meta's regional utility rates from the first message.
How fast do WhatsApp OTPs deliver compared to SMS?
Over the official Cloud API, delivery is almost instantaneous, typically taking between 1.5 to 3 seconds. Traditional SMS in South Asia can take anywhere from 10 seconds to several minutes during peak network congestion hours.
How should I store the OTP on my backend for verification?
Do not store OTPs in your primary relational database. Instead, use an in-memory store like Redis. When you generate the OTP in Node.js, save it in Redis with the user's phone number as the key, and set a strict Time-To-Live (TTL) of 5 minutes. When the user submits the code, compare it against the Redis value and delete the key immediately upon successful verification.
Can I customize the text of the OTP message?
Only within the constraints of the Authentication template. You cannot add promotional text, links, or custom greetings to an authentication template. Meta will reject any template in this category that contains anything other than the standard verification wording and the dynamic token placeholder.
What happens if a user replies to the OTP message?
If a user replies, it opens a "Service" conversation window. You can handle these incoming messages by setting up a Webhook in your Node.js application to receive the incoming payload from Meta. If you do not set up a webhook, the user's reply will simply be ignored by your system.
Your Next Step
To get started, go to developers.facebook.com, create a developer account, and set up a temporary test number to run this Node.js script in your local environment.