Skip to content
Contents
Workspace data

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

POST/v1/contacts/validate

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.

FieldTypeRequiredDescription
numbersstring[]RequiredThe numbers to check, in E.164.
channel_idstringOptionalWhich 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

StatusDescription
existsThe number is registered on WhatsApp.
not_existsThe number is not registered.
unknownThe 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.

reasonDescription
invalid_numberThe number is not a valid E.164 number at all.
lookup_failedThe lookup itself errored.
no_answerThe channel returned nothing for this number.
timeoutThe lookup did not answer in time.
curl
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_number on 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_in is 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_LIMITED and FEATURE_TEMPORARILY_UNAVAILABLE all 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

POST/v1/campaigns/{campaignId}/validatePOST/v1/campaigns/audience-preview

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

FieldDescription
statuschecked, unavailable or unsupported. Only the first carries a real result.
sampled, eligibleHow many were sampled, and how many were eligible to sample from.
summaryThe same four counts as the direct lookup.
resultsThe per-number entries, when the sample was small enough to return them.
truncated, results_truncatedWhether 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

CodeHTTPWhen it happens
CHANNEL_NOT_CONNECTED409The channel is not connected, so no lookup can be made.
CHANNEL_NOT_FOUND404No such channel, or none eligible when channel_id was omitted.
FEATURE_TEMPORARILY_UNAVAILABLE503The capability is temporarily switched off. Retryable.
RATE_LIMITED429More than 60 lookups in the minute.
VALIDATION_ERROR422A number was not valid E.164, or the batch was empty.

Every code and its status is on the errors page.