Agencies that run WhatsApp campaigns for clients check each opted-in list before paying for template messages. A bulk job removes invalid and duplicate numbers for free, then marks which numbers have a WhatsApp account and which are Business accounts. You budget, segment and choose fallback channels on numbers that can receive the campaign. The client's opt-in still decides who may be messaged.
Why check a client's list before the campaign?
Client lists arrive in every shape: exports from a shop, a CRM, event sign-ups, a loyalty program. They mix national and international formats, contain the same person twice and include landlines and old numbers. On WhatsApp, Meta charges per delivered template message by category and recipient country (WhatsApp Business pricing in 2026 has the details). An unchecked list still costs the agency in other ways:
- Budgets built on the wrong count. A list of 80,000 rows may hold far fewer reachable WhatsApp contacts. Quoting a client on the raw count sets the wrong expectation.
- Failed sends and skewed reports. Messages to numbers without an account fail. Delivery and read rates then look worse than the campaign deserves.
- Quality signals. Sending to people who don't recognise the sender leads to blocks and reports, which can lower the sender number's quality rating and messaging limits.
- Wasted creative and approval work on segments that turn out to be tiny.
Two cleaning steps are free: invalid numbers and duplicates are flagged after E.164 normalization and never charged. The WhatsApp check then answers registered or not per number, and the WhatsApp Business check adds a business flag.
How does the workflow look?
- Confirm the basis. Before you touch a list, get the client's written confirmation that the contacts opted in to WhatsApp messages from that brand and that its lawful basis covers checking their WhatsApp accounts (see the consent section below).
- Normalize for free. Paste a sample into the free phone list cleaner, which formats and de-duplicates numbers in your browser, or let the job do it with
default_countryfor national formats. - Estimate. Run the free
POST /v1/jobs/estimatewithchecks: ["whatsapp.business"](or["whatsapp"]). It counts valid, invalid, duplicate, cached and suppressed rows and returns the maximum cost. Upload a CSV in the dashboard instead if you don't use the API. - Run the job with
POST /v1/jobs, the estimate asmax_cost, the client's API key and a client reference inmetadata. - Download CSV or NDJSON. Each line keeps its
row_no, the input's position in your upload, so you join results back to the client's file by row number. Numbers are masked in the file (+44770*****01) with the normalizede164alongside. - Segment. Registered, opted-in contacts go to the WhatsApp send. Others stay on the channels they agreed to, such as SMS or e-mail. Viber is available as a bulk check where the client's audience uses it.
- Budget. Put the registered count into the free WhatsApp cost calculator with the recipient country and template category to quote the campaign.
- Delete. Purge the job's results with
DELETE /v1/jobs/{id}once the segments are built, or let the retention period (30 days by default) expire.
Repeat campaigns for the same client inside the freshness window are answered from your account's cache for free.
Which checks should you use?
| Check | What it answers | When to use it | Link |
|---|---|---|---|
| Format and duplicates (free) | Is the number possible? Is it a repeat? | Every list | Phone list cleaner |
whatsapp | Does the number have a WhatsApp account? | Before every template campaign | WhatsApp check |
whatsapp.business | Account, and is it a Business account? | Consumer campaigns where business numbers should be reviewed | WhatsApp Business check |
viber, telegram | Is the number on another app? | Fallback for contacts who opted in there | Viber, Telegram |
carrier | Mobile, fixed line or VoIP? | Planning an SMS fallback | Carrier lookup |
What should you do with each result?
| Result | Suggested action |
|---|---|
number_status: invalid_number | Return to the client for correction; not checked, not charged |
number_status: duplicate | Keep one row per person; not charged |
registered: true, business: false, opted in | Include in the WhatsApp send |
registered: true, business: true | Review: often a shop or company phone in a consumer list |
registered: false | Leave out of the WhatsApp send; use the channels the contact agreed to |
unknown | Keep the contact as it was; unknown is free and says nothing bad |
| Suppressed (opt-out) | Skipped and never charged; don't re-add it |
What does a request look like?
This bulk job uses the public sandbox key from the test values page, so you can run it as is. It holds four test numbers and the first one again without its +.
curl https://api.mobilevalidate.com/v1/jobs \
-H "Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" \
-H "Content-Type: application/json" \
-d '{"numbers":["+447700900001","+447700900002","+447700900006","+447700900003","447700900001"],"checks":["whatsapp","whatsapp.business"],"metadata":{"client":"client-a"}}'// 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.jobs.create({
numbers: [
"+447700900001",
"+447700900002",
"+447700900006",
"+447700900003",
"447700900001",
],
checks: ["whatsapp", "whatsapp.business"],
metadata: { client: "client-a" },
});
if (error) console.error(error.code, error.message);
else console.dir(data, { 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/jobs", {
method: "POST",
headers: {
Authorization: "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym",
"Content-Type": "application/json",
},
body: JSON.stringify({
numbers: [
"+447700900001",
"+447700900002",
"+447700900006",
"+447700900003",
"447700900001",
],
checks: ["whatsapp", "whatsapp.business"],
metadata: { client: "client-a" },
}),
});
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.jobs.create(
numbers=[
"+447700900001",
"+447700900002",
"+447700900006",
"+447700900003",
"447700900001",
],
checks=["whatsapp", "whatsapp.business"],
metadata={ "client": "client-a" },
)
print(result)<?php
$ch = curl_init('https://api.mobilevalidate.com/v1/jobs');
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',
'+447700900002',
'+447700900006',
'+447700900003',
'447700900001',
],
'checks' => ['whatsapp', 'whatsapp.business'],
'metadata' => [ 'client' => 'client-a' ],
]),
]);
$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","+447700900002","+447700900006","+447700900003","447700900001"],"checks":["whatsapp","whatsapp.business"],"metadata":{"client":"client-a"}}`)
req, err := http.NewRequest("POST", "https://api.mobilevalidate.com/v1/jobs", 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/jobs")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym"
req["Content-Type"] = "application/json"
req.body = JSON.generate({
"numbers" => [
"+447700900001",
"+447700900002",
"+447700900006",
"+447700900003",
"447700900001"
],
"checks" => ["whatsapp", "whatsapp.business"],
"metadata" => { "client" => "client-a" }
})
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):
{"object": "job", "id": "job_0VaLWhF3ddVUBwzqBXr2", "status": "completed", "livemode": false,
"checks": ["whatsapp.business"],
"progress": {"total": 5, "checks_total": 5, "done": 5, "conclusive": 3, "non_billable": 5},
"cost": {"breakdown": [
{"check": "whatsapp.business", "price_mode": "batch", "reason": null, "checks": 0,
"unit_price": {"amount": "0.0003", "currency": "USD"}, "max_cost": {"amount": "0", "currency": "USD"}},
{"check": "whatsapp.business", "price_mode": "realtime", "reason": "small_batch", "checks": 4,
"unit_price": {"amount": "0.004", "currency": "USD"}, "max_cost": {"amount": "0", "currency": "USD"},
"countries": ["GB"], "batch_minimum": 132}]},
"retention_days": 30, "metadata": {"client": "client-a"}}Two things show here. The job ran only whatsapp.business, because the business check already answers whether an account exists, so you pay one check per number. And four UK numbers are fewer than 132 per country, so they sit on a small_batch line at the real-time price; a real campaign list gets the bulk price. Sandbox jobs are never billed.
The download (GET /v1/jobs/{id}/download?format=csv):
row_no,input_masked,e164,country,number_status,service,status,registered,business,checked_at,cached,billed,kind,email,email_status,whatsapp.business.status,whatsapp.business.registered,whatsapp.business.business,whatsapp.business.billed
1,"'+44770*****01",+447700900001,GB,valid,whatsapp.business,completed,true,false,2026-11-08T02:49:40.848Z,false,false,phone,,,completed,true,false,false
2,"'+44770*****02",+447700900002,GB,valid,whatsapp.business,completed,false,false,2026-11-08T02:49:40.848Z,false,false,phone,,,completed,false,false,false
3,"'+44770*****06",+447700900006,GB,valid,whatsapp.business,completed,true,true,2026-11-08T02:49:40.848Z,false,false,phone,,,completed,true,true,false
4,"'+44770*****03",+447700900003,GB,valid,whatsapp.business,unknown,,,,false,false,phone,,,unknown,,,false
5,"'+44770*****01",+447700900001,GB,duplicate,,,,,,false,false,phone,,,,,,Row 1 goes to the WhatsApp send, row 2 stays on the client's other channels, row 3 is a Business account to review, row 4 is unknown (free) and row 5 is a duplicate of row 1 (free). Join on row_no to put the segments back into the client's file.
How much does it cost?
As of 8 November 2026, the live catalog lists these prices per conclusive answer:
| Check | Real time | Bulk |
|---|---|---|
WhatsApp (whatsapp) | $0.004 | $0.00025 |
WhatsApp Business (whatsapp.business) | $0.004 | $0.0003 |
Telegram (telegram) | $0.001 | $0.0003 |
Viber (viber) | bulk only | $0.00015 |
Carrier lookup (carrier) | $0.003 | $0.0015 |
So a 50,000-number list of one country costs at most $12.50 for the WhatsApp check, or $15 with the business flag, before free invalid, duplicate, cached and unknown rows are taken off. Bulk prices for WhatsApp apply to groups of at least 132 numbers per country. Viber is bulk only, and bulk-only checks are billed at least the price of 500 checks for each country that returns an answer. Current prices are on the pricing page; top-ups get a bonus of +5% from $100, +8% from $500 and +10% from $2,000.
To test with real numbers first, every account can run 20 phone numbers and 10 e-mail addresses through the real-time checks for free on the dashboard's Try it page. Request access: approval is automatic, usually within 10–15 minutes.
How do you run jobs for several clients?
Keep each client separate from the first request:
- One key per client. Create a live key for each client in the dashboard and name it after the client. An account can have up to 25 active keys, and live keys can be limited to your server IP addresses. Revoke a key when the engagement ends.
- Client reference in
metadata. Every job echoes itsmetadataback, so the job object carries the client reference. - Logs by key. Request logs filter by key, so you can see each client's jobs; the Usage page shows checks and cost by day and service.
- Spend caps. Pass
max_coston every job, set from the estimate, so a wrong file can't overspend. - Own account where the client pays. If a client wants its own balance and invoices, it can open its own account with us, so its checks are billed to it directly.
Can you give clients the results or sell list checks?
Checking a list so you can run the client's campaign is part of delivering your service. Handing the results themselves to clients, selling list cleaning as a product, or letting clients run checks through your account goes further. Section 18.2 of the Terms says results and the service may not be resold or made available to third parties without our written agreement. There is no self-serve reseller program; e-mail sales@mobilevalidate.com with your client volume to agree terms.
What about consent and lawful basis?
The contacts belong to your client, and so does the lawful basis for messaging them and for checking them:
- Opt-in for the brand. WhatsApp's business messaging policy requires that people gave the business their number and opted in to its messages. A registered answer is not consent. Our guide to WhatsApp number checks, consent and the law covers the rules.
- Specific authority to check. Our acceptable use policy requires that the lawful basis covers checking a person's account on a named app. A general marketing opt-in is not enough. Ask the client to confirm this in writing.
- Your role. The client usually decides the purpose; the agency processes on its instructions. Put that in your contract and data processing terms, and sign our DPA for your account.
- No list building. Don't check bought, scraped or generated lists, and don't use checks to find new people to message. Requests with 20 or more consecutive numbers are refused as enumeration.
- Objections. People can exclude their number through the opt-out form; suppressed numbers are skipped and never charged.
What are the common mistakes?
- Treating
registered: trueas permission. It says an account exists, nothing more. - Ordering
whatsappandwhatsapp.businessseparately. The business check covers both; you pay once. - Splitting a country into small jobs. Below 132 numbers per country the real-time price applies. Send one job per client list.
- Joining results by phone number in a spreadsheet. Formats differ; join on
row_no. - Deleting contacts without WhatsApp. They may be reachable by SMS or e-mail on the channels they chose.
- Keeping results forever. Build the segments, then purge the job.
- Mixing clients on one key. Separate keys keep logs, revocation and disputes clean.
For template text, the free WhatsApp text formatter previews bold, italic and monospace before you submit a template for approval.
Frequently asked questions
Why check a client's list before a WhatsApp campaign?
WhatsApp charges per delivered template message, but an unclean list still costs you: failed sends, skewed reports and wasted setup work. A bulk check removes invalid and duplicate rows for free and flags numbers without a WhatsApp account, so you plan volume, budget and fallback channels on numbers that can actually receive the campaign.
Does a WhatsApp account mean the contact agreed to messages?
No. A registered answer only says an account exists. WhatsApp's business messaging policy separately requires the person's opt-in for the business that sends. The opt-in, and the lawful basis for checking, belong to your client and must cover these contacts.
Who is responsible for consent, the agency or the client?
The client collected the contacts, so it usually decides the purpose and holds the opt-in records. The agency acts on its instructions. Put that in your contract and data processing terms, and only check lists the client confirms were collected lawfully for this purpose.
How do I keep each client's jobs and spend apart?
Create one API key per client and name it after the client, and add the client's reference in each job's metadata. The request logs filter by key, and results come back per job. An account can have up to 25 active keys.
What is the WhatsApp Business check for?
It returns whether the number has WhatsApp and whether the account is a Business account. In consumer campaigns, business accounts are often shops or company phones, so many agencies review them before sending. You pay for one check per number even if you also ask for the plain WhatsApp check.
Can I give my clients the cleaned list or a check report?
Handing results to third parties, or selling list checks as a service, needs a written agreement with us under section 18.2 of the Terms. E-mail sales@mobilevalidate.com before you offer it.
How many numbers do I need per country for the bulk price?
WhatsApp checks get the bulk price for groups of at least 132 numbers from one country in one job. Smaller groups are priced at the real-time price. The free estimate shows this as a small_batch line before you start.
Is a number without WhatsApp a bad contact?
Not necessarily. The person may prefer SMS or e-mail. Keep the contact for the channels they agreed to and leave it out of the WhatsApp send.
Related
- ServiceCheck if a phone number is registered on WhatsApp
- ServiceCheck if a number is a WhatsApp Business account
- ServiceCheck if a phone number is registered on Viber
- Use caseChannel selection for consented messaging
- Use caseCRM data hygiene and contact re-validation
- ArticleBulk WhatsApp number check: how to verify a customer list (CSV upload or API)

