Skip to main content

API reference

Verification API

Verify a single email address in real-time and get detailed results including deliverability status, risk flags, and enrichment data.

POST/v1/verify

Request body

ParameterTypeRequiredDescription
emailstringRequiredEmail address to verify
include_activitybooleanOptionalInclude engagement activity data (default: false)
options.deep_verifybooleanOptionalRequest 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_aibooleanOptionalInclude AI-powered analysis (default: true)
options.skip_cachebooleanOptionalForce 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.enrichbooleanOptionalReturn 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_msintegerOptionalHow 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
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 200 with "idempotent_replay": true, the receipt's current job_id, "credits_used": 0 and the "status": "pending" shape — read the result or refund for that attempt from GET /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 fresh job_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_CONFLICT before anything is charged. A malformed key is refused with 400 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 verify and batch call from 0.2.0 and retry only requests that carry one; earlier releases and the other SDKs do not.

Example request

cURL
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.

200 OK
{
  "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

FieldTypeDescription
job_idstringIdentifier of this check. GET /v1/jobs/{job_id} returns the same result together with its billing status.
emailstringThe address that was verified.
statusstringThe verdict. See Status values below.
sub_statusstringThe specific reason behind status. See Sub-status below. Omitted when there is nothing more specific to say.
quality_scoreinteger 0–100Overall quality. See Quality score below.
verificationobjectThe individual checks that produced the verdict. Fields below.
flagsobjectRisk signals about the address. Fields below.
domain_infoobjectThe domain’s provider type and email-authentication posture. Fields below. Omitted when the domain could not be looked up.
aiobjectAI 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.
enrichmentobjectfirst_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.
activityobjectEngagement activity for the address. Present only when include_activity was sent; see the Activity API.
domain_reputationobjectReputation 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_usednumberWhat this request cost: 1 for a fresh check, 0.5 for a cached result.
credits_remainingnumberYour credit balance after this request.
from_cachebooleantrue 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_secondsintegerHow old the cached result is. Present only when from_cache is true.
processing_time_msintegerTime the verification worker spent on the check, in milliseconds.
verified_atstring (ISO 8601)When evidence collection began. For a cached result this is the start time of the original check, not of this request.
classifier_versionintegerVersion 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_idstringIdentifier of this API request. Quote it when contacting support.

verification

FieldTypeDescription
syntax_validbooleanThe address is syntactically valid.
domain_existsbooleanThe domain has DNS records.
mx_foundbooleanThe domain publishes MX records, so it can receive mail.
mx_recordsstring[]The MX hostnames found. Omitted when there are none.
smtp_checkbooleanWhether the SMTP mailbox probe succeeded. Omitted when no probe was attempted (for example a domain with no MX records).
mailbox_existsbooleanThe mail server accepted the mailbox (RCPT TO). Omitted when the probe did not reach that point.
catch_allbooleanThe domain accepts mail for any address, so an accepted mailbox proves nothing on its own.
provider_accept_allbooleanThe provider accepts every address at the provider level, which makes the mailbox probe inconclusive. Present only when true.
greylistedbooleanThe mail server deferred the probe with a temporary rejection.
smtp_codeintegerThe mail server’s last SMTP reply code. Omitted when no probe ran.
smtp_messagestringThe mail server’s last SMTP reply. Omitted when no probe ran.

flags

FieldTypeDescription
disposablebooleanThe domain is a disposable or temporary mail provider.
role_accountbooleanA role address such as info@ or support@ rather than a person’s.
free_providerbooleanA free consumer provider such as Gmail or Yahoo.
spam_trapbooleanA known spam trap. Do not send.
abuse_emailbooleanThe address or domain is associated with abuse reports.
honeypotbooleanThe address appears to be a honeypot set up to catch senders.
complainerbooleanThe address is known to mark mail as spam.
bot_generatedbooleanThe address looks machine-generated.

domain_info

FieldTypeDescription
domainstringThe domain part of the address.
provider_typestringOne of free, corporate, educational, government or unknown.
auth_scoreinteger 0–100How completely the domain has set up SPF, DKIM and DMARC. Omitted when the lookup failed.
auth_gradestringauth_score as a letter: A (90–100), B (80–89), C (70–79), D (50–69) or F. Omitted when the lookup failed.
has_spfbooleanThe domain publishes an SPF record.
spf_policystringWhat 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_dmarcbooleanThe domain publishes a DMARC record.
dmarc_policystringThe DMARC policy: None, Quarantine or Reject. Omitted when there is no DMARC record.
has_dkimbooleanA DKIM key was found at a common selector.

Status values

StatusDescription
validSMTP accepted the address and no catch-all/provider-limited caveat was detected during this run
invalidNo valid mail server or SMTP rejected the mailbox
riskyMay receive mail, but risk signals or incomplete proof remain
unknownCould not determine deliverability
catch_allDomain accepts all emails - individual mailbox unverifiable
disposableTemporary email address
roleRole-based email (info@, support@)
spam_trapKnown spam trap - do not send
pendingNo 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:

200 OK · pending
{
  "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