Developers

How to check if a phone number is on WhatsApp with an API

How a WhatsApp registration check works, what registered, not registered and unknown mean, and how to call it responsibly from cURL, Node and Python.

By Published 8 min read

On this page

To check whether a phone number is on WhatsApp, send the number to a registration-check API and read back one of three answers: registered, not registered or unknown, each with a timestamp. With MobileValidate that is one POST /v1/lookup call with checks: ["whatsapp"]. This guide covers how such a check works, how to handle every answer in code, and how to use it responsibly.

What does a WhatsApp registration check actually answer?

It answers one narrow question: is a WhatsApp account associated with this phone number right now? Nothing more. It doesn't say who owns the number, whether they read messages, or whether the phone is switched on.

WhatsApp ties each account to a phone number and verifies that number with a code at sign-up. So an account is a useful sign that the number was in use on a smartphone at some point. That makes the check valuable for two jobs:

  • Channel selection. Before you send a passcode, a delivery update or a reminder the person asked for, you know whether WhatsApp is an option at all. See channel selection.
  • A deliverability signal. A number with an account is less likely to be a typo or a made-up entry. It is not proof of identity.

Because WhatsApp is so widely used (WhatsApp announced two billion users in 2020), a "no" in a market where almost everyone uses it is informative. A "no" in a market where other apps dominate says much less. The channel-by-country guide covers that.

A terminal sends an API request and a JSON response returns true, false and null values.A terminal sends an API request and a JSON response returns true, false and null values.
One REST call. Every answer is true, false or null (unknown).

How does a registration check work, conceptually?

Messaging apps need a way for users to find friends. When you install one, it can compare the numbers in your address book with its account directory and show which contacts use the app. This is called contact discovery. A registration check asks the same kind of question for a single number: does the directory hold an account for it?

The answer comes back as one of three states:

StateMeaningBilled?
registered: trueAn account exists for the numberYes
registered: falseA conclusive answer: no accountYes
registered: nullNo conclusive answer (timeout, service unavailable, unsupported country)No

Two design rules follow from this. First, unknown is never a no. A timeout says nothing about the number, so the API reports null and the reason. You're not charged for inconclusive results (unknown, unsupported country, timeout, invalid, duplicate). Second, every answer is dated. checked_at says when the answer was obtained. Accounts come and go, and numbers get reassigned by carriers, so an answer from last quarter is weaker than one from this morning.

We don't describe the upstream mechanics in more detail, and you shouldn't need them. Your integration depends only on the three states, the timestamp and the reason codes.

How do I make my first request?

Use a test key (mv_test_…). Test keys are free, never reach a real network and return fixed answers for the numbers in the reserved range +44 7700 900000–900999. …001 is registered, …002 is not, …003 is unknown and …004 stays pending for about five seconds. See test mode.

Shell
curl https://api.mobilevalidate.com/v1/lookup \
  -H "Authorization: Bearer $MOBILEVALIDATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"numbers": ["+447700900001", "+447700900002", "+447700900003", "+447700900004"],
       "checks": ["whatsapp"], "wait": 2}'

With a short wait of 2 seconds, the request returns before …004 is done. This is real test-mode output (the summary, next and the four check results):

JSON
{
  "id": "lkp_0VWFWKcBPOs4UJIn7CeW",
  "status": "pending",
  "next": {"poll_url": "/v1/lookups/lkp_0VWFWKcBPOs4UJIn7CeW", "poll_after_ms": 2000},
  "summary": {"total": 4, "registered": 1, "not_registered": 1, "unknown": 1, "pending": 1,
              "invalid": 0, "suppressed": 0}
}
JSON
{"service": "whatsapp.registered", "status": "completed", "registered": true,  "confidence": "high", "checked_at": "2026-09-25T16:15:04.516Z", "billed": false, "reason": null}
{"service": "whatsapp.registered", "status": "completed", "registered": false, "confidence": "high", "checked_at": "2026-09-25T16:15:04.516Z", "billed": false, "reason": null}
{"service": "whatsapp.registered", "status": "unknown",   "registered": null,  "confidence": null,   "checked_at": null, "billed": false, "reason": "UPSTREAM_TIMEOUT"}
{"service": "whatsapp.registered", "status": "pending",   "registered": null,  "confidence": null,   "checked_at": null, "billed": false, "reason": null, "poll_after_ms": 2000}

(Excerpt: some fields are trimmed. billed is false here because test keys never bill.)

How do I handle pending answers?

Most answers arrive within the default 10-second wait. The rest come back as pending, and the whole lookup has status: "pending" plus a next.poll_url. You have two options:

  1. Poll. GET /v1/lookups/{id}?wait=10 long-polls (wait can be up to 30 seconds) and returns as soon as the lookup completes. Respect poll_after_ms between polls.
  2. Webhook. Pass a verified webhook_endpoint_id and receive lookup.completed. See webhooks.

Polling the lookup above five seconds later returned "status": "completed", and the fourth result became:

JSON
{"service": "whatsapp.registered", "status": "completed", "registered": true,
 "confidence": "high", "confidence_score": 0.99, "checked_at": "2026-09-25T16:15:11.607Z",
 "cached": false, "age_seconds": 0, "billed": false, "reason": null, "poll_after_ms": null}

In a user-facing flow such as sign-up, don't block on a pending answer. Continue with your default channel and use the result for the next message.

What does the code look like in Node and Python?

Both examples send numbers in the POST body (never in the URL), poll while the lookup is pending, and map each answer to a channel decision.

Node.js / TypeScript (built-in fetch, Node 18+):

TypeScript
const API = "https://api.mobilevalidate.com";
const headers = {
  Authorization: `Bearer ${process.env.MOBILEVALIDATE_API_KEY}`,
  "Content-Type": "application/json",
};

type Check = { status: string; registered: boolean | null; reason: string | null; checked_at: string | null };

export async function whatsappStatus(numbers: string[]) {
  let res = await fetch(`${API}/v1/lookup`, {
    method: "POST", headers,
    body: JSON.stringify({ numbers, checks: ["whatsapp"], wait: 10 }),
  });
  let lookup = await res.json();
  while (lookup.status === "pending") {
    await new Promise((r) => setTimeout(r, lookup.next?.poll_after_ms ?? 2000));
    res = await fetch(`${API}/v1/lookups/${lookup.id}?wait=10`, { headers });
    lookup = await res.json();
  }
  return lookup.results.map((r: any) => {
    const c: Check | undefined = r.checks?.["whatsapp.registered"];
    const channel = c?.registered === true ? "whatsapp" : "sms"; // null (unknown) falls back, never blocks
    return { e164: r.e164, number_status: r.number_status, registered: c?.registered ?? null,
             reason: c?.reason ?? null, checked_at: c?.checked_at ?? null, channel };
  });
}

Python (requests):

Python
import os, time, requests

API = "https://api.mobilevalidate.com"
H = {"Authorization": f"Bearer {os.environ['MOBILEVALIDATE_API_KEY']}"}

def whatsapp_status(numbers, default_country=None):
    body = {"numbers": numbers, "checks": ["whatsapp"], "wait": 10}
    if default_country:
        body["default_country"] = default_country  # needed for national formats like "07700 900001"
    lookup = requests.post(f"{API}/v1/lookup", json=body, headers=H, timeout=40).json()
    while lookup.get("status") == "pending":
        time.sleep((lookup.get("next") or {}).get("poll_after_ms", 2000) / 1000)
        lookup = requests.get(f"{API}/v1/lookups/{lookup['id']}", params={"wait": 10},
                              headers=H, timeout=40).json()
    out = []
    for r in lookup["results"]:
        c = (r.get("checks") or {}).get("whatsapp.registered") or {}
        out.append({"e164": r.get("e164"), "number_status": r["number_status"],
                    "registered": c.get("registered"), "reason": c.get("reason"),
                    "checked_at": c.get("checked_at")})
    return out

Production code should also handle HTTP errors: 429 rate_limited (retry after Retry-After), 402 insufficient_balance, and 403 suspected_enumeration. The errors reference lists them all.

How should my application act on each answer?

Store the answer with its timestamp and decide per message type. A reasonable starting table:

AnswerSign-up / OTPOrder updates the customer opted intoCRM record
registered: trueOffer WhatsApp as a delivery option; keep SMS as fallbackSend on WhatsApp if the customer chose itStore true + checked_at
registered: falseGo straight to SMS or voiceUse SMS or e-mailStore false + checked_at
registered: null (unknown)Use your default channelKeep the previous routingDon't overwrite a stored answer
number_status: invalid_numberAsk the user to correct the numberFix the recordFlag for cleanup
number_status: suppressedUse your default channelUse your default channelDon't check again

Two practical rules. Never overwrite a conclusive stored answer with null. A timeout today doesn't erase what you learned last week. And refresh on a schedule, for example monthly, or when a WhatsApp delivery fails. Repeat checks inside the freshness window are served from your account's cache for free (cached: true, billed: false).

When should I use the WhatsApp Business check instead?

Request whatsapp.business when you need to know whether the account is a business account, for example to tell a supplier's support line apart from a personal number in a B2B CRM. It answers both questions at once. If a request asks for both whatsapp and whatsapp.business, they collapse into one whatsapp.business result.

Real test-mode output for +447700900006 (registered, business) and +447700900002 (no account):

JSON
{"whatsapp.business": {"service": "whatsapp.business", "status": "completed", "registered": true,
  "attributes": {"business": true}, "confidence": "high", "checked_at": "2026-09-25T16:15:11.650Z", "billed": false}}
{"whatsapp.business": {"service": "whatsapp.business", "status": "completed", "registered": false,
  "attributes": {"business": false}, "confidence": "high", "checked_at": "2026-09-25T16:15:11.650Z", "billed": false}}

The older top-level whatsapp object mirrors this result with a business field, so integrations written for the first API version keep working. In real time the business flag may be null even when registered is conclusive. See the WhatsApp Business check.

What are the limits, and why do they exist?

LimitValue
Numbers (and e-mails) per real-time lookup100
Identifiers per bulk job50,000
Checks per request20
Identifiers × checks2,000 per lookup, 100,000 per job
Request rate per key10 per second, burst 20
Consecutive numbers in one request19 at most; 20 or more → 403 suspected_enumeration

The last rule deserves an explanation. Contact discovery can be abused to map who uses an app. In 2021, researchers showed they could query 10% of US mobile numbers against WhatsApp and 100% against Signal with modest resources. In 2025, researchers from the University of Vienna and SBA Research reported that WhatsApp's contact discovery allowed the enumeration of 3.5 billion accounts, which Meta has since mitigated. A check API must not become a shortcut for that kind of mapping. So we refuse sequential ranges, cap daily volume per account, and return only yes/no/unknown, never names, photos or profiles. See rate limits and abuse.

How do I use the check responsibly?

The number usually belongs to a person, so the check is likely to be processing of personal data. Three rules help you stay within data-protection law and platform policy (for your specific case, consult your own counsel):

  1. Check only numbers you have a lawful reason to process: customers, sign-ups, and leads who gave you their number. Under the GDPR you need a lawful basis (Article 6), and data minimisation (Article 5(1)(c)) suggests keeping only what you need: the answer and its date, not the whole response.
  2. Use the answer to choose a channel, not to start conversations. The WhatsApp Business Messaging Policy says businesses may only contact people who gave them their number and opted in to receive messages. A true answer is not consent.
  3. Respect objections. People can object through our opt-out form. Suppressed numbers are skipped and never charged, and you should honour objections in your own systems too.

Our acceptable use policy forbids unsolicited bulk messaging, building profiles of individuals and checking ranges of numbers to find out who uses WhatsApp.

What are the key takeaways?

  • A WhatsApp check returns registered, not registered or unknown, always with checked_at. Only conclusive answers are billed.
  • Unknown is missing data. Fall back to your default channel and never overwrite a stored conclusive answer with it.
  • Use a short wait in user-facing flows and poll GET /v1/lookups/{id} or use a webhook for pending answers.
  • Request whatsapp.business when you also need the business flag.
  • The check is for numbers you already hold and for picking a channel for expected messages. Sequential ranges are refused by design.
  • Build and test every branch for free with the test numbers, then read the WhatsApp check page for pricing and coverage.

Sources

  1. WhatsApp Business Messaging Policy — WhatsApp, 2026
  2. Researchers discover security vulnerability in WhatsApp — University of Vienna, 2025
  3. All the Numbers are US: Large-scale Abuse of Contact Discovery in Mobile Messengers (NDSS 2021) — IACR ePrint / NDSS, 2021
  4. Two Billion Users: Connecting the World Privately — WhatsApp, 2020
  5. General Data Protection Regulation (EU) 2016/679 — European Union, 2016

Frequently asked questions

Does checking a number send a WhatsApp message or notify the person?

No. The check answers whether an account is associated with the number. No message is sent, and nothing appears on the person's phone. No name, photo or profile is returned.

What should my code do when the answer is unknown?

Treat unknown (registered: null) as missing data, not as a no. Keep your default channel, such as SMS, and check again later. Unknown answers are not charged.

Why was my request refused with suspected_enumeration?

The request contained 20 or more consecutive numbers. Checking number ranges is how people try to build lists of platform users, so the API refuses it. Check only numbers you already hold, such as customers and sign-ups.

Can I use the result to start WhatsApp conversations with new contacts?

No. WhatsApp's business messaging policy requires that people gave you their number and opted in to receive messages from you. Use the check to pick a channel for messages people already expect.

How do I find out whether the account is a WhatsApp Business account?

Request whatsapp.business instead of whatsapp. It answers both questions: whether an account exists, and in the business attribute whether it is a business account. In real time the business flag may be null.

All articles

Know before you send.

Tell us about your use case. We review every request and set you up with test and live keys.