Get started
Quick start
Make your first API request, understand the result, and handle a check that needs more time.
Prerequisites
- A Kawaa account (sign up free)
- An API key from your Dashboard
Get your API key
Navigate to Settings → API keys in your dashboard and create a new API key. Copy it somewhere safe.
ev_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxVerify an email
Make a POST request to the
/v1/verifyendpoint. Each verification uses 1 credit, or 0.5 when the result comes from cache. First, generate and export one idempotency key in your terminal for this operation:Create an operation key onceKAWAA_REQUEST_KEY=$(python3 -c 'import uuid; print(uuid.uuid4())') || exit 1 test -n "$KAWAA_REQUEST_KEY" || exit 1 export KAWAA_REQUEST_KEYRun one of the examples below from that terminal. Rerun the example with the same key and body if the response is lost; do not rerun key generation. Save the key with your request if you close the terminal. Generate a new key for each new verification operation.
# Refuse to send until the operation key is set in this terminal. : "${KAWAA_REQUEST_KEY:?Set KAWAA_REQUEST_KEY before running this request}" curl --fail-with-body --max-time 65 -X POST https://api.kawaa.com/v1/verify \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Idempotency-Key: $KAWAA_REQUEST_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "test@example.com"}'// Reusing the environment value also preserves the key across script reruns. const requestKey = process.env.KAWAA_REQUEST_KEY; if (!requestKey) throw new Error('Set KAWAA_REQUEST_KEY before running this request'); const response = await fetch('https://api.kawaa.com/v1/verify', { method: 'POST', headers: { 'X-Api-Key': 'YOUR_API_KEY', 'Idempotency-Key': requestKey, 'Content-Type': 'application/json', }, body: JSON.stringify({ email: 'test@example.com' }), signal: AbortSignal.timeout(65_000), }); if (!response.ok) { throw new Error(`Verification request failed: HTTP ${response.status}`); } const result = await response.json(); console.log(result);import os import requests # Refuse to send without the operation key; reuse it on script reruns. request_key = os.environ['KAWAA_REQUEST_KEY'] if not request_key: raise ValueError('Set KAWAA_REQUEST_KEY before running this request') response = requests.post( 'https://api.kawaa.com/v1/verify', headers={ 'X-Api-Key': 'YOUR_API_KEY', 'Idempotency-Key': request_key, }, json={'email': 'test@example.com'}, timeout=(5, 65), ) response.raise_for_status() print(response.json())These examples use cURL 7.76 or later with Python 3 for key generation, Node.js 20 or later, or Python with the
requestspackage. Save the Node.js example as a.mjsfile. Keep API keys on your server, outside browser code.Understand the response
This is what the request above normally returns (trimmed).
example.comis reserved for documentation and accepts no mail, so it comes backinvalidwith the reasonno_mx, or, if a DNS lookup fails at that moment,unknownwithtemporary_failure. Then try an address you own. Its result may still be uncertain if the provider limits verification.Response{ "success": true, "data": { "email": "test@example.com", "status": "invalid", "sub_status": "no_mx", "quality_score": 10, "verification": { "syntax_valid": true, "domain_exists": true, "mx_found": false, "greylisted": false }, "flags": { "disposable": false, "role_account": false, "free_provider": false, "spam_trap": false, "abuse_email": false, "honeypot": false, "complainer": false, "bot_generated": false } } }Decide on
status, and readsub_statusfor the reason.
Pending checks and safe retries
A successful HTTP 200 response can contain data.status: "pending". The check is still running. Use its job_id with GET /v1/jobs/{job_id} to read progress and the result, rather than starting another verification. Polling the job does not spend another verification credit.
If a response is lost, reuse the same key and request body. For a retryable queueing conflict or a rate limit, wait for Retry-After before retrying. Do not generate a new key on every retry. A completed unknown result is different from a pending check: a new check is charged under the current policy, even if it is still inconclusive.
A fully refunded failed attempt can start a newly charged attempt under the same key. See the retry and refund rules and the job response reference.
Status values
Handle all eight. An address that is a catch-all, disposable, a role account or a spam trap is reported under its own status, not folded into risky. A job's summary is different: it counts four buckets, so there catch-all, role and spam-trap addresses count toward risky and disposable ones toward invalid.
| Status | Description | Action |
|---|---|---|
| valid | SMTP accepted the address and no catch-all/provider-limited caveat was detected during this run | Low risk |
| invalid | Malformed, no such domain, no mail server, or SMTP rejected the mailbox | Do not send |
| risky | Mailbox not confirmed and something is against it: the server blocked the check, the mailbox is full, the catch-all test could not finish, or only DNS was checked | Read sub_status; use with caution |
| unknown | No verdict: no mail server could be reached or the connection dropped, the server greylisted or deferred the check, it ran out of time, a DNS lookup failed transiently, or the mailbox check was skipped | Retry later |
| catch_all | The domain or its provider accepts mail for every address, so this mailbox cannot be proven | Keep only with other evidence |
| disposable | A throwaway-mail provider | Block at signup |
| role | A shared address such as info@ or support@ | Leave out of marketing |
| spam_trap | At a domain on the reviewed trap-only list, which is empty today | Do not send |
Learn how provider limits affect result interpretation in Understanding results & provider limits.