How to check a number in Python with the Wapito WhatsApp API

Normalise to international format, check in modest batches, and store every result with its date so you never check the same number twice.

A script, a token and an HTTP client is the whole stack. Everything below runs from a file you can execute today, and the same code moves to a worker or a container without changing shape.

Checking numbers from Python is a batch job with a database in the middle: normalise with the phonenumbers package, post batches with requests, write every result with its date, and read /usage before the next batch. The three calls are simple; the discipline around them is what keeps the allowance intact and the number out of trouble, so put the loop in a function you can test with a stub.

Before you start

  • pip install requests
  • os.environ["WAPITO_TOKEN"]
  • A channel with a WhatsApp number linked to it, and its API token. Create one in the dashboard — the authentication guide shows where the token goes.

How it works

  1. Normalise the numbers first

    Convert to international format with a real phone-number library before you check anything. A number with a national trunk prefix left on is a different number, and checking it wastes an allowance and teaches you nothing.

    import requests
    
    url = "https://api.wapito.com/v1/contacts/+15551234567/exists"
    
    headers = {"Authorization": "Bearer wpt_YOUR_TOKEN"}
    
    response = requests.get(url, headers=headers)
    
    print(response.json())

    In Python a single check is requests.get on /contacts/{id}/exists after phonenumbers.format_number(parsed, PhoneNumberFormat.E164) has produced the id; requests leaves the plus sign alone in a path segment. The dict answers with exists as a bool and the jid the account maps to. Use this for one-off lookups only, because the batch endpoint is the right tool for a list.

    API reference for this step
  2. Check a batch

    Send a modest batch rather than one request per number. The response tells you, per number, whether an account exists and what identity it maps to, which is the part worth storing.

    import requests
    
    url = "https://api.wapito.com/v1/contacts/check"
    
    payload = { "phones": ["+15551234567", "+15559876543", "+15550000000"] }
    headers = {
        "Authorization": "Bearer wpt_YOUR_TOKEN",
        "Content-Type": "application/json"
    }
    
    response = requests.post(url, json=payload, headers=headers)
    
    print(response.json())

    In Python post a batch with requests.post(url, json={"phones": chunk}) where chunk is a slice of your normalised list; the response dict holds checked, found and a results list with the phone, whether it exists and the jid it maps to. Iterate results directly rather than zip against your chunk, since the API answers with the phone it actually checked.

    API reference for this step
  3. Store the result and watch the allowance

    Write each answer back to your own database with the date you checked, then read the usage endpoint before the next batch. Re-checking numbers you already know about is the easiest way to burn an allowance and attract attention at the same time.

    import requests
    
    url = "https://api.wapito.com/v1/usage"
    
    querystring = {"from":"2026-09-01","to":"2026-09-15"}
    
    headers = {"Authorization": "Bearer wpt_YOUR_TOKEN"}
    
    response = requests.get(url, headers=headers, params=querystring)
    
    print(response.json())

    In Python read the allowance with requests.get on /usage before each batch and compute the headroom from limits["number_checks_per_day"] minus totals["number_checks"]; if it is smaller than len(chunk), stop the run and log the shortfall rather than posting a batch that will be answered with 429 quota_exceeded. The counters reset daily, so a scheduler can simply try again tomorrow.

    API reference for this step

The whole script

Every step above in one runnable file. Save it as check-number.py, put your token in the environment, and run it.

"""Check Number with the Wapito WhatsApp API.

Normalise to international format, check in modest batches, and store every result with its date so you never check the same number twice.

Run it with:
    export WAPITO_TOKEN="wpt_..."
    python check-number.py
"""
import os

import requests

BASE_URL = "https://api.wapito.com/v1"
TOKEN = os.environ["WAPITO_TOKEN"]

# --- Normalise the numbers first ---
url = BASE_URL + "/contacts/+15551234567/exists"

headers = {"Authorization": "Bearer " + TOKEN}

response = requests.get(url, headers=headers)

print(response.json())

# --- Check a batch ---
url = BASE_URL + "/contacts/check"

payload = { "phones": ["+15551234567", "+15559876543", "+15550000000"] }
headers = {
    "Authorization": "Bearer " + TOKEN,
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())

# --- Store the result and watch the allowance ---
url = BASE_URL + "/usage"

querystring = {"from":"2026-09-01","to":"2026-09-15"}

headers = {"Authorization": "Bearer " + TOKEN}

response = requests.get(url, headers=headers, params=querystring)

print(response.json())

Receive the webhook

FastAPI receiver for channel — it verifies the signature, answers immediately and does the work after. Install it with pip install fastapi uvicorn and save it as webhook.py.

import hashlib
import hmac
import os
import time

from fastapi import FastAPI, Header, HTTPException, Request

app = FastAPI()
SECRET = os.environ["WAPITO_WEBHOOK_SECRET"].encode()
EVENTS = ["channel"]


def verify(raw: bytes, header: str | None) -> bool:
    """Checks the X-Wapito-Signature header: t=<ms>,v1=<hex hmac-sha256>."""
    if not header:
        return False
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    ts, sig = parts.get("t"), parts.get("v1")
    if not ts or not sig:
        return False
    if abs(time.time() * 1000 - int(ts)) > 300_000:  # 5 minute clock skew
        return False
    expected = hmac.new(SECRET, ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)


@app.post("/wapito")
async def wapito(request: Request, signature: str | None = Header(default=None, alias="X-Wapito-Signature")):
    raw = await request.body()
    if not verify(raw, signature):
        raise HTTPException(status_code=401, detail="bad signature")
    payload = await request.json()
    if payload["event"] in EVENTS:
        print(payload["event"], payload["data"])
    return {"ok": True}  # answer 2xx fast; do the real work in a queue
Every event, its payload and the retry rules

Errors you may hit

Gotchas

  • requests does not raise on a 4xx by itself. Call response.raise_for_status() or check response.ok, otherwise a 429 send_rate_limited silently looks like a successful send and your loop keeps hammering the queue.
  • json= and data= are not interchangeable. Pass json={...} so requests sets Content-Type: application/json and encodes UTF-8 for you; data= with a dict sends form encoding and the API answers 400 invalid_request.
  • The default requests timeout is None, which means a stalled connection hangs your worker forever. Always pass timeout=(5, 30) and treat a read timeout as "maybe delivered" - re-check with the message id before you send again.
  • A group id such as 120363000000000000@g.us is a string, not a number. Building it with an f-string from an int drops the precision and you get 404 not_found.
  • Emoji and accented text need no extra work in Python 3, but reading a CSV of numbers with the default encoding on Windows will mangle them - open files with encoding="utf-8".

Where to run it

  • Google Cloud Run (container, scale to zero, one revision per deploy)
  • Render background worker for the sender, Render web service for the webhook receiver
  • A 1 GB VPS with systemd for the worker and Caddy in front of the FastAPI receiver
  • AWS Lambda + API Gateway for the webhook receiver only (the sender needs a long-lived process for queue spacing)

Pitfalls in Python

  • phonenumbers.parse without a default region raises NumberParseException on a local number; pass the country you expect and catch the exception per row so one bad line does not end the run, collecting the failures with their line numbers for a manual pass afterwards. is_valid_number is a separate call and worth making before the format, since parse accepts digit strings that no carrier has ever issued.
  • A pandas merge on a column of numbers that were read as int64 has lost leading plus signs and sometimes digits; read the number column as str with dtype and keep it that way through every transform, including the write back to CSV, which will otherwise reformat it.
  • Wrapping the batch call in a retry decorator that retries on any HTTPError re-sends a batch that was answered 429 for quota, which burns the reset window; retry only on network errors, and make the decorator re-raise the 429 so the caller can end the run cleanly. tenacity's retry_if_exception_type with requests.ConnectionError expresses exactly that rule.

Frequently asked questions

How many numbers can I check per day?

Your plan sets an explicit daily allowance, and the API tells you how much is left through the usage endpoint. The harder limit is behavioural: even inside your allowance, checking a large list in a short burst is the pattern that draws attention, so spread it out.

Why is this riskier than sending a message?

Because a check involves no relationship at all. Sending a message to someone who wrote to you is normal behaviour; asking the network about thousands of numbers you have never contacted is what a scraper does. The abuse systems weight that difference heavily, and so does Wapito's metering.

Does a check tell me the person's name?

No. It tells you whether an account exists and gives you the identity you would address, nothing more. Profile details are a separate call with their own privacy rules, and a contact who has restricted their profile will not reveal a name or a photo to a stranger's number.

Related

Try it on your own number

Create a channel, link a WhatsApp number by QR or pairing code, and call the API in a couple of minutes. The Sandbox plan is free and needs no card.