Skip to main content

Guides

SDKs & libraries

Kawaa publishes Node.js and Python packages, both named kawaa: version 0.3.0 on npm and 0.3.0 on PyPI (on an older version? see Upgrading to 0.3.0, Upgrading from 0.1.x and Upgrading to 0.2.2). Clients for Go, Ruby, PHP, Java, C#, and Rust are not yet published to a package registry; until they are, call the REST API directly from those languages.

Node.js

JavaScript / TypeScriptPublished package

npm: kawaa

Installation
npm install kawaa
Quick example
import { Kawaa } from 'kawaa';

const kawaa = new Kawaa('YOUR_API_KEY');

// example.com publishes a null MX record: it accepts no mail.
const result = await kawaa.verify('test@example.com');
console.log(result.status); // 'invalid'

Python

Python 3.8+Published package

PyPI: kawaa

Installation
pip install kawaa
Quick example
from kawaa import Kawaa

kawaa = Kawaa('YOUR_API_KEY')

# example.com publishes a null MX record: it accepts no mail.
result = kawaa.verify('test@example.com')
print(result.status.value)  # 'invalid' (result.status is a VerificationStatus enum)

What the packages do

In Node.js 0.3.0 and Python 0.3.0:

  • Single and batch verification, job status and polling, result download, account and email finder
  • Not the whole API: no file upload, usage, webhook management, activity or deliverability methods
  • Promise-based with TypeScript types in Node.js; a synchronous and an async client in Python
  • An Idempotency-Key on every paid call, sent again on each retry, so a retry is not charged twice
  • Retries only what is safe to repeat: a 429, a read, or a paid call under its key
  • Webhook signature check on X-Kawaa-Signature-V2, with a 300-second replay window
  • Webhook parsing for the { event, data } envelope Kawaa sends
  • Result downloads that refuse a short export with IncompleteExportError instead of returning it as the whole job
  • Typed errors that carry the API error code and request ID
  • Redirects refused, so your API key is never sent on to another host
  • A kawaa command line in the Node.js package, built for scripts and agents

There is no usage method, and the account the packages return carries the credit fields but not the usage statistics GET /v1/account sends. Read those from the REST API directly. File verification is likewise REST-only: no published client uploads a file.

The catch-all flag, flags.catchAll in Node.js and flags.catch_all in Python, is true only when Kawaa saw the domain accept mail for any address. False covers two cases: the domain was checked and does not accept every address, or nothing measured it, as for a disposable address, a domain with no usable mail server, or a mail server that refused the mailbox or never answered. To tell them apart, read flags.catchAllMeasured (Node.js) or flags.catch_all_measured (Python): it is false when nothing measured catch-all, so treat an address as not catch-all only when it is true and the catch-all flag is false.

Upgrading to 0.3.0

Version 0.3.0 of both packages is a minor release because some code written for 0.2 needs a change. If you pin the packages to 0.2, for example with ^0.2.7 in package.json or kawaa~=0.2.5 for pip, you get 0.3.0 only when you change the pin. Upgrade with npm install kawaa@latest or pip install --upgrade kawaa, then check these three things:

  1. A batch's cost can be unknown. When a batch is sent again under the same key, most often because the client retried after an answer was lost, Kawaa does not charge again, and its answer says 0 for that repeat. Before 0.3.0 the packages reported that 0 as the batch's cost, so a paid batch looked free. Now verifyBatch() / verify_batch() read the batch's job and report what the batch was charged. If the job cannot be read, for example because the API key may not read jobs, the cost is left out (undefined in Node.js, None in Python) rather than shown as 0. So creditsUsed on a batch result is optional in TypeScript, and credits_used is Optional[float] in Python. In Python, adding None to a number raises TypeError. Code that adds up costs should not treat a missing cost as 0: the batch was charged, so count the total as incomplete, or read the batch's charge with getJobStatus() / get_job_status(). A repeat charged nothing more, so check idempotentReplay / idempotent_replay before adding a cost, or the same batch is counted twice. A repeated single verification whose cost cannot be read back now leaves its cost out too, where 0.2 reported 0.
  2. A failure with a success status is an error. If the API answers with a success status but "success": false in the body, Python now raises KawaaError instead of returning the body as a result. Node.js already threw an error; it now also carries the status, the error code, the request ID and the retry advice. In both, the error carries the call's idempotency key, and the request is not sent again.
  3. Redirects are refused. The packages send your API key with every request to the API, so they never follow a redirect from it: following one would send the key wherever the redirect points. The Kawaa API never redirects. If you see the redirect error, check the base URL you gave the client. An http:// address is the usual cause. Fetching a large result file from its signed download link is unchanged: that request carries no API key.

Errors also gained a field. error.nothingCharged in Node.js and e.nothing_charged in Python is true when the API said the request was refused before anything was charged. False means the API did not say so, not that you were charged.

Node.js and Python 0.2.5: whether catch-all was measured

Results carry a new flag, flags.catchAllMeasured in Node.js and flags.catch_all_measured in Python, on verifications, job result rows and download rows. It is true when Kawaa measured catch-all for the address. The catch-all flag itself is unchanged, so code written for 0.2.4 keeps working; before 0.2.5 its false could mean either answer. In TypeScript the new flag is optional in the type, so flags objects you build yourself still compile.

Node.js 0.2.7: bounded webhook headers

Upgrade webhook receivers to 0.2.7. Earlier Node.js releases could spend quadratic time refusing a malformed signature header. V2 verification now trims ASCII spaces and tabs with linear scans and refuses headers over 8,192 UTF-8 bytes or more than 16 v1 entries. Normal deliveries and secret-rotation headers within those limits still verify the exact raw body against the replay window. Python is unaffected.

Node.js 0.2.6: one set of error classes

An error a kawaa/webhooks helper throws, such as the ValidationError that parseWebhook throws for a bad signature, is now an instance of the ValidationError and KawaaError you import from kawaa, with import and with require. In 0.2.5 it was not, so a check such as error instanceof ValidationError around parseWebhook was false and a receiver fell through to its other error handling. Nothing else changed, and Python is unaffected.

Retries and charges

verify(), verifyBatch() / verify_batch(), verifyBatchAndWait() / verify_batch_and_wait() and find() charge credits. Each makes one Idempotency-Key per call and sends it on every attempt, so the API answers a retry from the first attempt instead of charging again. Pass your own as idempotencyKey (Node.js) or idempotency_key (Python).

  • A 429 is retried for every call: the rate limiter refuses a request before charging it.
  • A timeout, a dropped connection or a 5xx is retried for a read (a GET, which charges nothing) and for a paid call under its key.
  • A 409 the API marks retryable on a paid call means a copy of the same call is still running. It is retried under the same key, and the API replays the result once it is ready.
  • An answer marked retryable: false or retry_may_charge_again is never retried.

These rules cover requests to the API. When the API answers a large result download with a signed download URL, the file at that URL is fetched once, without retries: if that fetch fails, the download call fails and you can call it again.

Each wait is the API's Retry-After, up to 60 seconds, or an exponential backoff from one second when the API names none. maxRetries / max_retries is the number of retries after the first attempt: 3 by default, and 0 sends each request once.

If a paid call still fails, the error carries the key it sent: error.idempotencyKey in Node.js, e.idempotency_key in Python. Repeat the call with that key while its receipt lasts (at least 30 days for these calls) and you are not charged again. If the result has expired by then (after 7 days on the Free plan), the repeat is refused with 410 IDEMPOTENT_JOB_EXPIRED and charges nothing. Once the receipt has expired, the same key buys the work again, as a new key always does.

When the first answer to verify() is lost and the retry reaches a request the API had already accepted, the client reads the result back from its job (up to three reads, two seconds apart) and returns it with idempotentReplay / idempotent_replay set to true and creditsUsed / credits_used set to what that verification cost (left unset while that figure is still being settled). Nothing more is charged. If the result is not ready or cannot be read, you get the status pending and the job ID to poll, with the cost left unset.

A batch that is sent again under the same key is read back the same way. verifyBatch() / verify_batch() read the batch's job and return idempotentReplay / idempotent_replay set to true and creditsUsed / credits_used set to what the batch was charged so far. Nothing more is charged. Like the figure a new batch reports, it is not the final bill: addresses answered from cache are refunded later, and getJobStatus() / get_job_status() reports the final figures as creditsCharged / credits_charged, less creditsRefunded / credits_refunded. If the job cannot be read, the cost is left unset rather than shown as 0. Before 0.3.0 a repeated batch reported 0.

Webhook helpers

parseWebhook(), webhookMiddleware() and createWebhookHandler() (Node.js, from kawaa/webhooks) and parse_webhook(), flask_webhook_handler() and fastapi_webhook_handler() (Python, from kawaa.webhooks) check a delivery before your code sees it:

  • The X-Kawaa-Signature-V2 signature must match the raw body, and its time must be within 300 seconds of your clock, so a captured delivery cannot be replayed later. Change the window with toleranceSeconds / tolerance_seconds.
  • The legacy X-Kawaa-Signature header signs no time, so it is refused unless you pass allowLegacySignature: true / allow_legacy_signature=True. Even then, a request that carries the V2 header is checked against that header alone.
  • A missing or blank secret never turns the check off: the middleware and the handlers refuse to start without one, and parseWebhook() / parse_webhook() refuse every delivery.
  • They read the { event, data } envelope Kawaa sends to registered webhooks, and turn the flat body of a per-job webhook_url callback into the same envelope.

In FastAPI, from version 0.2.2 on, use fastapi_webhook_handler() as a dependency of your route:

Python: FastAPI
import os

from fastapi import Depends, FastAPI
from kawaa.webhooks import WebhookEvent, fastapi_webhook_handler

app = FastAPI()
get_event = fastapi_webhook_handler(os.environ["KAWAA_WEBHOOK_SECRET"])

@app.post("/webhook")
async def handle_webhook(event: WebhookEvent = Depends(get_event)):
    job = event.job_completed
    if job is not None:
        print(job.job_id)
    return {"status": "ok"}

Do not use that helper with version 0.2.0. There, FastAPI reads the helper's argument as a query parameter and answers every delivery 422 before the signature is checked, and a 4xx answer is not retried, so those deliveries are lost. Upgrade, or until you can, check the body with parse_webhook() in your own route, which works with 0.2.0 and later:

Python: FastAPI with 0.2.0
import os

from fastapi import FastAPI, HTTPException, Request
from kawaa import ValidationError
from kawaa.webhooks import parse_webhook

app = FastAPI()
SECRET = os.environ["KAWAA_WEBHOOK_SECRET"]

@app.post("/webhook")
async def handle_webhook(request: Request):
    body = await request.body()  # the raw bytes, never re-serialized JSON
    try:
        event = parse_webhook(body, request.headers.get("X-Kawaa-Signature-V2"), SECRET)
    except ValidationError:
        raise HTTPException(status_code=401, detail="Invalid signature")
    job = event.job_completed
    if job is not None:
        print(job.job_id)
    return {"status": "ok"}

The Webhooks page shows the same check without a package.

Upgrading from 0.1.x

Upgrade from 0.1.0 or 0.1.1

Those versions retry verify() after a 5xx or a dropped connection without an Idempotency-Key, so a retry that reaches the API after the first attempt already had its answer is charged as a new check. Their webhook helpers also read a flat payload that Kawaa no longer sends to registered webhooks. 0.2.0 fixes both.

Upgrade with npm install kawaa@latest or pip install --upgrade kawaa, then check your code against the changes below. Most webhook code needs at least one of them.

These changes in 0.2.0 can need code:

  1. The signature header. parseWebhook() / parse_webhook() check X-Kawaa-Signature-V2, and refuse a delivery signed more than 300 seconds away from your clock. Pass that header where you passed X-Kawaa-Signature. The middleware and the Flask and FastAPI helpers read it themselves.
  2. No secret, no parse. Called without a secret, parseWebhook(body) / parse_webhook(body) used to parse the body unchecked. Now they refuse it. A per-job webhook_url callback sent without a webhook_secret is unsigned: read it with parseWebhookUnverified(body) / parse_webhook_unverified(body).
  3. The event's fields moved. A job's fields are now under data, and its counts under data.summary. In Node.js, event.jobId is gone: a TypeScript error, and undefined in JavaScript. In Python, read them from event.job_completed or event.job_failed, which are None for any other event; event.job_id raises AttributeError.
  4. Credit fields can be missing. In TypeScript, AccountInfo.creditsUsed, BatchJobResult.creditsRemaining and FindResult.creditsRemaining are now optional. creditsUsed is undefined when the API withholds your usage, and usageWithheld is then true. In Python, the credits_remaining of a batch or a finder result is None when the API sends none (it was 0). Credit amounts are typed float and kept as the API sent them, so a fractional amount such as 49.5 is no longer cut to 49; a whole amount still arrives as an int.
  5. A missing secret stops your app at startup. webhookMiddleware() and createWebhookHandler() throw a TypeError, and flask_webhook_handler() and fastapi_webhook_handler() raise ValueError, when you create them with a missing or blank secret. In 0.1.x they started and answered 401 to every delivery.
  6. A short export is an error. downloadResults() / download_results() throw IncompleteExportError when the API cannot confirm the export holds every row of the job. 0.1.x returned the rows it had as if they were all of them. When the error's complete is false, the export was measured short: try again in a few seconds. When it is null / None, the API could not check the export, and a retry is not expected to change that: do not use it as the whole job, and do not retry it in a loop.
  7. Hand-built TypeScript results. JobStatusResult has a new required field, isSettled, so an object you build as one yourself, such as a test fake, needs it.
// 1. The signature header
// 0.1.x
parseWebhook(body, req.headers['x-kawaa-signature'], secret);
// 0.2.0
parseWebhook(body, req.headers['x-kawaa-signature-v2'], secret);

// 2. A callback sent without a webhook secret
// 0.1.x
parseWebhook(body);
// 0.2.0
parseWebhookUnverified(body);

// 3. The event's fields
// 0.1.x
console.log(event.jobId, event.results?.valid, event.downloadUrl);
// 0.2.0 (in JavaScript, leave out "as JobCompletedData")
if (event.event === 'job.completed') {
  const data = event.data as JobCompletedData;
  console.log(data.jobId, data.summary?.valid, data.downloadUrl);
}

// 4. Credit fields (TypeScript)
// 0.1.x
const used: number = account.creditsUsed;
// 0.2.0: creditsUsed is undefined when account.usageWithheld is true
if (account.creditsUsed !== undefined) console.log(account.creditsUsed);

// 5. A missing secret
// 0.1.x: answered 401 to every delivery
// 0.2.0: throws a TypeError here, when the app starts
webhookMiddleware(process.env.WEBHOOK_SECRET ?? '');

// 6. A short export
// 0.1.x: returned the rows it had
// 0.2.0: throws IncompleteExportError; when its complete is false, try again in a few seconds
const { results } = await kawaa.downloadResults(jobId, 'json');

// 7. A JobStatusResult you build yourself, such as a test fake (TypeScript)
// 0.2.0: add isSettled
const fake: JobStatusResult = { ...earlierFake, isSettled: true };

These commands install Node.js 0.3.0 and Python 0.3.0, including the changes listed under Upgrading to 0.2.2 below and the changes listed under Upgrading to 0.3.0.

Node.js 0.2.4: the command line's credit message

When a kawaa command is refused for credits (payment_required), next_step links the Buy credits tab as well as Billing. On a white-label sub-account, which cannot buy credits, it says to ask the account that provisioned it to allocate more. That and a new README opening are all that changed in Node.js 0.2.4; Python 0.2.4 changes only its README.

Node.js 0.2.3: API key validation

Pass the API key as the first argument: new Kawaa(apiKey, options). An options object such as new Kawaa({ apiKey }) now throws a TypeError immediately, with the correct call form. The error never includes the key.

A missing, null, empty or whitespace-only key throws AuthenticationError when the client is created. Valid string keys are sent unchanged.

Python 0.2.3: source archive checks

The source archive includes the response and signature fixtures needed by its tests. Install development dependencies to run the Flask and FastAPI webhook checks and rebuild the package. From the extracted archive:

Check a source archive
python -m pip install ".[dev]"
python -m pytest
python -m build

Upgrading to 0.2.2

Version 0.2.2 is a patch release of both packages, but these changes can need code. Which ones apply depends on the version you have:

  • Node.js 0.2.1: change 1.
  • Node.js 0.2.0: changes 1, 2 and 3.
  • Python 0.2.0: changes 1, 4 and 5. PyPI never had a version 0.2.1, so on PyPI 0.2.2 is the next version after 0.2.0.
  1. In both packages, found means a confirmed address. The found of a find() result is true only when email is set and verified is true, which means the address checked as valid. Before, any email with a confidence above 0 counted, so a catch-all, risky or role result, where the API returns its best guess with verified false, read as found. The guess is still in email: check email and status to use it knowingly. In Python, verified is also True only when the API sent a JSON true.
  2. Node.js: options must be your object's own properties. new Kawaa() and the options of verify(), verifyBatch(), verifyBatchAndWait(), waitForJob() and find() read only the options set on the object itself. An option the object inherits, for example through Object.create(defaults) or a getter on a class, is ignored and its default is used, so an inherited baseUrl no longer applies. Options passed as an object literal work as before.
  3. Node.js: your webhook handler's errors reach your code. When the handler you give createWebhookHandler() throws a ValidationError, for example from a failed client call inside it, 0.2.0 could answer { status: 401 } as if the signature were bad. From 0.2.1 the promise the handler returns rejects with that error, as it already did for any other error, so catch it or let your framework answer it.
  4. Python: the API key is checked when the client is created. Kawaa(api_key) and AsyncKawaa(api_key) raise AuthenticationError when the key is None, empty, only whitespace or not a str. In 0.2.0 a None key failed only on the first call, with an error that did not name the key, and an empty key was sent to the API. A bytes key, which 0.2.0 sent as given, is refused too: decode it first.
  5. Python: credits_used can be None. AccountInfo.credits_used is None when the API does not send the figure, which it leaves out for a key that has the account:read scope but not analytics:read, and usage_withheld is then True. 0.2.0 reported 0, so "spent nothing" and "not allowed to see it" looked the same. Check for None before you add, compare or format it. The Node.js package already did this.
// 1. found means a confirmed address
// 0.2.1 and earlier: a catch-all guess could have found: true with verified: false
// 0.2.2: found is false there, and email still holds the guess
function describe(result) {
  if (result.found) return `Confirmed: ${result.email}`;
  if (result.email) return `Best guess, not confirmed: ${result.email} (${result.status})`;
  return 'Not found';
}

// 2. Options your object inherits
// 0.2.0: new Kawaa(apiKey, Object.create(defaults)) used defaults.baseUrl
// 0.2.1 and later: pass them as the object's own properties (defaults is a plain object)
const kawaa = new Kawaa(apiKey, { ...defaults });

// 3. A ValidationError thrown inside your own webhook handler
const handle = createWebhookHandler(secret, async (event) => {
  await kawaa.verify(address); // can throw a ValidationError
});
// 0.2.0: await handle(req) could resolve to { status: 401 }
// 0.2.1 and later: await handle(req) throws the ValidationError; catch it, or let your framework answer it
const answer = await handle(req);

Python 0.2.2 also brings these changes from 0.2.0:

  • fastapi_webhook_handler() works in a FastAPI app; see Webhook helpers.
  • verify_batch() and verify_batch_and_wait() raise ValidationError for timeout_ms before sending anything. The API refuses timeout_ms on every batch (it applies to verify() only), so a batch given it could never start.
  • The package ships its type hints (py.typed), so mypy and pyright read them and can report errors they did not report with 0.2.0, for example where credits_used is used as a number without a check for None.
  • A batch result carries invalid_emails_skipped, the number of malformed addresses the API refused and did not verify, and enqueue_unknown, the number of addresses that could not be confirmed as queued. The API sent both, and 0.2.0 dropped them.
  • verify_signature(), and parse_webhook() with allow_legacy_signature=True, accept a str body and check it as its UTF-8 bytes. 0.2.0 raised TypeError for one.

Other languages

  • Go

    Not yet published

    Go 1.21+

    No package registry listing yet

  • Ruby

    Not yet published

    Ruby 2.7+

    No package registry listing yet

  • PHP

    Not yet published

    PHP 8.1+

    No package registry listing yet

  • Java

    Not yet published

    Java 11+

    No package registry listing yet

  • C# / .NET

    Not yet published

    .NET 6+

    No package registry listing yet

  • Rust

    Not yet published

    Rust 1.88+

    No package registry listing yet

There is no Go, Ruby, PHP, Java, C# / .NET or Rust package to install yet. The Node.js and Python packages above are published today; from those languages, call the REST API directly: it is a single JSON POST with an X-Api-Key header. For early access to one of these clients, email developers@kawaa.com.

Additional SDKs

Additional registry packages will be listed here after they are published and verified.

Want to build an SDK for another language? Let us know and we'll feature it here.