Understand the secure OTP flow
Your browser should send the receiver number to your own backend. Your backend creates or requests the OTP, calls HookMessage with a server-side API key, and returns only a safe result to the browser.
Never expose a HookMessage API key or a stored OTP code in React client code. Bind each OTP challenge to the intended user or login attempt, apply an expiry, limit verification attempts, and consume the challenge after one successful verification.
- Browser: collects the receiver number and submits it to your application.
- Application backend: authenticates the request, applies abuse limits, and calls HookMessage.
- HookMessage: queues the WhatsApp message and returns a message ID.
- Application backend: verifies the submitted code and creates the authenticated session.
Send the OTP from your backend
Use an API key with the OTP role. Free Sandbox requests omit `sender_id`; paid workspaces can send an assigned sender ID when the integration requires a specific connected number.
HTTP 201 means the request was accepted into the queue. Save the returned message ID and do not treat `queued` as proof of delivery.
export async function sendLoginOtp(receiverNumber: string) {
const response = await fetch(
`${process.env.HOOKMESSAGE_BASE_URL}/api/otp/send`,
{
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": process.env.HOOKMESSAGE_API_KEY!,
},
body: JSON.stringify({
receiver_number: receiverNumber,
app_name: "My application",
ttl_minutes: 10,
}),
},
);
const result = await response.json();
if (!response.ok) throw new Error(result.message ?? "OTP request failed");
return result;
}Handle delivery and verification separately
Message delivery answers whether WhatsApp accepted the outbound message. OTP verification answers whether the user entered the correct code. They are related, but they are not the same state.
For a simple application, poll `GET /api/messages/{message_id}` after a short delay. For production, consume signed webhooks and update your local delivery record idempotently.
- Store the message ID beside the login or signup attempt.
- Do not create a user session from a `queued` or `sent` delivery state.
- Compare OTP values using a server-side challenge store and constant-time logic where practical.
- Expire and consume successful challenges so a captured code cannot be replayed.
Production checklist
Before live traffic, test valid, expired, incorrect, repeated, and rate-limited codes. Confirm that logs never contain API keys, complete OTP values, or full recipient data.
- Use HTTPS and server-only environment variables.
- Rate-limit by account, receiver, IP, and risk signal.
- Use an idempotency key when a network retry could repeat a send.
- Monitor failed messages and sender disconnections.
- Provide a safe resend flow with a cooldown.