WhatsApp OTP
Deliver your own verification codes and track their WhatsApp message status.
Send a code with POST /otp/messages, then read its status with GET /otp/messages/{messageId} or receive signed webhooks. Your application generates, expires, and verifies the code. There is no code verification endpoint.
Requirements
- A sendable WhatsApp number in your workspace. Find its Sender ID under Developers > Overview > Senders.
- A template on that sender's WhatsApp Business account with status
APPROVED, categoryAUTHENTICATION, and authentication type. - Exactly one body code parameter and a valid template structure. The code fills the body and any OTP button parameters, including copy-code, one-tap, or zero-tap buttons.
- Available monthly API OTP quota. Check API OTP messages this month on Developers > Overview.
Use the template's exact name and language code. A standard utility or marketing template cannot be used for OTP sends.
Send a code
Set WATIFICATION_API_KEY to your server-side key. Replace the fictional values with your recipient, sender, and template.
curl --fail-with-body --request POST \
'https://api.watification.com/api/v1/otp/messages' \
--header "Authorization: Bearer $WATIFICATION_API_KEY" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: login-example-002' \
--data '{
"to": "+15555550123",
"senderId": "11111111-1111-4111-8111-111111111111",
"template": { "name": "login_code", "language": "en" },
"code": "483920",
"reference": "login-example-002"
}'| Field | Required | Value |
|---|---|---|
to | Yes | Valid phone number with a leading +; normalized to E.164. |
senderId | Yes | Watification sender UUID for a WhatsApp number in this workspace. |
template.name | Yes | Non-empty template name. |
template.language | Yes | Non-empty template language code. |
code | Yes | 1-15 ASCII letters or digits (A-Z, a-z, 0-9). |
reference | No | Your correlation string, at most 128 characters. |
Unknown body fields and query parameters are rejected with 422 DEVELOPER_INVALID_INPUT. Do not send additional fields inside template.
HTTP 202 returns:
{
"messageId": "22222222-2222-4222-8222-222222222222",
"status": "queued",
"reference": "login-example-002"
}reference is null when omitted. It is returned in status responses and outcome webhooks, so use it to match a message to your login or verification attempt. It is not an idempotency key.
Get the status
curl --fail-with-body \
'https://api.watification.com/api/v1/otp/messages/22222222-2222-4222-8222-222222222222' \
--header "Authorization: Bearer $WATIFICATION_API_KEY"HTTP 200 returns:
{
"messageId": "22222222-2222-4222-8222-222222222222",
"status": "sent",
"reference": "login-example-002",
"to": "+15555550123",
"createdAt": "2026-10-09T10:00:00.000Z",
"sentAt": "2026-10-09T10:00:02.000Z",
"failure": null
}sentAt is null before a sent outcome is recorded. failure is null or { "code": "...", "message": "..." } with a safe description. The supplied code is never returned in status responses or webhooks. Send no GET body or query parameters.
Statuses
The status vocabulary includes the following values. Do not require every intermediate status to appear.
| Status | Meaning |
|---|---|
accepted | Request reserved; message submission is not yet complete. |
received | Message recorded before queueing. |
queued | Accepted for sending. |
sending | A send attempt is in progress. |
sent | A sent outcome has been reported. |
delivered | WhatsApp reported delivery. |
read | WhatsApp reported the message as read. |
played | Playback status in the shared message vocabulary; not an expected OTP text outcome. |
failed | A send failure has been recorded. Inspect failure. |
Status corrections can move a failed message to a sent, delivered, or read outcome and clear its failure details. No message status means the customer successfully verified your code.
Idempotency
Set the optional Idempotency-Key header to a string of at most 128 characters. Use a non-empty, unique value for each logical send and persist it before making the request.
Within a workspace, repeating a completed request with the same key returns the original 202 body, including status: "queued", even if the current status has advanced. It does not send another message or consume another quota unit.
The normalized recipient, sender UUID, template name, language, and reference must match. A mismatch returns 409 IDEMPOTENCY_KEY_REUSED. The code is excluded from this comparison: sending a different code with the same key does not replace the original message. Use a new key to deliver a new code. After a network failure, retry with the same key and unchanged request.
Quota and events
One accepted send consumes one monthly API OTP unit. Idempotent repeats do not consume more; later send failures are not refunded. A zero or exhausted plan allowance returns 429 OTP_QUOTA_EXCEEDED. Plan amounts vary; use the dashboard's current allowance.
message.sent: first reportedsent,delivered,read, orplayedoutcome. The webhook'sdata.statuscontains the actual reported status.message.failed: first reported failure.
Each type is produced at most once per message, but webhook deliveries can repeat. Both types can occur for one message. There are no separate delivered or read events. See webhook handling.
Endpoint errors
All errors use Problem Details. These are the endpoint's stable error codes:
| HTTP | Code | Meaning |
|---|---|---|
| 401 | API_KEY_INVALID | Missing, invalid, or revoked key. |
| 404 | OTP_SENDER_NOT_FOUND | Sender not found in this workspace. |
| 404 | OTP_TEMPLATE_NOT_FOUND | Template name/language not found for the sender. |
| 404 | NOT_FOUND | GET message not found in this workspace. |
| 409 | OTP_SENDER_UNAVAILABLE | Sender cannot send. |
| 409 | OTP_TEMPLATE_NOT_USABLE | Template does not meet OTP requirements. |
| 409 | IDEMPOTENCY_KEY_REUSED | Key was used for a different request. |
| 422 | DEVELOPER_INVALID_INPUT | Invalid fields, phone, UUID, or header. |
| 422 | RECIPIENT_BLOCKED | Recipient is blocked. |
| 429 | OTP_QUOTA_EXCEEDED | Monthly API OTP allowance unavailable or exhausted. |
| 429 | RATE_LIMITED | Per-key request rate exceeded; use Retry-After. |