Developer documentation

Integrate WhatsApp OTP in minutes.

Your server sends requests to the WAppOTP API. Keep the project API key on your server and never expose it in browser JavaScript.

Build with AI

Integrate with Claude Code, Codex or Cursor

Paste this prompt into your AI coding agent inside your project. Set WAPPOTP_API_KEY in your.env first, and never paste the key into the chat. In the dashboard under Developers you can tailor it to your stack (Next.js, Laravel, Python, WordPress) and goal (phone verification, checkout verification or order messages).

You are integrating WAppOTP (WhatsApp OTP and messaging API) into this project.

## Context
- API base URL: https://api.wappotp.com
- Docs: https://wappotp.com/docs · OpenAPI: https://wappotp.com/openapi.json · Full reference for AI: https://wappotp.com/llms-full.txt
- Auth: Authorization: Bearer <secret API key> (keys start with wa_live_). Read it from the WAPPOTP_API_KEY environment variable.
- Phone numbers are always E.164, e.g. +639171234567.
- Detect the framework and language from this repository and follow its existing conventions (routing, env loading, HTTP client, validation, tests).

## Task
Goal: verify users' phone numbers with a WhatsApp one-time password during sign-up or login.
1. Add an endpoint that receives the user's phone number, normalises it to E.164 (for example +639171234567) and calls:
   POST https://api.wappotp.com/v1/otp/send
   Headers: Authorization: Bearer $WAPPOTP_API_KEY, Content-Type: application/json, Idempotency-Key: <unique per attempt>, x-whatsappotp-client-ip: <end user's IP>
   Body: {"to":"+639171234567","fallback":{"sms":true}}
   Response: {"verificationId":"…","status":"pending","expiresIn":300}
   Store verificationId server-side (session) — never trust it from the client alone.
2. Add an endpoint that receives the 6-digit code and calls:
   POST https://api.wappotp.com/v1/otp/verify
   Body: {"verificationId":"…","code":"482913"}
   Response: {"valid":true,"status":"verified"} — only mark the phone as verified when valid is true.
3. Build the UI: phone input → "Send code" → 6-digit code input → "Verify", with a resend button after 60 seconds and clear error messages.
4. Save the verified phone number (and the time it was verified) on the user record.

## Requirements
- Never hard-code or log the API key or webhook secret; add WAPPOTP_API_KEY (and WAPPOTP_WEBHOOK_SECRET if used) to .env.example with empty values and document them in the README.
- Call WAppOTP only from server-side code. Never expose the secret key to the browser or mobile app.
- Handle errors: 400 (invalid input — show the message), 402 (plan limit reached), 409 (idempotency key reused with a different body), 429 (rate limited — tell the user to wait). Log the x-request-id response header.
- Add rate limiting or CAPTCHA on public endpoints that trigger an OTP send.
- Add tests for the new service/endpoints with the HTTP calls mocked, and keep the existing code style.
- When done, list the files you changed, the env variables to set, and how to test it end to end.

Before writing code, briefly confirm your plan for where each piece goes in this codebase.
Try it

Send yourself a real OTP

Checking your session…
Authentication

Server-side API key

Add the project key to every developer API request:

Authorization: Bearer wa_live_xxxxx

API keys are permission-scoped. For a login-only integration, grant only otp:send and otp:verify. Add message, consent, or campaign scopes only when that integration actually uses them.

SMS

Send by SMS in the Philippines

POST /v1/otp/send { "to": "+639171234567", "channel": "sms" } → { "verificationId": "…", "channel": "sms", "smsCredits": 2, "expiresIn": 300 } POST /v1/messages { "to": "+639171234567", "channel": "sms", "text": "Order #1042 has shipped." }

Buy SMS credits under Billing. One credit sends one SMS of up to 160 characters (longer messages use more); OTPs go through a dedicated priority route and use 2 credits per SMS. SMS works for Philippine mobile numbers (+639…) only. Credits never expire, and messages the network rejects are refunded automatically. Messages cannot start with the word "TEST".

Reliability

SMS and email fallback for OTP

POST /v1/otp/send { "to": "+639171234567", "fallback": { "sms": true, "email": "[email protected]" } } → { "verificationId": "…", "status": "pending", "channel": "whatsapp", "expiresIn": 300 }

With fallback.sms and/or fallback.email, the code is sent by SMS (tried first) or email if no WhatsApp number is connected, or if WhatsApp has not sent it within about 45 seconds. The response's channel says where it went first. The email uses fixed wording with your project name, and verification works the same either way.

Templates

Send a saved template

POST /v1/messages { "to": "+639171234567", "template": "order_shipped", "variables": { "name": "Ana", "order": "#1042" } }

Create templates in the dashboard under Templates. Every {{placeholder}}must be supplied in variables; missing ones return a 400 that lists them.

Multiple numbers

Choosing the sending number

Pass connectionId to send from a specific number. Without it, the project's oldest connected number is used. Turn on sender rotation under Dashboard → Connections to spread recipients across all connected numbers instead: each recipient always hears from the same number, and volume is shared evenly.

Least privilege

Available API key scopes

otp:send Send OTP otp:verify Verify OTP messages:send Send transactional messages messages:read Read delivery state consents:read Read consent consents:write Manage consent campaigns:read Read campaigns campaigns:write Create campaigns
OTP

Send and verify codes

curl -X POST https://api.wappotp.com/v1/otp/send \ -H "Authorization: Bearer wa_live_xxxxx" \ -H "Idempotency-Key: order-login-12345" \ -H "Content-Type: application/json" \ -d '{"to":"+639171234567"}'

POST the returned verificationId and the six-digit code to /v1/otp/verify. Codes expire automatically and are stored only as hashes.

JavaScript / TypeScript

Server SDK

import { WhatsAppOtpClient } from "@whatsappotp/sdk"; const client = new WhatsAppOtpClient({ baseUrl: "https://api.wappotp.com", apiKey: process.env.WHATSAPPOTP_API_KEY!, }); const sent = await client.sendOtp( { to: "+639171234567" }, { idempotencyKey: crypto.randomUUID() } );
Transactional

Send normal WhatsApp messages

POST /v1/messages { "to": "+639171234567", "text": "Your order #1234 is ready." }

Use GET /v1/messages/:messageId or webhooks for asynchronous delivery status. Direct-message state progresses through QUEUED, SENDING, SENT, DELIVERED and READ when WhatsApp receipts are available; terminal failures use FAILED.

Images, videos and documents

POST /v1/messages/media { "to": "+639171234567", "type": "IMAGE", "url": "https://cdn.example.com/order-ready.jpg", "caption": "Your order is ready." }

Media must use a public HTTPS URL. The worker rejects redirects, private/reserved destinations, invalid MIME types, empty files, images larger than 16 MB, and video/document files larger than 32 MB. Documents require a filename.

Marketing

Consent before campaigns

Record OPTED_IN consent using /v1/consents before creating a campaign. The API rejects recipients without active consent or recipients on the suppression list. Inbound opt-out keywords are processed automatically. Campaigns may also include one optional IMAGE, VIDEO, or DOCUMENT using the same secure public-HTTPS media pipeline as transactional media messages. A queued campaign can be stopped with POST /v1/campaigns/:id/cancel; recipients that have not started delivery are marked BLOCKED and stale queue jobs are ignored by the worker.

Webhooks

Signed asynchronous events

Create webhook endpoints in the dashboard. Verify the x-whatsappotp-signature header by computing HMAC-SHA256 over timestamp + "." + the exact raw request body. The message.received event is emitted only for new one-to-one inbound messages; media events include metadata/caption only and the platform does not automatically download inbound media.

otp.verified message.received message.sent message.delivered message.read message.failed connection.connected connection.disconnected connection.logged_out consent.opted_out

The included JavaScript, PHP, and Python SDKs now provide a webhook signature verifier with a default five-minute timestamp tolerance to reject replayed requests.

Reliability

Retries, idempotency and request IDs

Send Idempotency-Key on OTP sends, transactional sends, and campaign creation. Reusing a key with the same body returns the original result; changing the body returns HTTP 409.

Every API response includes x-request-id. Store it with your application logs when troubleshooting.

PHP

Dependency-free PHP client

A PHP 8.1+ cURL client is included under integrations/php. It supports OTP, verification, transactional messages, consent, campaigns, idempotency keys, client-IP forwarding and request-ID errors.

Python

Dependency-free Python client

A Python 3.11+ standard-library client is included under integrations/python with the same server-side API coverage and retry controls.

WordPress / WooCommerce

Included WordPress plugin

The repository contains integrations/wordpress/whatsappotp. Configure the API URL and project key in WordPress settings, then use the shortcode [whatsappotp_verify].

Shopify

Keep the secret in an app backend

Never place the project secret in Liquid or storefront code. Your Shopify app backend calls WAppOTP and the storefront talks only to your app backend. A server-side integration guide is included under integrations/shopify.