message.comDevelopers

Verify WhatsApp quickstart Disabled

Verify a phone number by sending a one-time passcode over WhatsApp instead of SMS. WhatsApp is the strongest option in markets where it is the dominant messenger: delivery is over data rather than the carrier SMS rail, so it can be cheaper and more reliable internationally. The API contract below is committed and stable; the disabled-for-sending state is called out where it applies.

Setup time: 2-4 weeks$0.03 per successful verification

WhatsApp is currently disabled for sending. POST /v1/verify with channel: "whatsapp" returns 422 with { "error": "channel_not_available", "channel": "whatsapp" }. Every request and response shape on this page is the committed contract; the check call and read endpoints work identically across channels.

When to choose WhatsApp

Choose WhatsApp when your users are concentrated in regions where it is the primary messaging app, when international SMS costs or deliverability are a problem, or when you want a branded, in-app verification experience. For domestic phone verification with the widest reach and no external review, start with SMS; use voice as an accessibility and landline fallback.

Prerequisites

  • A message.com workspace with Verify activated and a positive credit balance. See Get started with Verify.
  • A Verify Service with whatsapp in its enabledChannels.
  • An API key with the verify scope (mk_live_...). See Authentication.
  • The destination country in the service's allowedCountries allowlist: WhatsApp is a phone channel and subject to the same allowlist as SMS.
  • A Meta Business Manager verification and an approved WhatsApp message template. Both are gated by Meta's own review timeline, which is why setup runs 2-4 weeks. See setup times.

Full verification walk

Three steps every time: start a verification, the user receives the code in WhatsApp, then check the code they typed. The request and response shapes are identical to every other channel.

1. Send the code

Call POST /v1/verify with the phone number as to and "whatsapp" as the channel. E.164 format is preferred.

cURL (macOS/Linux)
curl -X POST https://api.message.com/v1/verify \
  -H 'Authorization: Bearer mk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "to": "+14155550142",
    "channel": "whatsapp"
  }'
cURL (Windows)
curl -X POST https://api.message.com/v1/verify ^
  -H "Authorization: Bearer mk_live_..." ^
  -H "Content-Type: application/json" ^
  -d "{\"to\": \"+14155550142\", \"channel\": \"whatsapp\"}"
Node
const start = await fetch('https://api.message.com/v1/verify', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer mk_live_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ to: '+14155550142', channel: 'whatsapp' }),
});
const verification = await start.json();
// When live:
// { id, status: 'pending', channel: 'whatsapp', hint: '•••0142', dispatch: 'sent', expiresAt }
// Today:
// { error: 'channel_not_available', channel: 'whatsapp' }  (HTTP 422)
Python
import requests

start = requests.post(
    "https://api.message.com/v1/verify",
    headers={"Authorization": "Bearer mk_live_..."},
    json={"to": "+14155550142", "channel": "whatsapp"},
)
verification = start.json()
# When live:
# { "id": ..., "status": "pending", "channel": "whatsapp", "hint": "•••0142", "dispatch": "sent", "expiresAt": ... }
# Today:
# { "error": "channel_not_available", "channel": "whatsapp" }  (HTTP 422)

When WhatsApp is live, a successful start returns 201 with the same flat body every channel uses:

201 Created (when live)
{
  "id": "1f6a2e4c-9d3b-4a1e-8f2c-6b7a9e0d5c3f",
  "status": "pending",
  "channel": "whatsapp",
  "hint": "•••0142",
  "dispatch": "sent",
  "expiresAt": "2026-07-25T18:14:11.000Z"
}

Today, the same call returns 422:

422 Unprocessable (today)
{
  "error": "channel_not_available",
  "channel": "whatsapp"
}

2. User receives the code

The user receives the code as a WhatsApp message from your business account. The message body is rendered from the WhatsApp message template you get approved with Meta during setup; the service friendlyName is the brand shown as the sender.

3. Check the code

The check call is channel-agnostic and works today. When the user types the code, send it back with the id from step 1.

cURL (macOS/Linux)
curl -X POST https://api.message.com/v1/verify/check \
  -H 'Authorization: Bearer mk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "id": "1f6a2e4c-9d3b-4a1e-8f2c-6b7a9e0d5c3f",
    "code": "482913"
  }'
cURL (Windows)
curl -X POST https://api.message.com/v1/verify/check ^
  -H "Authorization: Bearer mk_live_..." ^
  -H "Content-Type: application/json" ^
  -d "{\"id\": \"1f6a2e4c-9d3b-4a1e-8f2c-6b7a9e0d5c3f\", \"code\": \"482913\"}"
Node
const check = await fetch('https://api.message.com/v1/verify/check', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer mk_live_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ id: verification.id, code: userSubmittedCode }),
});
const result = await check.json();
// success: { id: '1f6a2e4c-...', verified: true }
// failure: { id: '1f6a2e4c-...', verified: false, reason: 'bad_code' }
Python
check = requests.post(
    "https://api.message.com/v1/verify/check",
    headers={"Authorization": "Bearer mk_live_..."},
    json={"id": verification["id"], "code": user_submitted_code},
)
result = check.json()
# success: {"id": "1f6a2e4c-...", "verified": True}
# failure: {"id": "1f6a2e4c-...", "verified": False, "reason": "bad_code"}

A correct code returns 200 with { id, verified: true } and bills $0.03. A wrong code also returns 200, with verified: false and a reason. Read the verified field, not the HTTP status.

Fraud interactions

WhatsApp is a phone channel and inherits the same fraud controls as SMS. When WhatsApp is live these apply on every send:

  • Country allowlist. A number whose country is not in the service's allowedCountries returns 403 country_not_allowed.
  • Per-destination throttle. More than 3 sends to the same number in 60 seconds returns 429 rate_limited with scope: "destination".
  • Per-service velocity. The per-minute and per-day caps return 429 rate_limited with scope: "workspace".
  • Attempt cap. Five wrong codes lock the verification with too_many_attempts.

Country allowlists, velocity caps, and destination throttles are covered in depth in the Verify fraud guide.

Troubleshooting

SymptomCause and fix
422 channel_not_availableExpected today: WhatsApp is disabled for sending. Use SMS or email now. Do not retry this call for the same channel; it will not become available mid-session.
400 channel_not_enabledThe resolved service does not list whatsapp in enabledChannels. This is a per-service setting, distinct from the platform-wide channel_not_available.
403 country_not_allowed (when live)The number's country is not in the service's allowedCountries. Add the country to the allowlist.
400 invalid_numberThe to value fails the phone-shape check. Send digits, optionally with a leading +.
Check returns reason: "expired"The code is past expiresAt. Start a new verification. There is no separate resend endpoint.

Every reason above, with its exact firing condition, is on the Verify API error codes page.