Skip to main content

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
  1. Get your API key

    Navigate to Settings → API keys in your dashboard and create a new API key. Copy it somewhere safe.

    ev_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  2. Verify an email

    Make a POST request to the /v1/verify endpoint. 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 once
    KAWAA_REQUEST_KEY=$(python3 -c 'import uuid; print(uuid.uuid4())') || exit 1
    test -n "$KAWAA_REQUEST_KEY" || exit 1
    export KAWAA_REQUEST_KEY

    Run 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"}'

    These examples use cURL 7.76 or later with Python 3 for key generation, Node.js 20 or later, or Python with the requests package. Save the Node.js example as a .mjs file. Keep API keys on your server, outside browser code.

  3. Understand the response

    This is what the request above normally returns (trimmed). example.com is reserved for documentation and accepts no mail, so it comes back invalid with the reason no_mx, or, if a DNS lookup fails at that moment, unknown with temporary_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 read sub_status for 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.

StatusDescriptionAction
validSMTP accepted the address and no catch-all/provider-limited caveat was detected during this runLow risk
invalidMalformed, no such domain, no mail server, or SMTP rejected the mailboxDo not send
riskyMailbox 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 checkedRead sub_status; use with caution
unknownNo 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 skippedRetry later
catch_allThe domain or its provider accepts mail for every address, so this mailbox cannot be provenKeep only with other evidence
disposableA throwaway-mail providerBlock at signup
roleA shared address such as info@ or support@Leave out of marketing
spam_trapAt a domain on the reviewed trap-only list, which is empty todayDo not send

Learn how provider limits affect result interpretation in Understanding results & provider limits.

Next steps