Send OTP login codes over WhatsApp: a REST API example
Send one-time passcodes over WhatsApp from your own backend with an Authentication template, a test key and idempotent retries. Node example, error handling and what to build around it.
October 16, 2026 · 5 min read
A one-time passcode (OTP) is a small job with a few sharp edges: the message must arrive fast, the same code must never be sent twice by accident, and your app has to cope when WhatsApp refuses a send. This guide shows how to send sign-in codes over WhatsApp from your own backend with a REST API, using a test key first so nothing reaches a real phone while you build.
SendFromChat is an independent product and is not affiliated with Meta or WhatsApp. It runs on Meta's official WhatsApp Cloud API; you keep your own WhatsApp Business account.
The short version
- Get an Authentication template approved (Meta's library has ready-made one-time-code templates).
- Build with a test key, which runs every check and sends nothing.
- Generate and store the code on your side, then send it with one
POST /messageswhere the code is variable 1. - Send an
Idempotency-Keyso a retry never sends a second message, and handle WhatsApp's refusals by errortype.
1. An Authentication template
Messages you start outside a 24-hour conversation window must use a template Meta has approved (how the window works). For one-time passcodes, Meta has a dedicated Authentication category. It is for one-time codes only, so marketing copy doesn't belong in it (the category rules).
Two ways to get one:
- Use Meta's template library: it includes one-time-code templates in 16 languages. Keep Meta's wording and they are approved without the usual review.
- Create your own on the dashboard, or ask your assistant to draft and submit it.
Authentication messages are billed by Meta at its own rate, separate from your SendFromChat plan (WhatsApp API pricing).
2. A key you can't hurt yourself with
On the dashboard's Developers page, create a test key (sfc_test_…). It checks the plan, opt-outs, the template and the 24-hour window, then stops: nothing is sent, saved or counted. Build the whole flow with it, then swap in a live key (sfc_live_…) for production.
Keep keys on your server, never in browser or mobile-app code. If you build with Claude Code, you can ask it to set up SendFromChat in your project: it saves the key straight into .env.local or your host's secrets without the key appearing in the chat (connect your assistant).
3. Send the code
Generate the code on your server and store only what you need to check it later (for example a hash and an expiry time). Then send it as variable 1 of the Authentication template. SendFromChat fills in the copy-code button that Meta requires.
// lib/send-otp.js: runs on your server only
import crypto from "node:crypto";
export async function sendLoginCode(phone, loginAttemptId) {
const code = String(crypto.randomInt(0, 1_000_000)).padStart(6, "0");
// Save a hash of `code` and an expiry for this attempt before sending.
await saveCodeHash(loginAttemptId, hash(code), Date.now() + 10 * 60 * 1000);
const res = await fetch("https://sendfromchat.com/api/v1/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SENDFROMCHAT_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `login-${loginAttemptId}`,
},
body: JSON.stringify({
to: phone, // international format, e.g. +4915112345678
template: { name: "login_code", language: "en_US", variables: [code] },
}),
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error?.type}: ${body.error?.message}`);
return body; // { id: "wamid…", status: "accepted", … }
}
saveCodeHash and hash stand in for your own storage. The ten-minute expiry is an example, not a SendFromChat setting; choose what suits your app.
With a test key the response is 200 with "status": "test". With a live key it is 202 and "status": "accepted", which means Meta took the message, not that the person has seen it.
4. Don't send the same code twice
Two details matter most:
- The idempotency key comes from the login attempt, not from the HTTP attempt.
login-<attemptId>stays the same on every retry, so a network blip or a job re-run returns the first response (with anIdempotent-Replayed: trueheader) instead of sending a second code. Reusing a key with a different body returnsidempotency_mismatch, so a "resend code" button should create a new attempt id, and a new code. - Rate-limit requests in your own app. Every message you send counts toward your plan's monthly allowance, so cap how often one phone number or one IP can ask for a code.
5. Handle WhatsApp's refusals
Some errors are WhatsApp's rules, not bugs. Branch on error.type:
type |
What it means | What to do |
|---|---|---|
opted_out |
The person replied STOP | Offer another sign-in method |
template_not_approved |
The template isn't approved yet | Check its status on the dashboard or GET /templates?status=approved |
warmup_limit |
A new number hit today's cap (50, then 100, then 200 template sends a day for the first three days) | Retry later, or fall back to another method |
whatsapp_rejected |
Meta refused the send; the message carries Meta's reason | Log it and show a generic "try again" |
sending_paused |
Trial ended or plan limit reached | Alert yourself, not the user |
unauthorized |
Key missing, wrong or revoked | Check your environment variables |
The full list is in the REST API reference. Always keep a fallback (email code, authenticator app) so a user is never locked out because a WhatsApp send failed.
6. Know what happened next
For delivery status, add a webhook endpoint on the Developers page and subscribe to message.status (sent, delivered, read, or failed with a reason). Each request is signed with a SendFromChat-Signature header: verify it over the raw body before trusting the payload, and skip event ids you have already handled, because deliveries can repeat. The REST API reference has the verification function, and our order notifications guide shows the same pattern end to end.
Things to keep in mind
- Only message people who gave you their number for this purpose. A user typing their number into your sign-in form is the signal; keep a record, and read our opt-in guide.
- Treat the code like a password. Store a hash, expire it, limit attempts, and never log the plain code.
- Costs are two separate things: SendFromChat's flat monthly plan (from $19, 30-day free trial, no card) and Meta's per-message charge, billed to your own WhatsApp Business account at Meta's published rates with no markup from us.
A note for EU and UK customers
SendFromChat stores data in Mumbai, India. Transfers from the EU and UK are covered by the Standard Contractual Clauses and the UK Addendum in our Data Processing Agreement, and the sub-processors are listed publicly. Read it before you decide; our GDPR guide covers consent, opt-out and records on your side. This is not legal advice.
Try it
Start the 30-day free trial (no card), connect a number, create a test key and send yourself a code. Comparing routes? See SendFromChat vs Twilio and using Meta's Cloud API directly, which also covers the OTP-only case.
Do this from your AI assistant
SendFromChat connects Claude, ChatGPT, Cursor or any MCP client to the WhatsApp Business API — templates, bulk sends, inbox, chatbots and sequences, just by asking. 30 days free, no card.
Start freeMore guides
Send WhatsApp messages from n8n with an HTTP Request node
Build an n8n workflow that sends WhatsApp order updates and sign-in codes through a REST API, with a test key, idempotent retries and a webhook for delivery status and replies.
How to send a WhatsApp broadcast from a spreadsheet, safely
A step-by-step walkthrough for small businesses — turn a spreadsheet into a personalised WhatsApp broadcast, check the preview, respect opt-out and warm up a new number. Do it by asking Claude or ChatGPT, or in the dashboard.
WhatsApp order notifications from your app: a working example
Send order confirmations and shipping updates over WhatsApp from Node or Next.js with a REST API — test keys, templates, idempotent retries and signed delivery webhooks.
WhatsApp Business API in Europe — a country-by-country guide
Where WhatsApp actually dominates in Europe, what it costs to send in each market, and the rules that differ by country before you launch a campaign.