Lead Verification Workflow: Validate Numbers Across Channels
Most lead lists are dirtier than they look. Typos, dead numbers and contacts who simply are not on the channel you plan to use all erode campaign ROI. A lead-verification workflow fixes this by validating and enriching every number before you spend money messaging it, and you can build one with the number lookup APIs on a single account and one API key.
This guide is a practical design: five stages, a runnable Python pipeline, scoring rules, a refresh schedule and the mistakes that cost the most.
The pipeline at a glance
- Normalize every number to one format.
- Deduplicate so you never verify the same contact twice.
- Validate registration on each channel that matters for your market.
- Enrich only the numbers that passed.
- Score and route each contact to a channel it is confirmed to use.
The order matters: each stage shrinks the list that the next, more expensive stage has to process.
Stage 0: Collect and normalize
Get every number into a consistent shape: digits with the country code, no spaces or punctuation (E.164 without the +). The lookup operations tolerate a leading +, and the WhatsApp check also accepts spaces, dashes and a 00 prefix, but a single canonical form is what makes deduplication reliable. Reject anything you cannot parse instead of guessing a country code, since a wrong guess produces a confident "not registered".
Stage 1: Deduplicate
Remove duplicate numbers across all sources, such as forms, imports and event lists, before any call. Dedupe on the normalized form, keep a list of the source records each number came from, and verify each number once.
Stage 2: Validate per channel
This is the core. For each number, run the lightweight registration check on every platform that matters. The checks are independent: a hit on one platform says nothing about another.
| Channel | Operation | Notes |
|---|---|---|
check-exist-by-phone, or batch-check-10 | isExist is true, false or null; up to 10 numbers per batch call | |
| Telegram | check-exists | peer is +phone, @username or user:<id> |
| Signal | check | registered is true, false or null |
| Viber | check | the reply is wrapped, so read registered from inside the body |
| Snapchat | check-phone, or check-phones-batch | up to 50 numbers per batch call |
Treat null as "unknown, retry later", never as "no". Each channel has its own guide with the details: WhatsApp, Telegram, Signal, Viber and Snapchat. If you are choosing between the first two, read WhatsApp vs Telegram number lookup, and for large lists use the resumable approach in bulk verification for WhatsApp numbers.
A runnable validation stage
The script below normalizes, dedupes and runs four of the channel checks per number with a small thread pool; Snapchat follows the same pattern. It keeps the three-state answer as it is and never raises on a missing field.
import re
from concurrent.futures import ThreadPoolExecutor
import requests
API_KEY = "YOUR_API_KEY"
ROOT = "https://api.opendata-api.com/api/project"
def call(project, operation, **params):
params["key"] = API_KEY
try:
resp = requests.get(f"{ROOT}/{project}/{operation}", params=params, timeout=30)
body = resp.json()
except (requests.RequestException, ValueError):
return {}
# A reply can be wrapped more than once as {"code": ..., "data": {...}}
while isinstance(body, dict) and "code" in body and isinstance(body.get("data"), (dict, list)):
body = body["data"]
return body if isinstance(body, dict) else {}
def normalize(raw):
digits = re.sub(r"\D", "", raw)
if raw.strip().startswith("00"):
digits = digits[2:]
return digits if 8 <= len(digits) <= 15 else None
def check_channels(number):
return {
"number": number,
"whatsapp": call("whatsapp", "check-exist-by-phone", phoneNumber=number).get("isExist"),
"telegram": call("telegram", "check-exists", peer="+" + number).get("exists"),
"signal": call("signal", "check", number=number).get("registered"),
"viber": call("viber", "check", number=number).get("registered"),
}
raw_numbers = ["+1 555 123 4567", "0015551234567", "15551234567", "not a number"]
clean = sorted({n for n in map(normalize, raw_numbers) if n})
with ThreadPoolExecutor(max_workers=5) as pool:
results = list(pool.map(check_channels, clean))
for row in results:
print(row)
Keep concurrency modest at first and watch the response codes before you scale it up. A None in a column means unknown, so queue that number for a later retry.
Stage 3: Enrich the reachable numbers
For numbers that passed validation, pull public profile data to qualify and prioritize: the WhatsApp avatar, About text and business profile (see the profile guide), the Telegram bio and photo, the Viber display name, or the Snapchat username. This turns a bare phone number into a context-rich lead. Skip enrichment for numbers that failed; that is the cost-saving point of validating first. Profile fields are often empty by privacy choice, so score on what is present and never penalize an empty field.
Stage 4: Score and route
Turn the results into a decision:
def route(row):
channels = [c for c in ("whatsapp", "telegram", "signal", "viber") if row.get(c) is True]
unknown = [c for c in ("whatsapp", "telegram", "signal", "viber") if row.get(c) is None]
if channels:
status = "reachable"
elif unknown:
status = "retry"
else:
status = "unreachable"
return {"number": row["number"], "channels": channels, "status": status}
- Route each contact to a channel it is confirmed to use, ordered by your own preference for that market.
- Suppress
unreachablenumbers from paid campaigns, and keep them in a table so you do not pay to check them again next week. - Retry
retryrows after a pause, since unknown is not a verdict. - Flag numbers with no registration anywhere but a rich source record (for example, a paying customer) for manual review, because a typo is more likely than a missing user.
Keep the data fresh
Re-verify on a schedule rather than before every send: monthly for active lists is a reasonable starting point, and sooner for contacts who are about to receive a high-value message. Store the result, the channel and a timestamp with each number, and cache the 24-hour userHash values only for the enrichment calls that follow immediately.
Common mistakes
- Enriching before validating. It is the single biggest avoidable cost.
- Reading
nullas "no". An indeterminate answer needs a retry, not a suppression. - Probing emails in bulk. The Snapchat email probes use account recovery, so a positive hit can send a real recovery email to the owner. Use them in a sign-up form, not on a list.
- Skipping consent. Verification tells you a channel exists, not that the person agreed to hear from you.
Compliance
Process only numbers you have a lawful basis to use, honor opt-outs, and respect each platform's terms and local privacy and anti-spam law. This is general guidance and not legal advice. Once a contact is verified and consented, sending WhatsApp messages via API and sending Telegram messages from Python are the natural next steps.
FAQ
Should I validate every channel for every number? Only the channels that matter for your market and campaign. Each check is cheap, but running all of them on a large list adds up, so start with the one or two platforms your audience actually uses and add others when the data justifies it.
What does a null result mean? It means the answer was indeterminate, for example because of a transient condition. Retry after a short pause, and do not treat it as a negative.
How often should I re-verify a list? A scheduled refresh, such as monthly for active lists, is a reasonable starting point. Re-check sooner for contacts you are about to contact with something important.
Can I run different lookup projects with one account? Yes. Every lookup project works with the same API key and the same credit balance, so one pipeline can call WhatsApp, Telegram, Signal, Viber and Snapchat.
Does a successful lookup mean I can message the person? No. It only means the number has an account on that platform. You still need consent or another lawful basis, and you must follow the platform's rules.
Put it together
Browse the full catalog on the All APIs page, test any operation in the browser, and review pricing to estimate the credit cost for your list size. New accounts include free credits, so you can prototype the whole workflow at no cost: sign in and copy a key from your Dashboard.