Phone Numbers
Purchase, verify, KYC, and release WhatsApp phone numbers for your WABA
Phone Numbers
Manage WhatsApp phone numbers: purchase, verify, and release numbers for your WABA. Numbers are available in 50+ countries. US numbers provision instantly; regulated countries require a one-time identity-verification (KYC) form first, after which the number activates within a few business days. Use the whatsapp.number.activated webhook to know when a regulated number is ready instead of polling.
List Available Countries
Returns the countries you can buy a number in, each with its monthly price (cents), whether it needs KYC, and whether outbound calling is available. Use this to drive a country picker before purchasing.
const { data } = await zernio.whatsappphonenumbers.listWhatsAppNumberCountries();
data.countries.forEach(c =>
console.log(`${c.code}: $${(c.monthlyCents / 100).toFixed(2)}/mo${c.needsKyc ? ' (KYC)' : ''}`)
);response = client.whatsapp_phone_numbers.list_whats_app_number_countries()
for c in response.countries:
kyc = ' (KYC)' if c.needs_kyc else ''
print(f"{c.code}: ${c.monthly_cents / 100:.2f}/mo{kyc}")curl "https://zernio.com/api/v1/whatsapp/phone-numbers/countries" \
-H "Authorization: Bearer YOUR_API_KEY"Countries & pricing
Price is per number, per month, billed by the active day and separate from Meta's usage fees. Countries marked † provision instantly; all others require a one-time identity-verification (KYC) form and activate within 1-3 business days. The countries endpoint is the live source of truth.
| Price / mo | Countries |
|---|---|
| $2 | United States †, Canada †, U.S. Virgin Islands †, Ireland, Austria, Belgium, Czechia, Finland, France, Germany, Greece, Hungary, Italy, Lithuania, Luxembourg, New Zealand, Norway, Portugal, Romania, Slovakia, Spain, Switzerland, United Kingdom |
| $3 | Puerto Rico †, Netherlands †, Brazil, Croatia, Estonia |
| $4 | Israel †, Latvia |
| $5 | Cyprus, Mexico, Singapore |
| $6 | Iceland †, Australia |
| $8 | Denmark †, Chile, Dominican Republic, Réunion, Turkey |
| $10 | El Salvador †, Guadeloupe, Martinique, Panama, Poland, South Africa, Sweden, French Guiana |
| $12 | Costa Rica |
| $15 | Georgia, Thailand |
| $20 | Colombia †, Indonesia |
† Provisions instantly. Every other country requires the one-time KYC form and a short regulatory review. Country codes are ISO-2 (for example US, GB, DE); the countries endpoint returns each code with its current price.
List Your Numbers
const { data } = await zernio.whatsappphonenumbers.getWhatsAppPhoneNumbers();
data.numbers.forEach(n => console.log(`${n.phoneNumber} (${n.status})`));response = client.whatsapp_phone_numbers.get_whats_app_phone_numbers()
for n in response.numbers:
print(f"{n.phone_number} ({n.status})")curl "https://zernio.com/api/v1/whatsapp/phone-numbers" \
-H "Authorization: Bearer YOUR_API_KEY"Number Status
A live snapshot of a connected number, straight from Meta: display name + approval, quality rating, messaging-limit tier, throughput, official-business badge, connection status, and the owning Business Account's verification status. Useful for monitoring deliverability and limits.
const { data } = await zernio.whatsapp.getWhatsAppNumberInfo({
query: { accountId: 'YOUR_ACCOUNT_ID' }
});
console.log(data.phone.quality_rating, data.phone.messaging_limit_tier);response = client.whatsapp.get_whats_app_number_info(account_id='YOUR_ACCOUNT_ID')
print(response.phone.quality_rating, response.phone.messaging_limit_tier)curl "https://zernio.com/api/v1/whatsapp/number-info?accountId=YOUR_ACCOUNT_ID" \
-H "Authorization: Bearer YOUR_API_KEY"Purchase a Number
Pass country (ISO-2, default US) to choose the country. For instant countries the number provisions inline. For a regulated country the response is status: "kyc_required" with a kycUrl. Collect the KYC form and submit it before the number is ordered.
const { data } = await zernio.whatsappphonenumbers.purchaseWhatsAppPhoneNumber({
body: { profileId: 'YOUR_PROFILE_ID', country: 'GB' }
});
if (data.checkoutUrl) {
console.log('Complete payment:', data.checkoutUrl); // first number: Stripe checkout
} else if (data.status === 'kyc_required') {
console.log('Regulated country, collect KYC:', data.kycUrl);
} else {
console.log('Number provisioned:', data.phoneNumber.phoneNumber);
}response = client.whatsapp_phone_numbers.purchase_whats_app_phone_number(
profile_id='YOUR_PROFILE_ID', country='GB'
)
if getattr(response, 'checkout_url', None):
print(f"Complete payment: {response.checkout_url}") # first number: Stripe checkout
elif getattr(response, 'status', None) == 'kyc_required':
print(f"Regulated country, collect KYC: {response.kyc_url}")
else:
print(f"Number provisioned: {response.phone_number.phone_number}")curl -X POST https://zernio.com/api/v1/whatsapp/phone-numbers/purchase \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"profileId": "YOUR_PROFILE_ID", "country": "GB"}'Duplicate protection. Retrying a purchase is safe if you send a purchaseIntentId (any string up to 100 chars, one per intended purchase): a repeat with the same key returns status: "already_purchased" with the existing number instead of buying a second one. Independently, any second purchase within 10 minutes of a previous one returns 409 with code: "PURCHASE_VELOCITY"; pass allowMultiple: true to confirm it is intentional (for example, bulk provisioning).
Regulated Countries (KYC)
Regulated (non-US) countries require the registrant's identity details before the number is ordered. Before collecting anything, check availability for the country (checkWhatsAppNumberAvailability, GET /availability?country=XX). It returns whether there is deliverable inventory and an addressConstraint: geo means the registrant's address must be in one of the returned areas (a few countries stock numbers tied to a specific city), country means any in-country address works, none means no address is required. Checking first avoids the worst case: a submission that passes review but can never be assigned a number because the address is in the wrong area.
Then fetch the form for the country (getWhatsAppNumberKycForm): it returns the exact fields that country requires (text fields, an address, and document uploads such as an ID or business registration), plus whether the owner already has a reusable, approved verification for that country.
Then submit the collected values, documents, and address (submitWhatsAppNumberKyc). Each document can be sent inline (base64) or as a documentId from a prior upload. For an ID-card requirement, carriers usually need BOTH sides: combine the front and back into a single file before uploading (a one-sided ID is a common decline reason). The order goes to regulatory review and the number activates within 1-3 business days. Some countries (for example, Australia) also return a verification_required step: forward the verificationUrl to the end user to complete an ID check. Listen for whatsapp.number.activated / whatsapp.number.declined to know the outcome. If the regulator needs more information mid-review, whatsapp.number.action_required fires with the request, and the submitter is emailed; the missing details can be provided from the dashboard and the review resumes automatically.
# 1. Get the KYC form for the country
curl "https://zernio.com/api/v1/whatsapp/phone-numbers/kyc?country=GB&profileId=YOUR_PROFILE_ID" \
-H "Authorization: Bearer YOUR_API_KEY"
# 2. Submit the collected details (documents are base64-encoded)
curl -X POST "https://zernio.com/api/v1/whatsapp/phone-numbers/kyc" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"profileId": "YOUR_PROFILE_ID",
"country": "GB",
"values": { "REQUIREMENT_ID": "Acme Ltd | Jane Doe | +44..." },
"documents": [{ "requirementId": "REQUIREMENT_ID", "filename": "id.png", "base64": "..." }],
"address": {
"requirementId": "REQUIREMENT_ID", "country_code": "GB",
"street_address": "10 Downing Street", "locality": "London",
"administrative_area": "London", "postal_code": "SW1A 2AA"
}
}'The published SDKs don't yet expose dedicated getWhatsAppNumberKycForm / submitWhatsAppNumberKyc methods. Use raw HTTP (above) until the next SDK regen catches up. PII (ID documents, address) streams straight to our number provider in-request; Zernio never stores the document bytes.
Fix a Declined Number
If a regulated number is declined, you do not start over: fix only what the reviewer flagged and re-submit the same number. A declined number stays in the list under status: "pending_regulatory" with a regulatoryDeclineReason. Fetch the remediation form (getWhatsAppNumberRemediation, GET /{id}/remediate) — it returns ONLY the requirements that were declined — then re-submit the corrected values to the same endpoint (remediateWhatsAppNumber, POST /{id}/remediate). The number goes back to review on the same record; you keep its place in the queue.
# 1. Get just the declined requirements for the number
curl "https://zernio.com/api/v1/whatsapp/phone-numbers/PHONE_NUMBER_ID/remediate" \
-H "Authorization: Bearer YOUR_API_KEY"
# 2. Re-submit the corrected fields/documents (same shape as the KYC submit)
curl -X POST "https://zernio.com/api/v1/whatsapp/phone-numbers/PHONE_NUMBER_ID/remediate" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"values": { "REQUIREMENT_ID": "corrected value" },
"documents": [{ "requirementId": "REQUIREMENT_ID", "filename": "id.png", "base64": "..." }]
}'Verify a Number
After purchasing, verify the number with Meta by requesting an OTP and submitting it:
The published Node SDK (@zernio/node) and Python SDK (zernio-sdk) do not yet expose dedicated methods for requestWhatsAppVerificationCode / verifyWhatsAppPhoneNumber. Use raw HTTP requests until the next SDK regen catches up.
# Step 1: Request verification code
curl -X POST "https://zernio.com/api/v1/whatsapp/phone-numbers/PHONE_NUMBER_ID/request-code" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"method": "SMS"}'
# Step 2: Submit the code
curl -X POST "https://zernio.com/api/v1/whatsapp/phone-numbers/PHONE_NUMBER_ID/verify" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'