API Documentation
You send a phone number. We tell you if it has a WhatsApp account, plus what we know about the number. You can check an email too — alongside the number, or on its own.
Start here
Three steps to your first result:
- Create a free account.
- Make an API key in your dashboard.
- Send your first request.
Every request needs an API key. There is no way to use the API without one.
Base URL
Send all requests to this address:
https://api.waverify.appAdd the path to the end. For example, the check endpoint is https://api.waverify.app/api/v1/check.
Always use https. Plain http does not work.
Your API key
Send your key in a header called X-API-Key:
X-API-Key: wav_live_xxxxxxxxxxxxxxxxxxxxxxxxYou see the full key only once — when you create it. We do not keep a copy, so we cannot show it to you again. Save it somewhere safe straight away.
Keep the key in an environment variable. Do not write it directly in your code, and do not upload it to GitHub. If someone else gets your key, delete it in the dashboard and make a new one.
If you are signed in to the dashboard, your session uses a short-lived Bearer token instead of a key. Both work on the same endpoint. We record which one you used for each request.
Check a number
POST/api/v1/check
Send one phone number. Write it in international format: a + sign, then the country code, then the number.
Spaces, dashes and brackets are fine — we remove them for you. A number without a country code is rejected. We never guess the country.
What you send
| Field | Type | Description |
|---|---|---|
| phone | string | Required. International format, for example +8801712345678. |
| string | Optional. If you know an email for this person, send it and we check whether it is a real, working address — and look it up on Gravatar. Leave it out and nothing changes. |
Example request
curl -X POST https://api.waverify.app/api/v1/check \
-H "Content-Type: application/json" \
-H "X-API-Key: wav_live_your_api_key" \
-d '{"phone": "+8801712345678"}'With an email
The email is extra. It never changes the WhatsApp part of the answer. A bad email does not fail the request either: you still get status 200, with the problem described in email_info. See Email lookup below.
curl -X POST https://api.waverify.app/api/v1/check \
-H "Content-Type: application/json" \
-H "X-API-Key: wav_live_your_api_key" \
-d '{"phone": "+8801712345678", "email": "someone@example.com"}'What you get back
The number is on WhatsApp
{
"success": true,
"phone": "+8801712345678",
"exists": true,
"display_name": "Acme Store",
"name_source": "business_verified",
"about": "Open 9-6, Sat closed",
"business": true,
"profile_photo": "https://cdn.waverify.app/p/9f2c.jpg",
"profile_photo_id": "1721728451",
"device_count": 2,
"number_info": {
"country_code": "+880",
"region": "BD",
"location": "Bangladesh",
"carrier": "Grameenphone",
"line_type": "mobile",
"timezones": ["Asia/Dhaka"],
"international_format": "+880 1712-345678",
"national_format": "01712-345678"
},
"email_info": null,
"gravatar": null,
"response_time_ms": 214,
"cached": false,
"checked_at": "2026-07-23T10:04:11Z"
}The number is not on WhatsApp
This is still a successful request. You get status 200 with exists: false, and it uses one request from your monthly total.
{
"success": true,
"phone": "+14155552671",
"exists": false,
"display_name": null,
"name_source": null,
"about": null,
"business": false,
"profile_photo": null,
"profile_photo_id": null,
"device_count": null,
"number_info": {
"country_code": "+1",
"region": "US",
"location": "San Francisco, CA",
"carrier": null,
"line_type": "fixed_line_or_mobile",
"timezones": ["America/Los_Angeles"],
"international_format": "+1 415-555-2671",
"national_format": "(415) 555-2671"
},
"email_info": null,
"gravatar": null,
"response_time_ms": 187,
"cached": false,
"checked_at": "2026-07-23T10:04:12Z"
}Fields in the answer
| Field | Type | Description |
|---|---|---|
| success | boolean | Always true when the request worked. |
| phone | string | The number you sent, rewritten in the standard international format. |
| exists | boolean | true if this number has a WhatsApp account. |
| display_name | string | null | The name on the account. Business accounts have one. Personal accounts are usually null — WhatsApp does not give a stranger the name. This is normal, not an error. |
| name_source | string | null | Where the name came from: business_verified, business_name or contact_name. null when there is no name. |
| about | string | null | The short “about” text on the account. null if the account hides it. |
| business | boolean | true if this is a WhatsApp Business account. |
| profile_photo | string | null | Link to the profile picture. null if the account hides it. The link stops working after a few hours, so download the picture if you need to keep it. |
| profile_photo_id | string | null | An id for the current picture. It changes when the account changes its picture, so you can save it and see when the picture is new. |
| device_count | integer | null | How many devices the account uses. 1 means the phone only. More means WhatsApp Web or Desktop is also linked. null if we could not tell. |
| number_info | object | Facts about the number itself. Always there, even when the number has no WhatsApp account. See “Number details”. |
| email_info | object | null | Our verdict on the email you sent: whether it is a real address, and whether its domain can receive mail. null if you sent no email. See “Email lookup”. |
| gravatar | object | null | The public Gravatar profile for the email you sent. null if you sent no email, the address was malformed, or it has no profile. See “Email lookup”. |
| response_time_ms | integer | How long the check took on our server, in milliseconds. |
| cached | boolean | true if we answered from a saved recent result instead of checking again. |
| checked_at | string | Date and time of the check, in UTC. |
Names, and why they are missing
Business accounts give you a real name. Personal accounts almost never do: WhatsApp does not show a personal name to someone who is not a contact. So display_name is usually null for a normal person.
This is not an error. Do not show it to your users as one. Use name_source to explain what you did get.
about and profile_photo do come back for personal accounts, as long as the person kept the normal privacy setting. They are null if the person hid them.
Profile pictures expire
profile_photo is a WhatsApp link that stops working after a few hours. Do not save the link. If you need the picture, download it and keep your own copy.
To spot a new picture without downloading anything, save profile_photo_id and compare it next time.
Number details
Every answer has a number_info object. It comes from the number itself, not from WhatsApp, so it is filled in even when exists is false.
| Field | Type | Description |
|---|---|---|
| country_code | string | null | The country calling code, for example +880. |
| region | string | null | Two-letter country code, for example BD. |
| location | string | null | The country, or the city when we know it. |
| carrier | string | null | The mobile network. null for landlines and for numbers that moved to another network. |
| line_type | string | The kind of line. Always has a value — unknown when we cannot tell. |
| timezones | string[] | Time zones for this number, for example ["Asia/Dhaka"]. Can be empty. |
| international_format | string | null | The number written for use from any country. |
| national_format | string | null | The number written the way people write it at home. |
Values for line_type
mobilefixed_linefixed_line_or_mobiletoll_freepremium_rateshared_costpersonal_numberpageruanvoicemailunknownEmail lookup
Send an email and you get two things back: email_info, our verdict on the address itself, and gravatar, the public profile behind it.
Is the email real?
We check two things without ever sending a message: the address is written correctly, and its domain publishes somewhere to deliver mail.
That is as far as anyone can honestly go. The only way to prove one exact mailbox exists is to try to send to it, which gets the sender blocked, so we do not do it. Read deliverable as “this domain accepts mail”, not “this person exists”.
{
"email": "jane@acme.com",
"syntax_valid": true,
"domain": "acme.com",
"deliverable": true,
"mx_hosts": ["mx.acme.com"],
"disposable": false,
"role_account": false,
"free_provider": false,
"status": "valid",
"reason": null
}A bad email is not an error
You still get status 200, and the WhatsApp part of the answer is unaffected. The problem is described in email_info instead.
{
"email": "jane@@acme",
"syntax_valid": false,
"domain": "acme",
"deliverable": null,
"mx_hosts": [],
"disposable": false,
"role_account": false,
"free_provider": false,
"status": "invalid_syntax",
"reason": "The part after the @-sign contains invalid characters: '@'."
}deliverable has three values
true and false mean what you expect. null means we could not find out — usually the domain lookup timed out.
null is not false. If your code does if (!deliverable) it will call good addresses bad whenever a lookup is slow. Check the three cases separately.
| Field | Type | Description |
|---|---|---|
| string | The address you sent, lowercased and tidied up. | |
| syntax_valid | boolean | true if the address is written correctly and could exist. |
| domain | string | null | The part after the @. Treat it as a guess when syntax_valid is false. |
| deliverable | boolean | null | true if the domain has a mail server, false if it does not. null means we could not find out — that is not the same as false, so do not treat it as a bad address. |
| mx_hosts | string[] | The domain's mail servers, best first. Empty when there are none. |
| disposable | boolean | true for a known throwaway-inbox service. These do receive mail, which is exactly why it is worth knowing. |
| role_account | boolean | true for a shared mailbox like info@ or support@. Real, but no one person behind it. |
| free_provider | boolean | true for a consumer provider such as Gmail. Normal for a personal address. |
| status | string | One word for the whole verdict: valid, invalid_syntax, domain_not_found, no_mail_server, disposable, or unknown. |
| reason | string | null | A sentence explaining status. null when the address is fine. |
Values for status
validinvalid_syntaxdomain_not_foundno_mail_serverdisposableunknowndisposable wins over valid when both apply. Those addresses do receive mail — that is exactly why they are worth flagging.
Checking an email on its own
If the email is all you have, post it to /check/email. You get the same email_info, plus any Gravatar profile — with no phone number involved.
Two things make this route different. It never contacts WhatsApp, so it keeps working even when a number check cannot. And it does not use up any of your monthly requests — only the per-minute rate limit applies.
curl -X POST https://api.waverify.app/api/v1/check/email \
-H "Content-Type: application/json" \
-H "X-API-Key: wav_live_your_api_key" \
-d '{"email": "someone@example.com"}'{
"success": true,
"email": "jane@acme.com",
"email_info": {
"email": "jane@acme.com",
"syntax_valid": true,
"domain": "acme.com",
"deliverable": true,
"mx_hosts": ["mx.acme.com"],
"disposable": false,
"role_account": false,
"free_provider": false,
"status": "valid",
"reason": null
},
"gravatar": null,
"response_time_ms": 96,
"cached": false,
"checked_at": "2026-07-29T10:04:11Z"
}Gravatar profile
We also look the address up on Gravatar and put any public profile in gravatar.
Most emails have no Gravatar profile, so expect null often. It is also null when you sent no email, or when the address was too malformed to look up.
{
"display_name": "Jane Roe",
"about": "Product designer.",
"location": "Dhaka, Bangladesh",
"job_title": "Designer",
"company": "Acme",
"pronouns": null,
"avatar_url": "https://gravatar.com/avatar/…",
"profile_url": "https://gravatar.com/janeroe",
"verified_accounts": [
{ "service": "Twitter", "url": "https://twitter.com/janeroe" },
{ "service": "LinkedIn", "url": "https://linkedin.com/in/janeroe" }
]
}Every field inside can be null — people fill in only what they want to.
| Field | Type | Description |
|---|---|---|
| display_name | string | null | The name on the Gravatar profile. |
| about | string | null | The short text the person wrote about themselves. |
| location | string | null | Where the person says they are. |
| job_title | string | null | Their job title. |
| company | string | null | Where they work. |
| pronouns | string | null | The pronouns they list. |
| avatar_url | string | null | Link to their picture. |
| profile_url | string | null | Link to the full Gravatar profile. |
| verified_accounts | array | Other accounts they have proven are theirs. Each item has service and url. Can be empty. |
Errors
Every error looks the same. In your code, check error.code. Do not check the message text — the wording can change, but the code will not.
{
"success": false,
"error": {
"code": "quota_exceeded",
"message": "You have used all 100 requests included in the Free plan this period.",
"details": {}
}
}List of error codes
| Status | Code | What it means |
|---|---|---|
| 401 | authentication_failed | Your API key is missing, wrong, or was deleted. |
| 402 | quota_exceeded | You have used all the requests in your plan this month. |
| 403 | email_not_verified | You have not confirmed your email address yet. |
| 422 | validation_error | The phone number is missing, or it is not a real number. |
| 429 | rate_limit_exceeded | You sent too many requests in one minute. Wait, then retry. |
| 502 | provider_error | Our WhatsApp connection is down right now. Please try again soon. |
Limits
There are two separate limits. Please plan for both.
1. Requests per minute
Free: 10. Starter: 60. Pro: 300. Enterprise: your own limit.
If you go over, you get status 429 with the code rate_limit_exceeded. Wait a short time, then send the request again.
2. Requests per month
Each plan includes a number of requests per month. When you use them all, you get status 402 with the code quota_exceeded. You can upgrade your plan at any time.
If you check the same number twice in a short time, the second answer comes from our saved results and is marked cached: true. It is faster, but it still counts towards your monthly total.
Need help?
This page is the full reference. There is one endpoint, and everything it can return is listed above.
If something does not work the way this page describes, email support@waverify.app. Please include the error.code you received and the time of the request — that is enough for us to find it in our logs.