Use case: verification codes

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. 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. 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. 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.

Full guide in the API reference

# 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.

Your first instance is free
One instance, free for your first month — with code FREEINSTANCE, entered at checkout.
Flat rate

Flat rate, simple

$10 / month, per connected number

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.