Who does what in an OTP flow
A verification code proves that a person has access to a number at that moment. That applies to sign-up, passwordless login, second factor and confirming sensitive actions, such as an email change or a withdrawal. The temptation is to look for a ready-made endpoint that generates and validates the code. Do not make that depend on the delivery channel.
The right split of responsibilities is this:
| Step | Owner |
|---|---|
| Generate the code from a secure random source | Your backend |
| Store the hash, the expiry and the attempt counter | Your backend |
| Deliver the message to the user’s WhatsApp | WhatsApp API |
| Compare the typed code and invalidate it after use | Your backend |
| Block abuse by number, IP and account | Your backend |
That way, switching channels, adding SMS as a fallback or changing providers does not touch your security logic. The WhatsApp API becomes a transport, which is its role here.
Full example in Node.js
The snippet below shows both ends: issue and verify. Storage is abstracted as db, which can be Redis with expiry or a table with a validity column.
import { randomInt, createHash, timingSafeEqual } from 'node:crypto'
import { DApi } from 'd-api-sdk'
const dapi = new DApi({ apiKey: process.env.DAPI_API_KEY! })
const hash = (code: string) => createHash('sha256').update(code).digest()
export async function issueCode(phone: string) {
const code = randomInt(0, 1_000_000).toString().padStart(6, '0')
await db.otp.upsert(phone, {
hash: hash(code),
expiresAt: Date.now() + 5 * 60_000,
attempts: 0,
})
await dapi.messages.sendText({
sessionId: 'app-verification',
to: phone, // e.g. 14155550123
text: `${code} is your App verification code. It expires in 5 minutes. Do not share it with anyone.`,
})
}
export async function verifyCode(phone: string, typed: string) {
const record = await db.otp.get(phone)
if (!record || record.expiresAt < Date.now()) return false
if (record.attempts >= 5) return false
await db.otp.incrementAttempts(phone)
const ok = timingSafeEqual(hash(typed), record.hash)
if (ok) await db.otp.remove(phone) // single use
return ok
}Note that the plain-text code only exists in memory during the send. It never reaches the database or the logs. For SDK details, see the Node.js SDK and the WhatsApp API in Node.js guide.
Security checklist
- Real randomness. Use
crypto.randomIntor equivalent. NoMath.randomand no time-derived codes. - Hash, never plain text. If the database leaks, open codes become instant access.
- Short validity and single use. Five to ten minutes, and the code dies on the first successful verification.
- Attempt limit per code. Five wrong tries invalidate the code and require a new send.
- Send limit per number. For example, one resend every 60 seconds and at most five per hour. This protects the user from spam and keeps your number from looking like a runaway bot.
- Limits per IP and per account, to stop anyone trying to fire codes at thousands of different numbers.
- No codes in logs. Log that the send happened, with the phone number masked, never the code.
- Short, clear message: code first, then validity and the do-not-share warning.
Send speed and availability
A code that arrives late arrives after the user gave up. A few decisions help:
- Use a dedicated session for verification, separate from support and campaigns. That way a problem on a marketing number does not block your users’ logins.
- Send synchronously, with no queue in between. The user is looking at the screen, and the API response tells you right away if something went wrong.
- Watch the
connection.statusevent of the verification session and alert the team if it leavesconnected. - Offer SMS as a fallback after a few seconds or when the send fails. D-API has an SMS API on the same account.
A number used only for codes, with volume that tracks real app usage, has a healthy sending profile. Even so, it is worth knowing how to avoid bans.
When to use the official authentication template
On Meta’s official API, verification codes have their own template category: authentication. The template must be approved before use, and Meta charges per message, with rates that vary by category and country. In return, the send comes from the company’s verified number.
The official API is worth considering when:
- Code volume is very high and comes from users who have never talked to the company.
- The industry is regulated and internal policy requires the official channel.
- A verified brand on the sender matters for users to trust the code.
On D-API, an official connection sends the template through the messages/send/template route, with the code in bodyVariables. The rest of the application stays the same. See the official WhatsApp API (Meta Cloud API) and the comparison in official vs unofficial API. A verification code is a kind of transactional notification, with a stricter speed requirement. The 3-day free trial, no credit card, lets you test the whole flow with a real number.
