The WhatsApp check answers one question: does this phone number have a WhatsApp account right now? You get registered: true, false or null (unknown), with the time we checked. It works for numbers in any country, in real time or in bulk jobs. No message is sent, and no name, photo or profile is returned.
What does the WhatsApp check tell you?
It tells you whether a WhatsApp account is associated with the phone number. WhatsApp ties each account to one phone number and verifies that number with a code at sign-up, so an account is a useful sign that the number was active on a smartphone at some point.
registered: true: an account exists for the number.registered: false: we got a conclusive answer and there is no account.registered: null: we could not get a conclusive answer (statusandreasontell you why). You are not charged for it.
The answer describes the account, not the person. A number can be reassigned by the carrier, moved to a new phone or left dormant. WhatsApp's help center says accounts are generally deleted after 120 days of inactivity, so a number that changed hands recently can still show the previous owner's account for a while. Always read checked_at together with registered.
Who uses it, and why?
Most teams use the WhatsApp check to choose a channel before they send a message people expect, such as a one-time passcode, an order update or an appointment reminder.
- OTP and sign-up flows. If a number has no WhatsApp account, sending a verification code there will fail, so the flow can go straight to SMS or voice. Where WhatsApp is common, this can reduce SMS spend. See SMS cost reduction.
- Deliverability. A number with a WhatsApp account passed WhatsApp's own sign-up verification at some point. That makes it a useful extra signal next to a carrier lookup.
- Lead verification. A form entry with a WhatsApp account is less likely to be a typo or a made-up number. It is not proof of identity.
Usage differs a lot between markets. WhatsApp announced two billion users in February 2020 (WhatsApp blog). It is the default messenger in many countries, while other markets rely on Telegram, Viber, LINE or iMessage. A multi-check request covers several of them at once.
What do you get back?
Each number gets one result per requested check in checks. Because WhatsApp was the first service, WhatsApp results also appear in the older top-level whatsapp object, so existing integrations keep working.
| Field | Type | Meaning |
|---|---|---|
checks["whatsapp.registered"].registered | boolean or null | true account exists, false no account, null unknown |
…status | enum | completed, pending, unknown, unsupported_country or failed |
…confidence / …confidence_score | enum / 0–1 | How sure the answer is; null when not conclusive |
…checked_at | timestamp | When the answer was obtained |
…cached / …age_seconds | boolean / integer | Whether it came from your account's cache, and how old it is |
…billed | boolean | Whether this check was charged |
…reason | string or null | Why an answer is not conclusive, e.g. UPSTREAM_TIMEOUT |
whatsapp | object | The same result in the v1 shape (back-compatibility) |
The service has no extra attributes. For the business flag, use the WhatsApp Business check.
How is it billed?
You pay per number checked, and only for conclusive answers. You're not charged for inconclusive results (unknown, unsupported country, timeout, invalid, duplicate). Real-time lookups and bulk jobs have separate per-check prices. Bulk is the cheaper option when you don't need the answer straight away. See pricing for current rates.
Repeat checks of the same number inside the freshness window can come from your account's cache. Cache hits are free and marked cached: true, billed: false. Send max_age: 0 to force a fresh check. A fresh check is billed and counts against your rate limits. max_cost puts a ceiling on a request, and the API refuses the request if the most it could cost is higher.
What are the limits?
The WhatsApp check is available in real time (POST /v1/lookup, up to 100 numbers per request) and in bulk jobs (POST /v1/jobs, up to 50,000 numbers and e-mails per job). It covers numbers from every country. Numbers in national format need default_country, so the API can convert them to E.164.
- A request can hold up to 20 checks. The total of numbers × checks is capped at 2,000 per lookup and 100,000 per job.
- Requests that look like sequential number ranges or generated e-mail lists are rejected. For numbers, that means 20 or more consecutive numbers in one request, which is refused with
suspected_enumeration. - Each account has a daily cap on the numbers it can check.
GET /v1/limitsshows the cap and how much of it is left. - Invalid, duplicate and suppressed numbers are reported per row and never checked.
Most answers arrive within the default 10-second wait. The rest come back as pending, and you can poll for them or receive them by webhook.
How do I use it responsibly?
Check only numbers you have a lawful reason to process, such as your own customers and people who signed up or asked to be contacted. Use the result to pick a channel for messages people expect. Don't use it to start conversations nobody asked for. WhatsApp's business messaging policy requires opt-in before businesses message people. Our acceptable use policy forbids unsolicited bulk messaging, building profiles of individuals and checking ranges of numbers to find out who uses WhatsApp.
Anyone whose number was checked can object through the opt-out form. Numbers on the suppression list are skipped (number_status: suppressed) and never charged.
Example request
Test keys (mv_test_…) are free and never reach a real network. +447700900001 always answers "registered". See test mode for the other test numbers.
curl https://api.mobilevalidate.com/v1/lookup \
-H "Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" \
-H "Content-Type: application/json" \
-d '{"numbers":["+447700900001"],"checks":["whatsapp"]}'// npm install mobilevalidate · Node.js 20+ · save as check.mjs and run: node check.mjs
import { MobileValidate } from "mobilevalidate";
// Omit apiKey to read MOBILEVALIDATE_API_KEY from the environment.
const mv = new MobileValidate({ apiKey: "mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" });
const { data, error } = await mv.lookup({
numbers: ["+447700900001"],
checks: ["whatsapp"],
});
if (error) console.error(error.code, error.message);
else console.dir(data.results, { depth: null });// Node.js 18+, Deno, Bun or the browser console (ES module: save as .mjs or use "type": "module").
const res = await fetch("https://api.mobilevalidate.com/v1/lookup", {
method: "POST",
headers: {
Authorization: "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym",
"Content-Type": "application/json",
},
body: JSON.stringify({
numbers: ["+447700900001"],
checks: ["whatsapp"],
}),
});
console.log(res.status, res.headers.get("x-request-id"));
console.dir(await res.json(), { depth: null });# pip install mobilevalidate-sdk
from mobilevalidate import MobileValidate
# Omit api_key to read MOBILEVALIDATE_API_KEY from the environment.
mv = MobileValidate(api_key="mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym")
result = mv.lookup(numbers=["+447700900001"], checks=["whatsapp"])
print(result)<?php
$ch = curl_init('https://api.mobilevalidate.com/v1/lookup');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'numbers' => ['+447700900001'],
'checks' => ['whatsapp'],
]),
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_RESPONSE_CODE), PHP_EOL, $response, PHP_EOL;package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"numbers":["+447700900001"],"checks":["whatsapp"]}`)
req, err := http.NewRequest("POST", "https://api.mobilevalidate.com/v1/lookup", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.Status, string(out))
}require "net/http"
require "json"
uri = URI("https://api.mobilevalidate.com/v1/lookup")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym"
req["Content-Type"] = "application/json"
req.body = JSON.generate({
"numbers" => ["+447700900001"],
"checks" => ["whatsapp"]
})
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.code, res.bodyRuns as pasted with the public sandbox key, which answers the test values only. In the SDKs, omit the key to use MOBILEVALIDATE_API_KEY.
Response (excerpt, test mode: the first item of results):
{
"kind": "phone",
"input": "+447700900001",
"e164": "+447700900001",
"country": "GB",
"number_status": "valid",
"checks": {
"whatsapp.registered": {
"service": "whatsapp.registered",
"status": "completed",
"registered": true,
"attributes": null,
"confidence": "high",
"confidence_score": 0.99,
"checked_at": "2026-09-25T14:25:29.489Z",
"cached": false,
"age_seconds": 0,
"billed": false,
"reason": null,
"poll_after_ms": null
}
},
"whatsapp": {
"service": "whatsapp.registered",
"status": "completed",
"registered": true,
"confidence": "high",
"confidence_score": 0.99,
"checked_at": "2026-09-25T14:25:29.489Z",
"cached": false,
"age_seconds": 0,
"billed": false,
"reason": null,
"poll_after_ms": null
},
"test": true
}Try it now
Realtime lookup via POST /v1/lookup. Every example runs as pasted: it uses the public sandbox key, which answers the test values for free. In the SDKs, omit the key to use MOBILEVALIDATE_API_KEY.
curl https://api.mobilevalidate.com/v1/lookup \
-H "Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" \
-H "Content-Type: application/json" \
-d '{"numbers":["+447700900001"],"checks":["whatsapp.registered"]}'// npm install mobilevalidate · Node.js 20+ · save as check.mjs and run: node check.mjs
import { MobileValidate } from "mobilevalidate";
// Omit apiKey to read MOBILEVALIDATE_API_KEY from the environment.
const mv = new MobileValidate({ apiKey: "mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" });
const { data, error } = await mv.lookup({
numbers: ["+447700900001"],
checks: ["whatsapp.registered"],
});
if (error) console.error(error.code, error.message);
else console.dir(data.results, { depth: null });// Node.js 18+, Deno, Bun or the browser console (ES module: save as .mjs or use "type": "module").
const res = await fetch("https://api.mobilevalidate.com/v1/lookup", {
method: "POST",
headers: {
Authorization: "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym",
"Content-Type": "application/json",
},
body: JSON.stringify({
numbers: ["+447700900001"],
checks: ["whatsapp.registered"],
}),
});
console.log(res.status, res.headers.get("x-request-id"));
console.dir(await res.json(), { depth: null });# pip install mobilevalidate-sdk
from mobilevalidate import MobileValidate
# Omit api_key to read MOBILEVALIDATE_API_KEY from the environment.
mv = MobileValidate(api_key="mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym")
result = mv.lookup(numbers=["+447700900001"], checks=["whatsapp.registered"])
print(result)<?php
$ch = curl_init('https://api.mobilevalidate.com/v1/lookup');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'numbers' => ['+447700900001'],
'checks' => ['whatsapp.registered'],
]),
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_RESPONSE_CODE), PHP_EOL, $response, PHP_EOL;package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"numbers":["+447700900001"],"checks":["whatsapp.registered"]}`)
req, err := http.NewRequest("POST", "https://api.mobilevalidate.com/v1/lookup", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.Status, string(out))
}require "net/http"
require "json"
uri = URI("https://api.mobilevalidate.com/v1/lookup")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym"
req["Content-Type"] = "application/json"
req.body = JSON.generate({
"numbers" => ["+447700900001"],
"checks" => ["whatsapp.registered"]
})
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.code, res.body// .mcp.json (Claude Code), ~/.cursor/mcp.json (Cursor) or .vscode/mcp.json ("servers" instead of "mcpServers")
{
"mcpServers": {
"mobilevalidate": {
"type": "http",
"url": "https://mcp.mobilevalidate.com/mcp",
"headers": { "Authorization": "Bearer ${MOBILEVALIDATE_API_KEY}" }
}
}
}
// Then ask your agent, e.g.:
// "Check +447700900001 with whatsapp.registered" → tool: lookup_numbersThe hosted MCP server is live for customers with an agent key (mv_agent_…) or a test key. Or run it locally with npx -y @mobilevalidate/mcp.
Real test-mode result for this check
{
"service": "whatsapp.registered",
"status": "completed",
"registered": true,
"attributes": null,
"confidence": "high",
"confidence_score": 0.99,
"checked_at": "2026-09-25T14:25:28.950Z",
"cached": false,
"age_seconds": 0,
"billed": false,
"reason": null,
"poll_after_ms": null
}What you get
Fields of checks["whatsapp.registered"], generated from the public service catalog.
| Field | Type | Meaning |
|---|---|---|
| registered | boolean | null | true = found, false = not found, null = unknown (not charged). |
| status | enum | completed, pending, unknown, unsupported_country or failed. |
| checked_at | timestamp | When the answer was obtained. |
Frequently asked questions
Does the check send a message to the number or notify its owner?
No. The check only answers whether a WhatsApp account is associated with the number. Nothing is sent to the number and the result contains no name, photo or profile.
Can a number show as registered even though its owner stopped using WhatsApp?
Yes, for a while. WhatsApp says in its help center that accounts are generally deleted after 120 days of inactivity, so a recently abandoned or recycled number can still show an account. Use checked_at and a short max_age when freshness matters.
What is the difference between this check and the WhatsApp Business check?
This check answers only whether an account exists. The WhatsApp Business check answers the same question and also reports whether the account is a WhatsApp Business account. If you request both, the API runs only the business check because it answers both.
Why do some numbers come back as unknown?
Unknown means we could not get a conclusive answer, for example because the check timed out. Unknown results have registered set to null and are not charged.
Can I use the result to start WhatsApp marketing to people who never opted in?
No. The acceptable use policy forbids unsolicited messaging, and WhatsApp's own business messaging policy requires opt-in before a business messages someone. Use the check to choose a channel for people who have already agreed to hear from you.

