Skip to main content

API reference

Batch & file verification

Verify large lists of emails asynchronously. Submit batches or upload CSV/TXT files. The per-batch cap is plan-dependent: 100 emails on Free, up to 10,000 on Enterprise.

Batch verification

POST/v1/verify/batch

Submit an array of emails for asynchronous verification. Returns a job ID to track progress.

Request body

ParameterTypeRequiredDescription
emailsstring[]RequiredArray of email addresses. Plan-dependent cap: 100 (Free), 1,000 (Starter), 5,000 (Professional/Business), 10,000 (Enterprise)
optionsobjectOptionalVerification options (e.g. deep_verify, include_ai)
webhook_urlstringOptionalHTTPS URL to receive the completion notification
webhook_secretstringOptionalHMAC key used to sign the completion webhook (X-Kawaa-Signature). Requires webhook_url; max 256 characters
include_activitybooleanOptionalInclude activity data in per-email results when available

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

A batch is charged in full before the job is queued, so a request that times out or fails with a 5xx on the way back may already have been charged. Send an optional Idempotency-Key header (any opaque token of 1–255 printable ASCII characters with no spaces, unique per logical request). After dispatch, a repeat of the same list with the same key answers 202 with "idempotent_replay": true, the receipt's current job_id and "credits_used": 0 — nothing more is charged while that attempt remains active or its refund is pending. 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 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, and until the refund completes a repeat returns it uncharged. A key identifies one batch: reusing it with a different list, options or webhook is refused with 409 IDEMPOTENCY_KEY_CONFLICT before anything is charged. 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. The Node and Python SDKs send one automatically from 0.2.0; earlier releases and the other SDKs do not.

Example request

cURL
curl -X POST https://api.kawaa.com/v1/verify/batch \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "emails": [
      "user1@example.com",
      "user2@example.com",
      "user3@example.com"
    ],
    "webhook_url": "https://your-app.com/webhook",
    "webhook_secret": "your-signing-secret"
  }'

Response (202 Accepted)

202 Accepted
HTTP/1.1 202 Accepted
{
  "success": true,
  "data": {
    "job_id": "9f8f6d2e-4c1b-4f6e-9a3b-2d1e5c7a8b90",
    "status": "processing",
    "total_submitted": 3,
    "duplicates_removed": 0,
    "invalid_emails_skipped": 0,
    "unique_emails": 3,
    "estimated_time_seconds": 6,
    "credits_used": 3,
    "credits_remaining": 4997,
    "message": "Batch verification started. Use GET /v1/jobs/{job_id} to check status."
  }
}

File upload

POST/v1/verify/file

Upload a CSV or TXT file containing email addresses for verification. File verification is a two-step JSON flow: request a presigned upload URL, PUT your file to it, then start processing with the returned file_key.

Step 1: Request an upload URL

cURL
curl -X POST https://api.kawaa.com/v1/verify/file \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "get_upload_url",
    "filename": "emails.csv"
  }'

# Response
{
  "success": true,
  "data": {
    "upload_url": "https://kawaa-uploads.s3.amazonaws.com/uploads/...",
    "file_key": "uploads/user_abc123/2c9c1f8e-.../emails.csv",
    "expires_in_seconds": 900,
    "next_step": "Upload file to upload_url using PUT, then call this endpoint with action=process and file_key"
  }
}

Step 2: Upload the file

cURL
curl -X PUT "UPLOAD_URL_FROM_STEP_1" \
  --data-binary @emails.csv

Step 3: Start processing

cURL
curl -X POST https://api.kawaa.com/v1/verify/file \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Idempotency-Key: 6f1c2a4e-3b7d-4e0a-9c11-2d8f5a7b9e01" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "process",
    "file_key": "uploads/user_abc123/2c9c1f8e-.../emails.csv",
    "webhook_url": "https://your-app.com/webhook"
  }'

# Response (202 Accepted)
{
  "success": true,
  "data": {
    "job_id": "9f8f6d2e-4c1b-4f6e-9a3b-2d1e5c7a8b90",
    "status": "processing",
    "total_submitted": 1000,
    "duplicates_removed": 12,
    "invalid_emails_skipped": 3,
    "unique_emails": 985,
    "credits_used": 985,
    "credits_remaining": 4015,
    "message": "File verification started. 12 duplicate(s) removed. Use GET /v1/jobs/{job_id} to check status."
  }
}

Safe file retries

Keep the same Idempotency-Key when retrying the process request. If the original invocation is still dispatching the file, the retry returns 202, dispatch_in_progress: true, the original job_id, and a Retry-After header without reading the upload again or charging again. This remains an in-progress receipt even if the object at that file_key was overwritten after dispatch began. After the dispatch lease expires, recovery reads the upload and refuses changed contents with 409 IDEMPOTENCY_KEY_CONFLICT rather than sending different work under the original charge. If reconciliation later finalizes only part of the file, a replay reports the completed unique_emails and credits_refunded counts instead of claiming the whole parsed file was accepted.

Supported formats & limits

CSV:
Comma-, semicolon- or tab-separated (detected from the first line) with optional headers; the first column containing an @ is used
TXT:
One email per line
Max file size:
50MB (all plans)
Max emails per file:
your plan's batch cap — 100 (Free) up to 10,000 (Enterprise)
Upload URL expiry:
15 minutes

Check job status

GET/v1/jobs/{job_id}

Query parameters

ParameterDescription
include_resultsEmbed per-email results in the response (default: true). Pass false when polling for status only; it is cheaper and faster on large jobs
limitResults per page (default: 100, max: 1,000)
next_keyOpaque cursor from the previous page's next_key

When results are included, the job carries a results[] array of verification results (the same shape as POST /v1/verify), plus has_more and next_key for the next page. Each result also carries job_id and batch_job_id — both the id of this job, so a result can be traced back after it has been copied out of the response. For a full download in one request use GET /v1/jobs/{job_id}/download below.

Response

200 OK
{
  "success": true,
  "data": {
    "job_id": "9f8f6d2e-4c1b-4f6e-9a3b-2d1e5c7a8b90",
    "status": "completed",
    "total_emails": 1000,
    "processed_emails": 1000,
    "progress_percent": 100,
    "summary": {
      "valid": 800,
      "invalid": 100,
      "risky": 50,
      "unknown": 50
    },
    "created_at": "2026-02-03T14:30:00.000Z",
    "updated_at": "2026-02-03T14:35:42.000Z",
    "completed_at": "2026-02-03T14:35:42.000Z",
    "credits_used": 1000,
    "credits_refunded": 0,
    "billing_status": "charged",
    "results": [ { "email": "john@example.com", "status": "valid", "sub_status": "mailbox_exists", "quality_score": 91 } ],
    "has_more": true,
    "next_key": "eyJqb2JfaWQiOi..."
  }
}

Job status values

StatusDescription
pendingAccepted and queued; no email has been verified yet
processingVerification in progress
completedAll emails verified
failedJob failed (check error_message)
cancelledReserved. There is no way to stop a running job today, and nothing produces this status — once accepted, a job runs until it completes or fails. Size the batch before you send it.
enqueue_unknownSome emails have unconfirmed queue status; credits are held and reconciled automatically

Download results

GET/v1/jobs/{job_id}/download

Query parameters

ParameterDescription
formatcsv or json (default: csv)
filterFilter by status: all, valid, invalid, risky, unknown, spam_trap, disposable, role, catch_all (default: all)

Response

The response depends on how many results match. Up to 1,000 results are returned in the body: a CSV file (Content-Type: text/csv, sent as an attachment) for format=csv, or for format=json a plain object without the usual success/data wrapper:

200 OK · format=json
{
  "job_id": "9f8f6d2e-4c1b-4f6e-9a3b-2d1e5c7a8b90",
  "status": "completed",
  "result_count": 2,
  "results": [
    { "email": "john@example.com", "status": "valid", "sub_status": "mailbox_exists", "quality_score": 91 },
    { "email": "jane@example.com", "status": "valid", "sub_status": "mailbox_exists", "quality_score": 88 }
  ]
}

More than 1,000 results are written to a file instead, and the endpoint answers with a JSON envelope whatever format you asked for. Fetch download_url (a pre-signed link, no API key needed) within expires_in_seconds to get the CSV or JSON file:

200 OK · envelope
{
  "download_url": "https://kawaa-uploads.s3.amazonaws.com/downloads/...",
  "format": "csv",
  "result_count": 4812,
  "expires_in_seconds": 3600
}

A client that always saves the body to a file will therefore save this envelope as results.csv on large jobs. Check the response Content-Typetext/csv is the file, application/json with a download_url is the envelope.

Example

cURL
# Small job: the body is the CSV
curl -X GET "https://api.kawaa.com/v1/jobs/9f8f6d2e-4c1b-4f6e-9a3b-2d1e5c7a8b90/download?format=csv&filter=valid" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -o results.csv

# Any size: save the body, then follow the envelope if one came back
curl -s "https://api.kawaa.com/v1/jobs/9f8f6d2e-4c1b-4f6e-9a3b-2d1e5c7a8b90/download?format=csv&filter=valid" \
  -H "X-Api-Key: YOUR_API_KEY" -o results.csv
if jq -e .download_url results.csv >/dev/null 2>&1; then
  curl -s "$(jq -r .download_url results.csv)" -o results.csv
fi

List all jobs

GET/v1/jobs

List all verification jobs for your account with pagination.

Query parameters

ParameterDescription
limitNumber of results (default: 50, max: 100)
offsetOpaque pagination cursor. Pass the next_offset value from the previous page; the response also includes has_more
statusFilter by job status

Response

200 OK
{
  "success": true,
  "data": {
    "jobs": [
      {
        "job_id": "9f8f6d2e-4c1b-4f6e-9a3b-2d1e5c7a8b90",
        "filename": "emails.csv",
        "total_emails": 1000,
        "processed_emails": 1000,
        "status": "completed",
        "summary": { "valid": 800, "invalid": 100, "risky": 50, "unknown": 50 },
        "created_at": "2026-02-03T14:30:00.000Z",
        "completed_at": "2026-02-03T14:35:42.000Z"
      },
      {
        "job_id": "063a7a5f-a27c-49ff-ab6b-a29bf3bb81b2",
        "filename": null,
        "total_emails": 1,
        "processed_emails": 1,
        "status": "completed",
        "summary": { "valid": 0, "invalid": 1, "risky": 0, "unknown": 0 },
        "created_at": "2026-02-03T14:20:17.310Z",
        "completed_at": "2026-02-03T14:20:17.464Z"
      }
    ],
    "total": 1395,
    "total_is_approximate": false,
    "has_more": true,
    "next_offset": "eyJ1c2VyX2lkIjoi..."
  }
}

filename is the uploaded file's name for file jobs and null for batch and single verifications (single verifications made through POST /v1/verify are listed here too, as one-email jobs). total counts every job matching the listing, not just the page returned; total_is_approximate: true means the count stopped at its scan bound, so total is a floor. When has_more is false, next_offset is null.

Credits

  • 1 credit per email in batch
  • Credits deducted upfront when job is created
  • Emails that cannot be queued are refunded automatically; cached results are billed at 0.5 credits (the discount is reconciled on completion)
  • Credits held for emails the workers never process are refunded by job reconciliation