WhatsApp OTP API
— no subscription
Send a one-time code to a WhatsApp number and check the code your user types, with two REST calls. PayPerWA generates, sends, expires and checks each code. You pay per OTP sent; verifying is free.
Per OTP sent
Meta ₹0.115+PayPerWA ₹0.20Verify: free
₹0.115 is Meta's list rate for an Indian number, billed by Meta to your WhatsApp Business Account. PayPerWA's ₹0.20 comes from your prepaid wallet. Price per OTP by country
- No DLT registration
- No subscription or setup fee
- No minimum monthly spend
Your App
Business account
482913 is your verification code. For your security, do not share this code.
This code expires in 10 minutes.
10:41
Sample message
Two calls: send a code, then check it
PayPerWA generates the code, sends it with your approved Authentication template, expires it and checks it. Your server never stores a code.
1POST
/api/v1/otp/send
Sends a fresh code to the number. Body fields (any other field is refused with 400):
to- Required. 10 to 15 digits starting with the country code, never 0. A leading + or 00, spaces and dashes are fine: +91 98765-43210 and 0091 98765 43210 are the same number.
code_length- 4 to 8 digits. Default 6.
expiry_minutes- 1 to 30. Default 10. If your template shows an expiry footer, send the same number.
template_name- Optional. Must be an approved Authentication template. Default: your most recently updated one.
language- Optional. Defaults to the template's language.
channelId- Optional, if you have more than one WhatsApp number. The X-Channel-Id header works too.
Returns verification_id, expires_at, message_id and a cost object. Its wallet_deduction is PayPerWA's fee; Meta bills its own fee to your WhatsApp Business Account, so it is not taken from your wallet. The code itself is never in a response.
2POSTFree
/api/v1/otp/verify
Send the code your user typed, as a string of digits, with either to or verification_id (exactly one). A valid, authorised request gets HTTP 200 whatever the outcome (auth, rate-limit and 503 errors aside); read status:
approved- valid: true. The code is now used up.
incorrect- Wrong code. attempts_remaining says how many tries are left.
max_attempts- The 5th wrong try. The code is cancelled; send a new one.
expired- Past expires_at (reported once, within 2 minutes of expiry). Send a new code.
not_found- No pending code: never sent, already used or cancelled, expired more than 2 minutes ago, or (by verification_id) replaced by a newer send.
Nothing charged
When a send is refused
These answers come back before anything is charged or sent, with success: false, an error message and a code:
- 429
OTP_COOLDOWN - A code went to this number less than 30 seconds ago.
- 429
OTP_TOO_MANY_SENDS - 5 codes went to this number in the last hour.
- 429
OTP_TOO_MANY_FAILED_CHECKS - 15 wrong codes were entered for this number in the last 24 hours.
- 400
OTP_TEMPLATE_REQUIRED - No approved Authentication template (on that channel, if you named one).
- 400
OTP_TEMPLATE_NOT_AUTHENTICATION - template_name is not an Authentication template.
- 400
OWN_NUMBER - to is the WhatsApp number you send from.
- 402
INSUFFICIENT_WALLET_BALANCE - Your wallet balance is below what the send costs. required and available come in your wallet's currency, named in currency.
- 503
OTP_UNAVAILABLE - Verification is briefly unavailable (send or verify). Try again shortly.
The 429s also give retry_after_seconds and a Retry-After header. A body that fails validation gets 400 Validation failed with details per field. If WhatsApp rejects the send itself, you get Meta's error, PayPerWA's fee goes back to your wallet, the attempt does not count toward the resend limits and an earlier code for the number stays valid. A contact who opted out of your messages does not block an OTP: that block covers marketing and utility templates, and the person asked for this code.
# 1. Send a code (6 digits, valid for 10 minutes unless you change it)
curl -X POST https://payperwa.com/api/v1/otp/send \
-H "Authorization: Bearer ppw_live_sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{"to": "919876543210", "code_length": 6, "expiry_minutes": 10}'
# 2. Check the code your user typed
curl -X POST https://payperwa.com/api/v1/otp/verify \
-H "Authorization: Bearer ppw_live_sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{"to": "919876543210", "code": "482913"}'// Node.js 18+ (built-in fetch). Run this on your server, never in the browser.
const BASE = "https://payperwa.com/api/v1";
const API_KEY = process.env.PAYPERWA_API_KEY; // ppw_live_sk_...
async function ppw(path, body) {
const res = await fetch(`${BASE}/${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const json = await res.json();
// e.g. 429 while the 30-second resend cooldown runs
if (!res.ok) throw new Error(`PayPerWA ${res.status}: ${JSON.stringify(json)}`);
return json.data;
}
// 1. Send a code when the user asks for one
async function sendOtp(phone) {
const { verification_id, expires_at } = await ppw("otp/send", { to: phone });
return { verification_id, expires_at };
}
// 2. Check the code the user typed
async function verifyOtp(phone, code) {
// code must be a string of digits, e.g. "048213" (a number would drop the leading zero)
const { valid, status, attempts_remaining } = await ppw("otp/verify", { to: phone, code: String(code).trim() });
// status: "approved" | "incorrect" | "max_attempts" | "expired" | "not_found"
return { valid, status, attempts_remaining };
}import os
import requests
BASE = "https://payperwa.com/api/v1"
API_KEY = os.environ["PAYPERWA_API_KEY"] # ppw_live_sk_...
def ppw(path, body):
res = requests.post(
f"{BASE}/{path}",
headers={"Authorization": f"Bearer {API_KEY}"},
json=body,
timeout=30,
)
if res.status_code != 200:
# e.g. 429 while the 30-second resend cooldown runs
raise RuntimeError(f"PayPerWA {res.status_code}: {res.text}")
return res.json()["data"]
# 1. Send a code when the user asks for one
def send_otp(phone):
data = ppw("otp/send", {"to": phone})
return data["verification_id"], data["expires_at"]
# 2. Check the code the user typed
def verify_otp(phone, code):
# code must be a string of digits, e.g. "048213"
data = ppw("otp/verify", {"to": phone, "code": str(code).strip()})
# status: approved | incorrect | max_attempts | expired | not_found
return data["valid"], data["status"]<?php
const PPW_BASE = "https://payperwa.com/api/v1";
function ppw(string $path, array $body): array {
$ch = curl_init(PPW_BASE . "/" . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("PAYPERWA_API_KEY"), // ppw_live_sk_...
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_TIMEOUT => 30,
]);
$resp = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($resp === false || $status !== 200) {
// e.g. 429 while the 30-second resend cooldown runs
throw new RuntimeException("PayPerWA $status: " . ($resp ?: "no response"));
}
return json_decode($resp, true)["data"];
}
// 1. Send a code when the user asks for one
function send_otp(string $phone): array {
return ppw("otp/send", ["to" => $phone]); // verification_id, expires_at, ...
}
// 2. Check the code the user typed
function verify_otp(string $phone, string $code): array {
// ["valid" => bool, "status" => approved | incorrect | max_attempts | expired | not_found]
return ppw("otp/verify", ["to" => $phone, "code" => trim($code)]);
}Responses
POST /otp/send · 200 (INR wallet, Meta-direct billing)
{
"success": true,
"data": {
"verification_id": "0b6f8f1e-6c1d-4f43-9a51-2f3c8f1d7a20",
"to": "919876543210",
"status": "pending",
"expires_at": "2026-11-09T10:50:00.000Z",
"code_length": 6,
"template_name": "login_code",
"message_id": "wamid.HBgLMTIzNDU2Nzg5MA==",
"cost": {
"meta_fee": 0,
"platform_fee": 0.2,
"wallet_deduction": 0.2,
"total_outlay": 0.2,
"billing_mode": "meta_direct",
"breakdown": "Meta bills customer directly + PayPerWA ₹0.20 fee",
"country": "IN",
"pricing_source": "meta_direct"
}
}
}POST /otp/verify · 200
{
"success": true,
"data": {
"valid": true,
"status": "approved",
"verification_id": "0b6f8f1e-6c1d-4f43-9a51-2f3c8f1d7a20"
}
}
{
"success": true,
"data": {
"valid": false,
"status": "incorrect",
"verification_id": "0b6f8f1e-6c1d-4f43-9a51-2f3c8f1d7a20",
"attempts_remaining": 3
}
}https://payperwa.com/api/v1 · header Authorization: Bearer ppw_live_sk_… · key permission messages:send · 100 requests a minute per key. Call the API from your server, never from browser JavaScript. Full reference in the API docs.Why send OTPs on WhatsApp
Fewer moving parts than SMS, and fewer digits to retype.
No DLT registration
SMS OTPs in India need DLT entity, header and template registration. On WhatsApp you need one approved Authentication template, which you create in the dashboard and Meta reviews.
A copy-code button
Every code arrives with a Copy code button, so your user copies it in one tap instead of retyping it.
Code suggestions on iPhone
On iOS 26 and later, iPhones can suggest numeric codes of 3 to 8 digits straight from the WhatsApp notification.
No SMS-pumping fees
SMS pumping bills you for floods of texts that earn attackers a share of carrier revenue. A WhatsApp OTP sends no SMS, so that fraud has nothing to pump.
Two things to know before you switch
- Your user needs WhatsApp on that number. PayPerWA has no SMS fallback, so keep another way to sign in (email, or your current SMS provider) for people without it.
- Meta requires opt-in. People must agree to receive messages from you on WhatsApp, so ask for that clearly in your sign-in flow.
Protections you would otherwise build yourself
Expiry, attempt limits and resend limits are part of the API, not homework.
Cryptographic codes
Every code comes from a cryptographically secure random number generator.
Stored only as a keyed hash
PayPerWA keeps a keyed hash, never the code itself, and never returns the code in an API response.
Codes expire
Each code lasts 1 to 30 minutes, 10 by default. After that it cannot be used.
Single use
A code is deleted the moment it is approved. A new code replaces the previous one as soon as Meta accepts it; if the send is refused with an error, the previous code still works.
Limits on wrong guesses
5 wrong tries cancel a code. After 15 wrong codes for one number in 24 hours, no new code is sent to it until fewer than 15 remain in that window.
Resend limits, before any charge
30 seconds between sends to the same number and at most 5 an hour. A blocked request returns 429 with retry_after_seconds and a Retry-After header, and costs nothing.
WhatsApp does not stop bots
A bot can still submit your sign-up form and make you send codes you pay for. The limits above stop repeated sends to one number, not sends to many different numbers, so add a CAPTCHA and your own rate limits to the form that requests a code. What you do avoid is SMS pumping (carrier revenue-share fraud), because no SMS is sent.
Two numbers. That's the price.
Per OTP sent you pay Meta's authentication fee and PayPerWA's fee, shown separately. Checking a code costs nothing.
Meta's authentication fee
₹0.115
Meta's list rate per OTP to an Indian number. Billed by Meta to the payment method on your WhatsApp Business Account. Rates by country →
PayPerWA fee
₹0.20
Per OTP sent, from your prepaid PayPerWA wallet, whatever the recipient's country.
- FreeVerifying a code
- NoneSubscription
- NoneSetup fee
- NoneMinimum monthly spend
Example: 1,000 codes to Indian numbers cost Meta ₹115 (billed by Meta) + PayPerWA ₹200 (from your wallet). Minimum wallet recharge ₹750.
Price per OTP by country
Meta's authentication fee depends on the recipient's country. PayPerWA's fee depends only on your wallet's currency. Verifying is free everywhere.
- Indian accounts (INR wallet): an OTP to an Indian number costs Meta ₹0.115 + PayPerWA ₹0.20 = ₹0.315. To other countries PayPerWA's fee stays ₹0.20 and Meta charges that country's rate.
- Accounts outside India (USD wallet): Meta's fee from the table + PayPerWA $0.004 per OTP sent.
| Recipient's country | Meta's authentication fee | + PayPerWA (USD wallet) | = Per OTP sent |
|---|---|---|---|
| India | $0.0014 | $0.004 | $0.0054 |
| Nigeria | $0.0067 | $0.004 | $0.0107 |
| Indonesia | $0.025 | $0.004 | $0.029 |
| Saudi Arabia | $0.0107 | $0.004 | $0.0147 |
| United Arab Emirates | $0.0157 | $0.004 | $0.0197 |
| Brazil | $0.0068 | $0.004 | $0.0108 |
| Mexico | $0.0085 | $0.004 | $0.0125 |
| United States | $0.0034 | $0.004 | $0.0074 |
| Egypt | $0.0036 | $0.004 | $0.0076 |
| United Kingdom | $0.022 | $0.004 | $0.026 |
| South Africa | $0.0095 | $0.004 | $0.0135 |
| Kenya | $0.004 | $0.004 | $0.008 |
Meta's list rates from its USD rate card effective 1 October 2026, billed by Meta to your WhatsApp Business Account (in the currency that account is billed in). Meta charges per delivered message, and PayPerWA refunds its own fee if Meta rejects a send or reports it as failed. Meta can change rates only on 1 January, 1 April, 1 July or 1 October. Very high-volume senders messaging users in certain other countries can become eligible for Meta's higher authentication-international rates; see the WhatsApp OTP API guide. Every other country is on the rates page.
Set it up in four steps
One template, one API key, two calls.
- 1
Create your account
Sign up free, connect your WhatsApp Business number with Meta's embedded signup and top up your wallet (minimum ₹750).
- 2
Create an Authentication template
Dashboard → Templates → New → Authentication. Meta reviews it before you can send. Your Meta business must be verified first: PayPerWA stops the submission and links you to Meta's business verification if Meta reports it as not verified or still pending.
- 3
Get an API key
Dashboard → Settings → API. Use a key with the
messages:sendpermission and keep it on your server. - 4
Call send, then verify
/otp/sendwhen the user asks for a code,/otp/verifywhen they type it in.
The message Meta allows
Authentication templates use wording fixed by Meta, so there is no message copy for you to write.
- Body: <code> is your verification code.
- Optional: "For your security, do not share this code."
- Optional footer with the expiry time
- A Copy code button
- No links, media or emoji
The PayPerWA dashboard creates copy-code templates only. Meta's one-tap and zero-tap autofill are Android-only, and PayPerWA does not offer them.
731905 is your verification code. For your security, do not share this code.
This code expires in 5 minutes.
Sample of the fixed format
WhatsApp OTP API questions
Something else? Talk to us.
How much does a WhatsApp OTP cost with PayPerWA?
Do I need DLT registration to send OTPs on WhatsApp in India?
What if my user does not have WhatsApp?
Does PayPerWA support one-tap or zero-tap autofill?
How long is a code valid, and how many attempts does a user get?
Does WhatsApp OTP stop bots and fake sign-ups?
Do I need to generate or store the codes myself?
Can I send OTPs to numbers outside India?
How does PayPerWA compare with Twilio Verify?
WhatsApp OTP guides
Costs, templates, code and security, in more depth.
WhatsApp OTP API in India: How It Works, Costs and Setup
Read the guideWhatsApp OTP vs SMS OTP in India: An Honest Comparison
Read the guideWhatsApp Authentication Templates: Copy Code, One-Tap and Zero-Tap Explained
Read the guideAdd WhatsApp OTP Login to Your App (Node.js, Python, PHP)
Read the guideOTP Security Best Practices for WhatsApp and SMS
Read the guide
Add WhatsApp OTP to your sign-in
Meta's authentication fee + PayPerWA ₹0.20 per OTP sent. Verifying is free. No subscription, no setup fee.


