Number validation
Asking WhatsApp whether a number exists, over a live channel. It is a point-in-time answer and never a deliverability guarantee — and it answers in THREE states, not two.
The lookup
It spends the channel's live session, so it is rate limited to 60 requests a minute per workspace and is not something to call on every keystroke. Omitting channel_id lets the API choose an eligible channel.
| Field | Type | Required | Description |
|---|---|---|---|
| numbers | string[] | Required | The numbers to check, in E.164. |
| channel_id | string | Optional | Which channel to ask through. Chosen for you when omitted. |
It requires BOTH contacts.read and messages.send — one of only two operations in the whole API that names two scopes, because a lookup spends a sending channel. See authentication.
The three answers
| Status | Description |
|---|---|
| exists | The number is registered on WhatsApp. |
| not_exists | The number is not registered. |
| unknown | The lookup did not resolve. This is not an answer about the number. |
unknown is NOT a soft not_exists
This is the single most important fact on this page. The underlying client filters out non-existent numbers and resolves nothing for a query that came back empty, so both arrive as the same absence. Render three answers with three tones, and never sum unknown with not_exists — treating an unresolved lookup as a dead number quietly removes real customers from a list.
| reason | Description |
|---|---|
| invalid_number | The number is not a valid E.164 number at all. |
| lookup_failed | The lookup itself errored. |
| no_answer | The channel returned nothing for this number. |
| timeout | The lookup did not answer in time. |
curl -X POST https://whats.azzamkh.sa/api/v1/contacts/validate \
-H "Authorization: Bearer $WA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "numbers": ["+9665XXXXXXXX", "+9665YYYYYYYY"], "channel_id": "ch_..." }'
# 200 OK
# {
# "channel_id": "ch_...",
# "results": [
# { "e164": "+9665XXXXXXXX", "status": "exists" },
# { "e164": "+9665YYYYYYYY", "status": "unknown",
# "reason": "no_answer" }
# ],
# "summary": { "checked": 2, "exists": 1, "not_exists": 0, "unknown": 1 }
# }
# A refused LOOKUP is not an answer about the number:
# 409 { "error": { "code": "CHANNEL_NOT_CONNECTED", ... } }summary counts checked, exists, not_exists and unknown. The four are disjoint and the last three sum to checked.
What it does not tell you
- The check is a channel CAPABILITY:
check_numberon the channel. It is true on QR and the simulated adapters and false on Meta. Absent means do not offer the check, never that the check will fail — see channels. - Existing is not consent. A number that exists but is not
opted_inis still refused a marketing message. - The answer is true at the moment it was asked. Nothing about it is cached on your behalf, and nothing about it is a promise about delivery.
- A refused LOOKUP is never an answer about the number.
CHANNEL_NOT_CONNECTED,RATE_LIMITEDandFEATURE_TEMPORARILY_UNAVAILABLEall mean the check could not run, and none of them belongs in a composer as a red error on the recipient. - 60 requests a minute per workspace, whatever the batch size.
The campaign check
A campaign runs the same lookup over a BOUNDED SAMPLE of its resolved audience, as an optional part of the pre-launch check. Pass validate_numbers: true — it is never implied by validate, precisely because it spends the channel's live session. See campaigns.
The number_check block
| Field | Description |
|---|---|
| status | checked, unavailable or unsupported. Only the first carries a real result. |
| sampled, eligible | How many were sampled, and how many were eligible to sample from. |
| summary | The same four counts as the direct lookup. |
| results | The per-number entries, when the sample was small enough to return them. |
| truncated, results_truncated | Whether the sample, or the returned results, were cut short. |
It reports; it does not block
A not_exists in the sample does not remove anybody from ready. The block exists so a person can decide, not so the platform can decide for them — and because the sample is bounded, its proportions are an estimate over the audience rather than a verdict on it.
Errors that matter here
| Code | HTTP | When it happens |
|---|---|---|
| CHANNEL_NOT_CONNECTED | 409 | The channel is not connected, so no lookup can be made. |
| CHANNEL_NOT_FOUND | 404 | No such channel, or none eligible when channel_id was omitted. |
| FEATURE_TEMPORARILY_UNAVAILABLE | 503 | The capability is temporarily switched off. Retryable. |
| RATE_LIMITED | 429 | More than 60 lookups in the minute. |
| VALIDATION_ERROR | 422 | A number was not valid E.164, or the batch was empty. |
Every code and its status is on the errors page.