WhatsApp OTP
Prove that someone holds a WhatsApp number, for a sign-up or a sign-in without a password. There are two ways; use the link unless you must send the code yourself.
1. The link (recommended): the person sends you the code
Nothing is sent to someone who did not write first, so your instance only ever replies: the safest message there is, with no daily limit, no night hours and no waiting.
POST /v1/otps
{"instance_id": "01K…", "channel": "link", "brand": "Acme", "reference": "signup-7", "locale": "en"}
201 {"data": {"id": "01K…", "channel": "link", "status": "pending", "code": "48291376",
"link": "https://wa.me/201000000001?text=My%20Acme%20verification%20code%3A%2048291376",
"qr": "data:image/png;base64,…", "expires_at": "2026-10-04T12:10:00+00:00", "…": "…"}}
- Show the
linkas a button on a phone, or theqrimage on a computer, with thecodeas a fallback they can type. - They send the message. When it reaches your instance before
expires_at, the OTP becomesverified. - An
otp.completedwebhook bringsphone: the number that sent it, which is what you sign them in with.
Good to know:
- Add
phoneto the request whenever you know it, to accept the code only from that phone. Without it, whoever sends the code is verified, so someone could forward your link to another person. - Give your app's name as
brand: it is in the text they send ("My Acme verification code: …") and in the answer, so a forwarded link says what it signs in to. - The
codeis in this answer only; keep it in your page, not in a log. It can be written with spaces, dashes or Arabic digits. - Your instance answers with a short "✅ Verified for Acme. You can go back to the app." in their language (
localeenorar), as a reply. Their message still arrives asmessage.received, with the code's digits masked, and gets no auto-reply. - Rarely, WhatsApp hides the sender's number (a private id): an OTP made without a
phonestill completes, withphonenull. Never sign anyone in by a null phone.
2. The code: your instance sends it
POST /v1/otps
{"instance_id": "01K…", "channel": "code", "phone": "201012345678", "brand": "Acme", "code_length": 6}
It answers 202 with status: "pending" (never the code), and the person gets "Acme code: 482913. It expires in 10 minutes. Never share it." (or its Arabic, with locale: "ar").
Check what they type with POST /v1/otps/{id}/check and {"code": "482913"}: 200 with status: "verified", or 422 with attempts_left. The fifth wrong code fails it. An OTP that is expired, failed, cancelled or already verified answers 409 with its status.
A code is a message like any other: to someone who never wrote to the instance it is a first message, counted against the day's new people and the warm-up, and not sent at night or while WhatsApp restricts the instance. Since a late code is useless, it is never kept waiting: if it cannot go within about two minutes, the OTP fails at once with the reason in failed_reason and an otp.failed webhook. That is why the link is better: it always works. People who receive a code and never write back also count against the instance's reply score.
Managing an OTP
GET /v1/otps/{id}:status(pending,verified,failed,expired,cancelled),phone,verified_phone,reference,attempts_left,sends,failed_reason,expires_at,verified_at.verified_phoneis null when WhatsApp hid the sender's number. Never the code.POST /v1/otps/{id}/resend: a new code (the old one stops working) and a freshexpires_in, while pending and once the last one went out (409while it is still on its way); wrong attempts still count, the limits per phone below apply (429withRetry-After), and at most 5 codes per OTP.DELETE /v1/otps/{id}: cancels a pending one; a code not sent yet is not sent.- In your panel, OTP (in the menu, for admins) lists them all, filtered by status, way and instance, and cancels a pending one. Its Try it makes a link for you to complete from your own phone.
Options and limits
expires_inis 1 to 30 minutes (10 by default),code_length4 to 8 digits (6),brandup to 30 characters (letters, spaces and& ' . -; no digits, no web address, not "WhatsApp" or "Meta"). Codes of 6 digits or more are safer: with 4, five guesses on each of ten codes a day give a stranger about one chance in 200.- One phone gets at most one code a minute, 3 an hour and 10 a day from your account (
429withRetry-After), and someone who never wrote to the instance at most 3 a day (the first message and two more), and none on a later day until 7 days after the last. The link way has no such limit. Your account may start 30 OTPs a minute. - A code takes one of your plan's messages; a link takes one only for its confirmation, which is skipped if the plan has no room.
Security
Codes are random, stored only as a keyed hash, compared in constant time and never returned after the link's own answer. Starting and checking need a key that may send; reading, one that may read; a key for one instance reaches only that instance's OTPs. Send an Idempotency-Key header: a retry returns the first OTP (without its code; start a new one if you lost it). OTPs are kept 30 days.