Verification

Verify a list of addresses

Submits up to your plan's batch size in unique, well-formed addresses (Free 100, Starter 1,000, Professional and Business 5,000, Enterprise 10,000; more is a 413, and the raw list may hold at most 10,000 entries) and answers immediately with a job_id. Charges about 1 credit per address at the moment the job is accepted, not when it finishes.

Follow it with GET /v1/jobs/{id}, or supply webhook_url and be told. Send Idempotency-Key to make a retry safe.

options.timeout_ms is refused: it bounds the synchronous wait on POST /v1/verify and nothing on this path honours it.

post/v1/verify/batch

Request

  • Base URL: https://api.kawaa.com
  • URL: https://api.kawaa.com/v1/verify/batch
  • Auth: one of:
    • API key in header X-Api-Key
    • HTTP bearer
    • OAuth 2 (authorization code, authorization URL https://api.kawaa.com/oauth/authorize, token URL https://api.kawaa.com/oauth/token, scopes: verification:write)

Headers

Idempotency-Keystring

Makes a charging call safe to retry: a repeat under the same key replays the original outcome instead of buying the work again. The window depends on the route: the receipt behind it expires, and after that the same key is a new paid request. It is 30 days for a deliverability check, at least 30 days for a single find, 24 hours for an activity lookup, and 15 minutes for a bulk find. What a replay returns also differs: a batch replays its JOB, an activity lookup replays its RESULT. A different request under the same key is 409 IDEMPOTENCY_KEY_CONFLICT, before any charge.

How long a key stays safe differs per route, and for the verification routes it depends on the plan. POST /v1/verify, /v1/verify/batch and /v1/verify/file keep their receipts for the job's data-retention period, never less than 30 days, so longer on plans that keep results longer. The job itself is kept only for the plan's data-retention period, which is 7 days on Free: a repeat that arrives after the job's retention period has ended but while its receipt is still live is refused with 410 IDEMPOTENT_JOB_EXPIRED and charged nothing, rather than bought a second time; send a new key to run it again. Once the receipt expires the same key claims a fresh target and creates another paid job. POST /v1/deliverability/check keeps its receipt 30 days. POST /v1/find keeps its receipt at least 30 days and its stored address only for the plan's retention period; a repeat after the address expires but before the receipt does return 410 IDEMPOTENT_JOB_EXPIRED without charging. The activity lookups keep receipts about 24 hours, and POST /v1/find/bulk 15 minutes. Without a key, GET /v1/activity/{email} and GET /v1/activity/domain/{domain} still answer an identical repeat from the same account free for 15 minutes after the lookup that was charged (POST /v1/find/bulk does the same for its content); a key buys a fresh read inside that window. An unkeyed POST /v1/verify that arrives while an identical one from the same account is still being verified (at most 15 minutes after its charge) joins it and is not charged; once that verification has its answer, a repeat is charged as usual. Only a key makes a later retry free.

Retry with the key you have rather than minting a new one. A key is not a refund (it prevents a second charge, it does not undo the first), but it stays good far longer than the shortest advertised retention suggests, and dropping one early is how a retry becomes a second paid job.

The characters are checked after trimming. Surrounding whitespace is trimmed from the header first; the resulting key must be 1-255 printable ASCII characters (0x21-0x7e) other than a comma, with no internal spaces or anything outside that range. Leading and trailing whitespace is accepted and is not part of the key. Because OpenAPI constraints apply to the raw header rather than the post-trim value, this schema deliberately has no minLength, maxLength, or pattern; those constraints would make generated clients reject values the API accepts. Invalid post-trim values get 400 before the route starts. A UUID is the obvious choice. Sending the header twice with different raw values is also a 400. A key may not contain a comma, because a header sent more than once reaches the API joined by commas; a comma is refused with the same 400, before anything is charged. A receipt claimed under a comma-bearing key before 2026-10-03 still replays for its window.

Request body

emailsstring[] required

The addresses to verify. maxItems here is the ABSOLUTE ceiling (Enterprise); the limit that applies is your plan's (Free 100, Starter 1,000, Professional and Business 5,000, Enterprise 10,000), and a longer list is 413 BATCH_SIZE_EXCEEDED before anything is charged: a capacity refusal, not malformed input, so an integration can branch on the status and split or upgrade rather than treating it as a bad request. GET /v1/account reports the plan. At least one entry must be a usable address; a list with none is a 400.

webhook_urlstring

Where to POST the completion callback. HTTPS only: anything else is refused with 400 WEBHOOK_URL_INVALID. Without a webhook_secret the callback is unsigned. Omit the field for no callback; an explicit null is a wrong-type value and is refused before the job is created or charged. No scheme pattern is declared: the URL is parsed first, which tolerates surrounding whitespace and normalizes the protocol, so " https://x" and "HTTPS://x" are both accepted, and an anchored lower-case pattern refused both. It must still be HTTPS; that is checked on the parsed protocol, not on the string you sent.

include_activityboolean
webhook_secretstring

Signs the callback to webhook_url. Stored with the job, and every delivery then carries X-Kawaa-Signature-V2 (an HMAC of the send time and the body, which lets you refuse a replay) and the legacy X-Kawaa-Signature computed from it. The flat callback body has no timestamp of its own, so check the t in X-Kawaa-Signature-V2. Without it the callback is UNSIGNED and anything that can reach your URL can post a convincing completion. Only meaningful alongside webhook_url. It has no meaning on its own: a secret with no webhook_url is 400 INVALID_WEBHOOK_SECRET, because there is no callback to sign. dependentRequired says so above. Omit the field for unsigned callbacks; an explicit null is refused with 400 INVALID_WEBHOOK_SECRET before the job is created or charged.

Response

Accepted and charged. The verdicts arrive on the job.

A replay is also a 202, and it charged nothing. Repeating the submission under the same Idempotency-Key returns the ORIGINAL job with idempotent_replay: true and credits_used: 0. Read those two fields before reconciling a debit from this status: the 202 alone does not distinguish a new paid job from a recovered one. A repeat that arrives while another request under the key is still queueing the batch is a replay too, with dispatch_in_progress: true and a Retry-After header: repeat after the delay, or poll the job.

successunknown required

Changes

No recorded changes to this operation across all 1 revision of this API.