API reference
Verification API
Verify a single email address in real-time and get detailed results including deliverability status, risk flags, and enrichment data.
/v1/verifyRequest body
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | Required | Email address to verify |
include_activity | boolean | Optional | Include engagement activity data (default: false) |
options.deep_verify | boolean | Optional | Request extended checks (default: true). The current production workers attempt SMTP mailbox probing when deep verification is requested and the target mail server supports the check; otherwise Kawaa falls back to syntax, domain, MX, and AI risk analysis. Every option is a JSON boolean — a string such as "false" is rejected with 400 INVALID_OPTION. |
options.include_ai | boolean | Optional | Include AI-powered analysis (default: true) |
options.skip_cache | boolean | Optional | Force a fresh check instead of serving a recent cached or in-flight result (default: false). A cached result costs 0.5 credits and sets from_cache: true; a fresh one costs 1. An inconclusive result is never reused — catch_all, risky and unknown statuses, and temporary outcomes such as greylisted or temporary_failure, are checked again at the full 1 credit, because a stored non-answer is not a result. On an accept-all domain that is most of a list, though not all of it: an address ruled disposable or spam_trap is settled before the catch-all probe and is reused normally. Every fresh check replaces or invalidates the worker's option-scoped cache entry, so a later request cannot resurrect older evidence. Concurrent work that started earlier cannot overwrite a forced result if it finishes later. |
options.enrich | boolean | Optional | Return the enrichment block shown in the example response (default: false). Without it the response has no enrichment key. Requires the Business plan or higher — on a lower plan the whole request is rejected with 403 PLAN_UPGRADE_REQUIRED, so omit it unless your plan includes it. That code means no API key will work whatever its scopes; the fix is a plan change. |
options.timeout_ms | integer | Optional | How long, in milliseconds, this request waits for a fresh (cache-miss) check to finish before answering with status: "pending" (default: 24,000). Values are clamped to 5,000–60,000; a non-number is rejected with 400. It only bounds the wait on this endpoint — the check itself continues and GET /v1/jobs/{job_id} returns the result. Not accepted on the batch or file routes. |
Any other field, at the top level or inside options, is rejected with 400 BAD_REQUEST naming the supported fields, so a misspelled option can never be silently ignored and charged for.
Safe retries: the Idempotency-Key header
This endpoint charges before it answers, so a request that times out or fails with a 5xx on the way back may already have been charged. To retry it safely, send an optional Idempotency-Key header: any opaque token of 1–255 printable ASCII characters with no spaces (a UUID is fine), unique per logical request. Repeating the same request with the same key returns the job the first attempt created and charges nothing more.
curl -X POST https://api.kawaa.com/v1/verify \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Idempotency-Key: 6f1c2a4e-3b7d-4e0a-9c11-2d8f5a7b9e01" \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'- After dispatch, a repeat answers
200with"idempotent_replay": true, the receipt's currentjob_id,"credits_used": 0and the"status": "pending"shape — read the result or refund for that attempt fromGET /v1/jobs/{job_id}. The request claims its uncharged job first; the debit, credit-ledger entry and funded job state then commit together. If a caller stops between that commit and queueing, a repeat safely finishes the interrupted enqueue and returns the normal acceptance or result shape without a second charge. Once a failed attempt is fully refunded, the same key starts a newly charged attempt under a freshjob_id; the old failed job remains terminal (until the refund completes, a repeat returns that failed job uncharged). - A key identifies one request on this endpoint. Reusing it with a different address or options is refused with
409 IDEMPOTENCY_KEY_CONFLICTbefore anything is charged. A malformed key is refused with400 INVALID_IDEMPOTENCY_KEY. - Keys are scoped to your account and to this endpoint, and a repeat is answered with the receipt's current job throughout its retention window (at least 30 days); only the fully refunded rotation described above changes that current job. Two requests racing with the same key produce one charge and one job.
- Without the header the endpoint behaves exactly as before. The Node and Python SDKs send a key automatically on every
verifyand batch call from 0.2.0 and retry only requests that carry one; earlier releases and the other SDKs do not.
Example request
curl -X POST https://api.kawaa.com/v1/verify \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"include_activity": true,
"options": {
"deep_verify": true,
"include_ai": true
}
}'This request runs on every plan. To also receive the enrichment block, add "enrich": true inside options — that option requires the Business plan or higher, and sending it without one returns 403 instead of a verification result.
Response
Shown with every optional block populated. enrichment appears only when you send options.enrich on a Business-or-higher plan, activity only with include_activity, and ai only when AI analysis returns something to report.
{
"success": true,
"data": {
"job_id": "5e809a8a-1d1a-4c92-86f8-2b7395e5d803",
"email": "john@example.com",
"status": "risky",
"sub_status": "catch_all",
"quality_score": 72,
"verification": {
"syntax_valid": true,
"domain_exists": true,
"mx_found": true,
"mx_records": ["mx1.example.com", "mx2.example.com"],
"smtp_check": true,
"catch_all": true,
"greylisted": false,
"smtp_code": 250,
"smtp_message": "250 2.1.5 Recipient OK"
},
"flags": {
"disposable": false,
"role_account": false,
"free_provider": false,
"spam_trap": false,
"abuse_email": false,
"honeypot": false,
"complainer": false,
"bot_generated": false
},
"domain_info": {
"domain": "example.com",
"provider_type": "corporate",
"auth_score": 84,
"auth_grade": "B",
"has_spf": true,
"spf_policy": "SoftFail",
"has_dmarc": true,
"dmarc_policy": "Quarantine",
"has_dkim": true
},
"enrichment": {
"first_name": "John",
"full_name": "John",
"gender": "male",
"country": "United States",
"country_code": "US"
},
"ai": {
"typo_suggestion": null,
"typo_confidence": null,
"catch_all_confidence": 15
},
"activity": {
"level": "active",
"score": 78,
"last_activity_at": "2026-01-15T10:30:00Z",
"engagement": { "open_count": 12, "click_count": 5, "reply_count": 1 },
"deliverability": { "bounce_count": null, "complaint_count": null },
"confidence": 80,
"data_points": 18
},
"credits_used": 1,
"credits_remaining": 4998,
"from_cache": false,
"processing_time_ms": 1345,
"verified_at": "2026-02-03T14:22:15.123Z",
"request_id": "2a4104cd-ef74-4412-89e2-a4859a72bd11"
}
}Response fields
Every field a completed result can carry. A field marked as omitted is absent from the JSON rather than null, so read it with a default. Fields are added without a version change; unknown ones are informational.
Top-level fields
| Field | Type | Description |
|---|---|---|
job_id | string | Identifier of this check. GET /v1/jobs/{job_id} returns the same result together with its billing status. |
email | string | The address that was verified. |
status | string | The verdict. See Status values below. |
sub_status | string | The specific reason behind status. See Sub-status below. Omitted when there is nothing more specific to say. |
quality_score | integer 0–100 | Overall quality. See Quality score below. |
verification | object | The individual checks that produced the verdict. Fields below. |
flags | object | Risk signals about the address. Fields below. |
domain_info | object | The domain’s provider type and email-authentication posture. Fields below. Omitted when the domain could not be looked up. |
ai | object | AI analysis: typo_suggestion and typo_confidence when the address looks like a typo of a known domain, catch_all_confidence on a catch-all domain. Present only when there is something to report. |
enrichment | object | first_name, last_name, full_name, gender, country and country_code where they can be inferred. Present only when options.enrich was sent on a plan that includes it. |
activity | object | Engagement activity for the address. Present only when include_activity was sent; see the Activity API. |
domain_reputation | object | Reputation of the domain across Kawaa’s activity data: reputation_score, reputation_grade and engagement metrics, as GET /v1/activity/domain/{domain} returns them. Present only when include_activity was sent and there is data for the domain. |
credits_used | number | What this request cost: 1 for a fresh check, 0.5 for a cached result. |
credits_remaining | number | Your credit balance after this request. |
from_cache | boolean | true when the result was served from a recent check of the same address rather than a fresh one. Send options.skip_cache to force a fresh check. |
cache_age_seconds | integer | How old the cached result is. Present only when from_cache is true. |
processing_time_ms | integer | Time the verification worker spent on the check, in milliseconds. |
verified_at | string (ISO 8601) | When evidence collection began. For a cached result this is the start time of the original check, not of this request. |
classifier_version | integer | Version of the classifier that produced the verdict. When the classifier changes, cached results from an older version are never reused — the address is verified afresh. |
request_id | string | Identifier of this API request. Quote it when contacting support. |
verification
| Field | Type | Description |
|---|---|---|
syntax_valid | boolean | The address is syntactically valid. |
domain_exists | boolean | The domain has DNS records. |
mx_found | boolean | The domain publishes MX records, so it can receive mail. |
mx_records | string[] | The MX hostnames found. Omitted when there are none. |
smtp_check | boolean | Whether the SMTP mailbox probe succeeded. Omitted when no probe was attempted (for example a domain with no MX records). |
mailbox_exists | boolean | The mail server accepted the mailbox (RCPT TO). Omitted when the probe did not reach that point. |
catch_all | boolean | The domain accepts mail for any address, so an accepted mailbox proves nothing on its own. |
provider_accept_all | boolean | The provider accepts every address at the provider level, which makes the mailbox probe inconclusive. Present only when true. |
greylisted | boolean | The mail server deferred the probe with a temporary rejection. |
smtp_code | integer | The mail server’s last SMTP reply code. Omitted when no probe ran. |
smtp_message | string | The mail server’s last SMTP reply. Omitted when no probe ran. |
flags
| Field | Type | Description |
|---|---|---|
disposable | boolean | The domain is a disposable or temporary mail provider. |
role_account | boolean | A role address such as info@ or support@ rather than a person’s. |
free_provider | boolean | A free consumer provider such as Gmail or Yahoo. |
spam_trap | boolean | A known spam trap. Do not send. |
abuse_email | boolean | The address or domain is associated with abuse reports. |
honeypot | boolean | The address appears to be a honeypot set up to catch senders. |
complainer | boolean | The address is known to mark mail as spam. |
bot_generated | boolean | The address looks machine-generated. |
domain_info
| Field | Type | Description |
|---|---|---|
domain | string | The domain part of the address. |
provider_type | string | One of free, corporate, educational, government or unknown. |
auth_score | integer 0–100 | How completely the domain has set up SPF, DKIM and DMARC. Omitted when the lookup failed. |
auth_grade | string | auth_score as a letter: A (90–100), B (80–89), C (70–79), D (50–69) or F. Omitted when the lookup failed. |
has_spf | boolean | The domain publishes an SPF record. |
spf_policy | string | What the SPF record says about senders it does not list: Pass (+all), Fail (-all), SoftFail (~all) or Neutral (?all). Omitted when there is no SPF record. |
has_dmarc | boolean | The domain publishes a DMARC record. |
dmarc_policy | string | The DMARC policy: None, Quarantine or Reject. Omitted when there is no DMARC record. |
has_dkim | boolean | A DKIM key was found at a common selector. |
Status values
| Status | Description |
|---|---|
| valid | SMTP accepted the address and no catch-all/provider-limited caveat was detected during this run |
| invalid | No valid mail server or SMTP rejected the mailbox |
| risky | May receive mail, but risk signals or incomplete proof remain |
| unknown | Could not determine deliverability |
| catch_all | Domain accepts all emails - individual mailbox unverifiable |
| disposable | Temporary email address |
| role | Role-based email (info@, support@) |
| spam_trap | Known spam trap - do not send |
| pending | No result yet. The check is still running, so the response carries job_id and credits_held instead of a result. Poll GET /v1/jobs/{job_id}. |
Sub-status
sub_status is the specific reason behind status — one of mailbox_exists, mailbox_reject, catch_all, provider_accept_all, policy_block, greylisted, temporary_failure, connection_failure, dns_only, no_mx, domain_not_found or syntax_error. Branch on status for the decision and read sub_status for the explanation; new sub-status values may be added without a version change, so treat unknown ones as informational.
Pending responses
A cache-miss verification that has not finished within the request window returns 200 with status: "pending" rather than holding the connection open. There is no quality_score or result block on this response — treat it as "ask again shortly", not as a verdict:
{
"success": true,
"data": {
"job_id": "8f14e45f-ceea-467a-9a1f-2b0c8f9c4a11",
"email": "user@example.com",
"status": "pending",
"message": "Verification is still in progress. 1 credit is held while processing; if the worker fails before producing a result, job reconciliation refunds it. Use GET /v1/jobs/{job_id} to check status.",
"credits_held": 1
}
}Poll GET /v1/jobs/{job_id} until it reports a terminal status. The held credit is refunded by job reconciliation if the check never produces a result, so a pending response is not a charge for nothing.
Quality score
The quality_score is a 0-100 score indicating overall email quality:
- 90-100: Excellent - lowest observed risk
- 70-89: Good - Generally safe
- 50-69: Fair - Review recommended
- 0-49: Poor - High risk of issues
For catch-all domains, policy blocks, and provider-limited responses, see Understanding results & provider limits.
Credits
- 1 credit per verification (cache miss)
- 0.5 credits for cached results (within 24 hours)
- Async cache-miss checks hold 1 credit while processing
- Held credits are refunded by job reconciliation if the worker fails before producing a result
- Activity data included at no extra cost when available