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
npm: kawaa
npm install kawaaimport { 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
PyPI: kawaa
pip install kawaafrom 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:
- 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 (undefinedin Node.js,Nonein Python) rather than shown as 0. SocreditsUsedon a batch result is optional in TypeScript, andcredits_usedisOptional[float]in Python. In Python, addingNoneto a number raisesTypeError. 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 withgetJobStatus()/get_job_status(). A repeat charged nothing more, so checkidempotentReplay/idempotent_replaybefore 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. - A failure with a success status is an error. If the API answers with a success status but
"success": falsein the body, Python now raisesKawaaErrorinstead 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. - 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
429is retried for every call: the rate limiter refuses a request before charging it. - A timeout, a dropped connection or a
5xxis retried for a read (aGET, which charges nothing) and for a paid call under its key. - A
409the API marksretryableon 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: falseorretry_may_charge_againis 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-V2signature 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 withtoleranceSeconds/tolerance_seconds. - The legacy
X-Kawaa-Signatureheader signs no time, so it is refused unless you passallowLegacySignature: 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-jobwebhook_urlcallback into the same envelope.
In FastAPI, from version 0.2.2 on, use fastapi_webhook_handler() as a dependency of your route:
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:
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:
- The signature header.
parseWebhook()/parse_webhook()checkX-Kawaa-Signature-V2, and refuse a delivery signed more than 300 seconds away from your clock. Pass that header where you passedX-Kawaa-Signature. The middleware and the Flask and FastAPI helpers read it themselves. - 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-jobwebhook_urlcallback sent without awebhook_secretis unsigned: read it withparseWebhookUnverified(body)/parse_webhook_unverified(body). - The event's fields moved. A job's fields are now under
data, and its counts underdata.summary. In Node.js,event.jobIdis gone: a TypeScript error, andundefinedin JavaScript. In Python, read them fromevent.job_completedorevent.job_failed, which areNonefor any other event;event.job_idraisesAttributeError. - Credit fields can be missing. In TypeScript,
AccountInfo.creditsUsed,BatchJobResult.creditsRemainingandFindResult.creditsRemainingare now optional.creditsUsedisundefinedwhen the API withholds your usage, andusageWithheldis thentrue. In Python, thecredits_remainingof a batch or a finder result isNonewhen the API sends none (it was0). Credit amounts are typedfloatand kept as the API sent them, so a fractional amount such as49.5is no longer cut to49; a whole amount still arrives as anint. - A missing secret stops your app at startup.
webhookMiddleware()andcreateWebhookHandler()throw aTypeError, andflask_webhook_handler()andfastapi_webhook_handler()raiseValueError, when you create them with a missing or blank secret. In 0.1.x they started and answered401to every delivery. - A short export is an error.
downloadResults()/download_results()throwIncompleteExportErrorwhen 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'scompleteisfalse, the export was measured short: try again in a few seconds. When it isnull/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. - Hand-built TypeScript results.
JobStatusResulthas 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 };# 1. The signature header
# 0.1.x
parse_webhook(body, request.headers.get("X-Kawaa-Signature"), secret)
# 0.2.0
parse_webhook(body, request.headers.get("X-Kawaa-Signature-V2"), secret)
# 2. A callback sent without a webhook secret
# 0.1.x
parse_webhook(body)
# 0.2.0
parse_webhook_unverified(body)
# 3. The event's fields
# 0.1.x
print(event.job_id, event.valid, event.total, event.download_url)
# 0.2.0: job_completed is None for any other event, job.failed included
job = event.job_completed
if job is not None:
print(job.job_id, job.total_emails, job.download_url)
if job.summary is not None:
print(job.summary.valid)
# 4. Credit fields
# 0.1.x: an int, and 0 when the API sent none
# 0.2.0: the number as sent (49.5, or a whole int), and None when the API sent none
batch = kawaa.verify_batch(emails)
if batch.credits_remaining is not None:
print(batch.credits_remaining)
# 5. A missing secret
# 0.1.x: answered 401 to every delivery
# 0.2.0: raises ValueError here, when the app starts
handler = flask_webhook_handler(os.environ.get("KAWAA_WEBHOOK_SECRET", ""))
# 6. A short export
# 0.1.x: returned the rows it had
# 0.2.0: raises IncompleteExportError; when its complete is False, try again in a few seconds
results = kawaa.download_results(job_id)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:
python -m pip install ".[dev]"
python -m pytest
python -m buildUpgrading 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.
- In both packages,
foundmeans a confirmed address. Thefoundof afind()result is true only whenemailis set andverifiedis true, which means the address checked as valid. Before, anyemailwith aconfidenceabove 0 counted, so a catch-all, risky or role result, where the API returns its best guess withverifiedfalse, read as found. The guess is still inemail: checkemailandstatusto use it knowingly. In Python,verifiedis alsoTrueonly when the API sent a JSONtrue. - Node.js: options must be your object's own properties.
new Kawaa()and the options ofverify(),verifyBatch(),verifyBatchAndWait(),waitForJob()andfind()read only the options set on the object itself. An option the object inherits, for example throughObject.create(defaults)or a getter on a class, is ignored and its default is used, so an inheritedbaseUrlno longer applies. Options passed as an object literal work as before. - Node.js: your webhook handler's errors reach your code. When the handler you give
createWebhookHandler()throws aValidationError, 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. - Python: the API key is checked when the client is created.
Kawaa(api_key)andAsyncKawaa(api_key)raiseAuthenticationErrorwhen the key isNone, empty, only whitespace or not astr. In 0.2.0 aNonekey failed only on the first call, with an error that did not name the key, and an empty key was sent to the API. Abyteskey, which 0.2.0 sent as given, is refused too: decode it first. - Python:
credits_usedcan beNone.AccountInfo.credits_usedisNonewhen the API does not send the figure, which it leaves out for a key that has theaccount:readscope but notanalytics:read, andusage_withheldis thenTrue. 0.2.0 reported0, so "spent nothing" and "not allowed to see it" looked the same. Check forNonebefore 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);# 1. found means a confirmed address
# 0.2.0: a catch-all guess could have found True with verified False
# 0.2.2: found is False there, and email still holds the guess
def describe(result):
if result.found:
return f"Confirmed: {result.email}"
if result.email:
return f"Best guess, not confirmed: {result.email} ({result.status})"
return "Not found"
# 4. The API key is checked when the client is created
# 0.2.0: with KAWAA_API_KEY unset, this failed only on the first call
# 0.2.2: it raises AuthenticationError here; decode a bytes key before you pass it
kawaa = Kawaa(os.environ.get("KAWAA_API_KEY"))
# 5. Usage the API withholds
# 0.2.0: account.credits_used was 0
# 0.2.2: account.credits_used is None, and account.usage_withheld is True
account = kawaa.get_account()
if account.credits_used is not None:
print(account.credits_used)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()andverify_batch_and_wait()raiseValidationErrorfortimeout_msbefore sending anything. The API refusestimeout_mson every batch (it applies toverify()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 wherecredits_usedis used as a number without a check forNone. - A batch result carries
invalid_emails_skipped, the number of malformed addresses the API refused and did not verify, andenqueue_unknown, the number of addresses that could not be confirmed as queued. The API sent both, and 0.2.0 dropped them. verify_signature(), andparse_webhook()withallow_legacy_signature=True, accept astrbody and check it as its UTF-8 bytes. 0.2.0 raisedTypeErrorfor one.
Other languages
Go
Not yet publishedGo 1.21+
No package registry listing yet
Ruby
Not yet publishedRuby 2.7+
No package registry listing yet
PHP
Not yet publishedPHP 8.1+
No package registry listing yet
Java
Not yet publishedJava 11+
No package registry listing yet
C# / .NET
Not yet published.NET 6+
No package registry listing yet
Rust
Not yet publishedRust 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.