WhatsApp OTP where the user writes first
One API call returns a link, the user sends the pre-filled message, your number answers with a 6-digit code. No cold sends, flat monthly rate, no per-message fees.
$10 per number / month · No per-message fees · Cancel anytime
How it works
The user writes first, WaAPI does the rest
Keyword, code, expiry and check run inside WaAPI. Your backend makes two API calls and stores one identity per user — to verify a number at sign-up, or as the second factor (2FA) at login.
-
1
Show a link
One API call returns a link. Your login page shows it, or a QR code of it, for the user on a computer.
-
2
The user sends it
The link opens a chat with your number and a short keyword. The user sends it and gets a 6-digit code back within seconds — in your own words, if you like.
-
3
You check the code
The user types the code in, a second API call checks it and returns a stable identity for that account — the same one every time they come back.
Safe for your number
Every code answers a message the user sent you. It is not a new conversation and doesn’t use the new-recipient budget, and nothing goes to anyone who didn’t ask for it.
Why not send codes to a typed-in number?
It works with the send-message action, but each code to someone who never wrote to you is a new conversation: by default up to 60 new recipients a day per number, and cold messages are the most common reason numbers get restricted. One restricted number stops every login at once — we don’t recommend it.
The code
Two API calls
The first call returns the link with a short keyword. The user sends it to your number and gets a 6-digit code back within seconds; the second call checks what they typed.
Switch verification on for the instance in your account. There you can also write the code message in your own words, with {code} where the digits go. Subscribe to otp_code_sent and otp_verified for push updates.
# 1. Start a verification. Show the link (or a QR code of it) on your login page.
curl --request POST \
--url https://waapi.app/api/v1/instances/123/otp \
--header 'authorization: Bearer your-api-token' \
--header 'content-type: application/json' \
--data '{"app_name": "Acme"}'
# 2. The user sends the pre-filled message and gets a 6-digit code back.
# Check the code they type in (request_id from step 1):
curl --request POST \
--url https://waapi.app/api/v1/instances/123/otp/REQUEST_ID/verify \
--header 'authorization: Bearer your-api-token' \
--header 'content-type: application/json' \
--data '{"code": "482913"}'
<?php
$apiToken = 'your-api-token';
$api = 'https://waapi.app/api/v1/instances/123';
function post(string $url, array $body, string $apiToken): array
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['authorization: Bearer ' . $apiToken, 'content-type: application/json'],
CURLOPT_POSTFIELDS => json_encode($body),
]);
return json_decode(curl_exec($ch), true);
}
// 1. Start a verification and show the link on your login page.
$request = post("$api/otp", ['app_name' => 'Acme'], $apiToken)['data'];
echo $request['link']; // keep $request['request_id'] with the login attempt
// 2. Check the code the user types in.
$result = post("$api/otp/{$request['request_id']}/verify", ['code' => $_POST['code']], $apiToken);
if ($result['data']['verified'] ?? false) {
$identity = $result['data']['identity']; // store it with your user
}
import requests
API = "https://waapi.app/api/v1/instances/123"
HEADERS = {"authorization": "Bearer your-api-token"}
# 1. Start a verification and show the link on your login page.
request = requests.post(f"{API}/otp", json={"app_name": "Acme"}, headers=HEADERS).json()["data"]
print(request["link"]) # keep request["request_id"] with the login attempt
# 2. Check the code the user types in.
result = requests.post(
f"{API}/otp/{request['request_id']}/verify", json={"code": "482913"}, headers=HEADERS
).json()
if result.get("data", {}).get("verified"):
identity = result["data"]["identity"] # store it with your user
const api = 'https://waapi.app/api/v1/instances/123';
const headers = { authorization: 'Bearer your-api-token', 'content-type': 'application/json' };
// 1. Start a verification and show the link on your login page.
const { data: request } = await fetch(`${api}/otp`, {
method: 'POST', headers, body: JSON.stringify({ app_name: 'Acme' }),
}).then((r) => r.json());
console.log(request.link); // keep request.request_id with the login attempt
// 2. Check the code the user types in.
const result = await fetch(`${api}/otp/${request.request_id}/verify`, {
method: 'POST', headers, body: JSON.stringify({ code: '482913' }),
}).then((r) => r.json());
if (result.data?.verified) {
const identity = result.data.identity; // store it with your user
}
Response
{
"data": {
"request_id": "01k8z3m4q5r6s7t8v9w0x1y2z3",
"status": "pending",
"mode": "register",
"token": "K7P3MX",
"message_text": "Code K7P3MX",
"link": "https://wa.me/15551234567?text=Code%20K7P3MX",
"expires_at": "2026-11-01T10:10:00+00:00",
"code_sent_at": null,
"latency_ms": null
}
}
Honest answer
When the official API is the better choice
Many sign-ups an hour
One instance runs up to 120 verifications an hour, and codes queue behind its other messages. Far beyond that, Meta’s Cloud API bills authentication messages per message, with sending tiers that rise with your volume.
Login can never fail
WaAPI connects your own number as an independent service, and a number that automates can be restricted. If a missing code locks users out of something critical, use an official channel — and keep a fallback either way.
The full comparison: Meta’s Cloud API vs. WaAPI.
Fallback
Always keep a second channel
Not every user has the app on the phone in front of them, and any channel can be down. Offer SMS or email as a second way next to the link.
The otp_code_sent webhook tells you the code went out. If it hasn’t come within half a minute of the user tapping the link, or they tap “send another way”, switch.
Flat rate, simple
No message quota and no per-message fees; new contacts are paced by send protection. Media, groups, channels, SDKs and MCP included. Cancel anytime.
- No per-message fees
- No per-user fees
- No setup fee
- Cancel anytime
OTP FAQ
Rules, numbers and what happens when something goes wrong.
-
Every code answers a message the user sent you to ask for it. You are still responsible for following the platform’s terms and the law where you operate, and for not using the channel for marketing they didn’t agree to.
-
Per instance up to 120 an hour, per account 1000. Per user and instance up to 3 codes in 10 minutes and 10 a day. Codes don’t count against the new-recipient limit.
-
The link is valid for 10 minutes and the code for 5 minutes, and each code works once.
-
Yes, per instance in your account: write your own text with {code} where the digits go, and optionally {minutes} and {app_name}. Leave the field empty for the built-in English or German text.
-
Yes, as the possession factor at login. Store the identity the first verification returns at sign-up; at login, start the request with that identity, and only the same WhatsApp account can pass. It is about as strong as an SMS code: use it next to the password, not instead of it, and keep backup codes or email, so a restricted number or a lost phone locks nobody out.
-
Answers to the user’s own message are the safest kind of message, but no number is immune. Then it can’t send until the restriction is lifted, which is why a fallback channel matters. Our guide on restrictions explains how they happen and how to get a number unlocked.
Try a code flow on your own number
Test free for 3 days · No credit card.