Send WhatsApp OTPs with one API call
WA Gateway sends verification codes from your own WhatsApp number to your users. Link your phone once, create an API key, and send a code with a single HTTP request.
https://ars-whatsapp-api.onrender.com- One endpoint. POST /send-otp is all you need. No templates to register, no approval process.
- Your own number. Messages come from the WhatsApp number you link, not an unknown sender.
- One-tap copy. Every message has a Copy Code button, so users never retype the code.
curl -X POST "https://ars-whatsapp-api.onrender.com/send-otp" \
-H "x-api-key: $WA_GATEWAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"phoneNumber": "+8801712345678",
"otp": "482910"
}'{
"statusCode": 200,
"success": true,
"message": "OTP sent successfully",
"data": {
"messageId": "3EB0A1F4C29D7E83B6105F",
"recipientJid": "8801712345678@s.whatsapp.net"
}
}How it works
When you link your phone, WA Gateway becomes one of its Linked Devices, just like WhatsApp Web. Each OTP you send goes out from that number.
- STEP 1Your server
Generates a code and calls POST /send-otp
- STEP 2WA Gateway
Checks your API key and formats the number
- STEP 3Your WhatsApp
Your linked phone sends the message
- STEP 4User's phone
Receives the code with a Copy Code button
Quickstart
Send your first OTP in about two minutes.
- 1Create an account
Sign up and verify your email.
- 2Link your WhatsApp
In the dashboard, open WhatsApp Device. On your phone, open WhatsApp → Settings → Linked Devices → Link a device and scan the QR code. The status turns green when it is connected.
- 3Create an API key
Open API Keys → Generate. Save the key in an environment variable on your server, not in your code.
# Keep this file out of git WA_GATEWAY_KEY=_ars_2k03_a9f1b8c2d3e4f5061728394a5b6c7d8e9f0123456789abcd - 4Send an OTP
Replace the number with your own and run it.
curl -X POST "https://ars-whatsapp-api.onrender.com/send-otp" \ -H "x-api-key: $WA_GATEWAY_KEY" \ -H "Content-Type: application/json" \ -d '{ "phoneNumber": "+8801712345678", "otp": "482910" }' - 5Check your phone
The code arrives on WhatsApp within a few seconds. The request also shows up in Message Logs in the dashboard.
Authentication
Every request needs an API key. Send it in the x-api-key header, or as Authorization: Bearer <key>. Both work the same way.
Keys start with _ars_2k03_ followed by 48 characters. You can create several keys, for example one per app or environment, and they all send from your linked number.
Revoking a key
Delete a key in API Keys and it stops working right away. Requests that use it get 401 Unauthorized.
x-api-key: _ars_2k03_a9f1b8c2d3e4f5061728394a5b6c7d8e9f0123456789abcdPhone number format
Send the number with its country code. Spaces, dashes, brackets and + are removed for you, and Bangladeshi local numbers that start with 0 get 880 added. After cleaning, the number must have 10 to 15 digits.
| YOU SEND | SENT TO | RESULT |
|---|---|---|
| +8801712345678 | 8801712345678 | ✓ Works |
| 01712345678 | 8801712345678 | ✓ Works (Bangladeshi local format) |
| +880 1712-345678 | 8801712345678 | ✓ Works |
| +14155550123 | 14155550123 | ✓ Works (any country) |
| 12345 | — | ✗ 400: invalid number (needs 10–15 digits) |
Rate limits
Each IP address can make 30 requests per minute. Extra requests get 429 Too Many Requests. Wait a minute, then try again.
{
"statusCode": 429,
"message": "ThrottlerException: Too Many Requests"
}Send an OTP
Sends a verification code to a WhatsApp number. The user gets a message with the code and a one-tap Copy Code button. A 200 response means WhatsApp accepted the message.
Headers
Authorization: Bearer <key> instead.application/jsonBody parameters
"482910"). It is shown in bold and copied by the button. Your app generates and checks it.Other fields in the body are ignored.
Response fields
200 on success.true on success."OTP sent successfully"8801712345678@s.whatsapp.net. Shows which number was used after formatting.curl -X POST "https://ars-whatsapp-api.onrender.com/send-otp" \
-H "x-api-key: $WA_GATEWAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"phoneNumber": "+8801712345678",
"otp": "482910"
}'{
"statusCode": 200,
"success": true,
"message": "OTP sent successfully",
"data": {
"messageId": "3EB0A1F4C29D7E83B6105F",
"recipientJid": "8801712345678@s.whatsapp.net"
}
}Verify OTP codes
WA Gateway only delivers the code, so checking it is up to your app. A safe flow has two parts:
- Send: generate a random 6-digit code, save a hash of it with an expiry time, then call
POST /send-otp. - Verify: when the user types the code, compare its hash with the saved one. Delete it after success, after it expires, or after too many wrong tries.
Why these rules
crypto.randomInt or secrets, not Math.random, so codes cannot be guessed.import crypto from "node:crypto";
// Use Redis or your database in production.
const codes = new Map();
const hash = (code) => crypto.createHash("sha256").update(code).digest("hex");
// 1. Generate a code, remember it, and send it.
export async function sendLoginCode(phoneNumber) {
const otp = crypto.randomInt(100000, 1000000).toString();
codes.set(phoneNumber, {
hash: hash(otp),
expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes
attempts: 0,
});
const response = await fetch("https://ars-whatsapp-api.onrender.com/send-otp", {
method: "POST",
headers: {
"x-api-key": process.env.WA_GATEWAY_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ phoneNumber, otp }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
}
// 2. Check the code the user typed in.
export function verifyLoginCode(phoneNumber, code) {
const entry = codes.get(phoneNumber);
if (!entry || Date.now() > entry.expiresAt) return false;
entry.attempts += 1;
if (entry.attempts > 5) {
codes.delete(phoneNumber); // too many wrong tries
return false;
}
const ok = crypto.timingSafeEqual(
Buffer.from(hash(code)),
Buffer.from(entry.hash),
);
if (ok) codes.delete(phoneNumber); // a code works only once
return ok;
}Handle errors
Errors fall into two groups, and your app should treat them differently:
Show a friendly message: "Check your number" or "Please wait a minute and try again".
Your key is wrong or your WhatsApp is disconnected. Retrying will not help. Alert your team and show the user a general "try again later" message.
const response = await fetch("https://ars-whatsapp-api.onrender.com/send-otp", {
method: "POST",
headers: {
"x-api-key": process.env.WA_GATEWAY_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ phoneNumber, otp }),
});
const body = await response.json();
if (response.ok) return body.data.messageId;
// Validation errors return a list of messages, others a single string.
const message = Array.isArray(body.message) ? body.message.join(", ") : body.message;
switch (response.status) {
case 400:
// Bad number or not on WhatsApp: ask the user to check their number.
throw new UserInputError(message);
case 429:
// Too many requests: wait a minute before trying again.
throw new RetryLaterError(message);
case 401:
case 503:
// Wrong key, or your WhatsApp is disconnected: alert your team.
// Retrying will not help until it is fixed.
notifyTeam(message);
throw new Error("Could not send the code. Please try again later.");
default:
throw new Error(message);
}Error codes
Errors return a non-2xx status and a JSON body. message explains what went wrong; for missing or invalid fields it is a list of messages.
| STATUS | MEANING | WHAT TO DO |
|---|---|---|
| 400 | phoneNumber or otp is missing or not a string | Send both fields as strings. |
| 400 | Invalid phone number | Use 10–15 digits with the country code. |
| 400 | Number is not registered on WhatsApp | Ask the user for a WhatsApp number. |
| 401 | API key missing, invalid or revoked | Check the header and that the key still exists. |
| 429 | More than 30 requests in a minute | Wait a minute, then retry. |
| 503 | Your WhatsApp is not connected | Dashboard → WhatsApp Device → scan the QR again. |
{
"message": [
"OTP code is required"
],
"error": "Bad Request",
"statusCode": 400
}What users receive
This is exactly how the message looks in the user's WhatsApp. Try the Copy Code button: it copies the code, just like on a real phone.
- Sender
Your linked WhatsApp number. Users see your name instead if they saved you as a contact.
- The code
Your otp value, in bold, followed by a short security warning.
- Footer and time
A fixed note that the code expires in 5 minutes, and when it arrived.
- Copy Code button
One tap copies the code to the clipboard, so users can paste it into your app without typing.
For your security, do not share this code.
Message logs
Every successful send is recorded in the dashboard under Message Logs, with the recipient, the time, the messageId and the status. You can search by phone number or message ID. Use it to check whether a code was sent when a user says they did not get one. The last 100 messages are shown.
Best practices
- ✓ Call from your server
Keep your API key in an environment variable on the server.
- ✓ Limit requests per user
Allow one code per user every 60 seconds to stop abuse.
- ✓ Expire codes after 5 minutes
Matches the message users see.
- ✓ Use one key per app
If one leaks, you revoke only that key.
- ✓ Watch for 503
It means your phone was unlinked. Relink it quickly so logins keep working.
- ✓ Keep the device linked
If you log out of WhatsApp or remove the linked device, sending stops with 503.
FAQ
Can I change the message text?+
Not yet. Every OTP uses the same template shown in "What users receive". Only the code changes.
Can I send to numbers outside Bangladesh?+
Yes. Send the number with its country code, for example +14155550123. Any number registered on WhatsApp works.
Can I link more than one WhatsApp number?+
No. One account links exactly one WhatsApp number, and every API key on the account sends from it. Use a separate account for a second number.
Does WA Gateway check if the code the user typed is correct?+
No. WA Gateway only delivers the code. Your app creates it, stores it and checks it. See "Verify OTP codes" for a complete example.
Is there a test mode?+
No. Every request sends a real WhatsApp message. Use your own number while testing, or the Message Tester in the dashboard.
Can I call the API from my website or mobile app directly?+
No. Browsers are blocked by CORS, and putting your key in client code would let anyone send messages from your number. Always call it from your server.