Metadata-Version: 2.5
Name: kawaa
Version: 0.2.3
Summary: Official Python SDK for Kawaa Email Verification API
Project-URL: Homepage, https://kawaa.com
Project-URL: Documentation, https://kawaa.com/docs/sdks
Project-URL: Support, https://kawaa.com/support
Author-email: Kawaa <support@kawaa.com>
License: MIT
License-File: LICENSE
Keywords: api,email,kawaa,validation,verification
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.8
Requires-Dist: httpx>=0.24.0
Provides-Extra: dev
Requires-Dist: build>=1.2.0; extra == 'dev'
Requires-Dist: fastapi>=0.100.0; extra == 'dev'
Requires-Dist: flask>=2.0.0; extra == 'dev'
Requires-Dist: hatchling>=1.24.0; extra == 'dev'
Requires-Dist: mypy>=1.0.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
Requires-Dist: pytest-httpx>=0.22.0; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Description-Content-Type: text/markdown

# Kawaa Python SDK

Python SDK source for [Kawaa](https://kawaa.com) Email Verification API.

## Features

- Verify single emails or batch lists
- Automatic retry with exponential backoff for rate limits, failures before a request reaches the API (connection refused, connect or pool timeout, any method), and safe repeats: GETs plus `verify`/`verify_batch`/`find`, which send one `Idempotency-Key` per call and reuse it after a read timeout, dropped response or 5xx so a retry is not charged twice. `download_results()` fetches the signed download URL directly and is not retried. A `409` `CONFLICT` marked `retryable` on one of those keyed calls means a copy of the same call is still running (for example this client's own retry reached the API while the first attempt was still working): it is retried with the same key, and the repeat is replayed rather than charged again. A response that says `retry_may_charge_again` is never sent again automatically, and neither is one that says `retryable: false` (waiting cannot cure it). `max_retries` counts retries after the first attempt (default 3, so up to four requests); `0` sends exactly one request. Before a retry the client waits as long as the API's `Retry-After` asks, up to `MAX_RETRY_AFTER_SECONDS` (60 seconds); without one it backs off 1, 2, 4 seconds and so on. When the retries run out, the error carries the key that every attempt sent (see Error Handling)
- A single `verify()` whose retry is answered as a replay of a request the API had already accepted (its first response was lost after the charge) reads the verification back from its job (up to three reads, two seconds apart) instead of returning the replay's bare `pending`. The result carries `idempotent_replay=True` and `credits_used` set to what that original verification cost (0.5 for a cached result). `credits_used` is `None` when the job's cache discount was still being applied after the short poll; read the job by `job_id` (its `credits_charged` less `credits_refunded`) for the final charge. If it cannot be read back (still running, or the key lacks `verification:read`) you get the replay itself: status `pending`, the `job_id` to poll with `get_job_status`, and `credits_used` 0 because the repeat charged nothing more. So a keyed repeat that was read back reports the original `credits_used` (for example 1), not 0: only `idempotent_replay=True` tells you that this call charged nothing more, so check it before adding up `credits_used` across calls
- Credit amounts are fractional (a cached verification costs 0.5, so a balance of `260.25` is ordinary), and every credit field (`credits_used`, `credits_remaining`, `credits_held`, `monthly_allowance`, the credit webhooks) is typed `float`. The value is the JSON number as the API sent it: an `int` when it is whole, a `float` when it is not
- Batch responses expose `idempotent_replay` when a repeat recovered the existing job. In that case `credits_used` is `0` for the repeat; `credits_remaining` is `None` when the original post-charge balance cannot be safely recovered
- `find()` is the email finder. It needs the Starter plan or higher: on the Free plan the API refuses it with `403 PLAN_UPGRADE_REQUIRED` before anything is charged
- `find()` exposes `idempotent_replay` too, and it behaves differently: a keyed repeat (the client sends an `Idempotency-Key` on every call) replays the stored result free for 30 days, and a keyless identical repeat is de-duplicated inside a fixed two-minute bucket. Either way it still reports `credits_used: 5`, what the SEARCH cost, and only the marker says whether THIS call paid it. `None` means the API said nothing either way, not that it charged. Summing `credits_used` across responses without checking it over-counts by exactly what the de-duplication saved
- Both sync and async clients
- Full type hints for IDE support
- Webhook signature verification
- Framework integrations (Flask, FastAPI)

## Local Development

Install the published package with `pip install kawaa` (0.2.0 or later). To work on the SDK itself:

```bash
python3 -m pip install -e .
```

## Quick Start

```python
from kawaa import Kawaa

# Create client with your API key
client = Kawaa("your-api-key")

# Verify a single email
result = client.verify("john@example.com")
print(f"Status: {result.status.value}")  # valid, invalid, risky, unknown, spam_trap, disposable, role, catch_all (or pending on sync timeout)
print(f"Score: {result.score}")     # 0-100 quality score

if result.is_valid:
    print("Email is valid!")

if result.flags.disposable:
    print("Warning: disposable email")
```

## Single Email Verification

```python
from kawaa import Kawaa

client = Kawaa("your-api-key")

# Basic verification
result = client.verify("user@example.com")

# Access results
print(f"Email: {result.email}")
print(f"Status: {result.status.value}")  # the string, e.g. "invalid"; result.status is a VerificationStatus enum
print(f"Score: {result.score}")        # 0-100
print(f"Disposable: {result.flags.disposable}")
print(f"Role account: {result.flags.role}")
print(f"Free provider: {result.flags.free_provider}")
print(f"Catch-all: {result.flags.catch_all}")

# AI-suggested correction (if typo detected)
if result.suggestion:
    print(f"Did you mean: {result.suggestion}")

# Customize verification options (the real API options)
result = client.verify(
    "user@example.com",
    deep_verify=True,     # Deep (SMTP-level) verification
    include_ai=True,      # AI typo detection / catch-all confidence
    enrich=False,         # Opt-in data enrichment (name, gender, country);
                          # Business plan and up
    timeout_ms=24000,     # How long POST /v1/verify waits for a synchronous
                          # result (ms). Single-verify only: batch and file
                          # jobs are asynchronous and refuse it.
    skip_cache=False,     # Bypass the verification cache
)
```

`enrich` needs the Business plan or higher. On a lower plan the API refuses the whole request with `403 PLAN_UPGRADE_REQUIRED` and charges nothing, so leave it out there.

## Batch Verification

For verifying many emails at once:

```python
from kawaa import Kawaa

client = Kawaa("your-api-key")

emails = [
    "john@example.com",
    "jane@example.com",
    "invalid@",
    "john@example.com",  # duplicate - will be removed
]

# Start batch job
job = client.verify_batch(emails)
print(f"Job ID: {job.job_id}")
print(f"Duplicates removed: {job.duplicates_removed}")
print(f"Malformed, not verified: {job.invalid_emails_skipped}")  # "invalid@" here
print(f"Credits used: {job.credits_used}")

# Poll for completion
result = client.wait_for_job(job.job_id, timeout=300)
print(f"Valid: {result.valid_count}")
print(f"Invalid: {result.invalid_count}")
print(f"Risky: {result.risky_count}")

# result.results is the first page only (up to 100 results). Read the rest
# with next_key while has_more is true, or use download_results(job_id).
page = result
while True:
    for r in page.results:
        print(f"{r.email}: {r.status.value} (score: {r.score})")
    if not page.has_more:
        break
    page = client.get_job_status(job.job_id, include_results=True, next_key=page.next_key)
```

### One-liner with wait

```python
# Start job and wait for completion in one call
result = client.verify_batch_and_wait(
    emails,
    poll_interval=2.0,  # Check every 2 seconds
    timeout=600,        # Wait up to 10 minutes
)
```

### Using webhooks

```python
# Get notified when job completes
job = client.verify_batch(
    emails,
    webhook_url="https://hooks.example.com/kawaa",
    webhook_secret="your-secret",  # Optional, for signature verification
)
```

## Download Results

```python
# Download as JSON
results = client.download_results(job_id)
for r in results.results:
    print(f"{r.email}: {r.status.value}")

# Download as CSV
csv_data = client.download_results(job_id, format="csv")
print(csv_data)  # CSV string

# For large result sets, you'll get a presigned URL
if results.download_url:
    print(f"Download from: {results.download_url}")
```

Every JSON response and presigned-URL export carries a completeness verdict.
The SDK returns those exports only when the API reports `complete=True`. A
measured-short export, or an older job whose completeness cannot be verified,
raises `IncompleteExportError`. If `error.complete is False`, retry the same
download after a few seconds; if it is `None`, do not treat the rows as the
whole job result set. A small inline CSV is raw CSV and carries no verdict;
the API refuses a measured-short inline CSV, but use JSON when you require a
positive completeness proof. The async client applies the same rule.
If the API refuses a measured-short inline CSV before it can return an
envelope, the same typed error has `complete is False`, HTTP status 409, and
null row counts; retry it in the same way.

## Async Client

For async/await support:

```python
import asyncio
from kawaa import AsyncKawaa

async def main():
    async with AsyncKawaa("your-api-key") as client:
        # Single verification
        result = await client.verify("user@example.com")
        print(f"Status: {result.status.value}")

        # Batch verification
        job = await client.verify_batch(emails)
        result = await client.wait_for_job(job.job_id)
        print(f"Valid: {result.valid_count}")

asyncio.run(main())
```

## Webhook Handling

Registered webhooks (`POST /v1/webhooks`, Settings → Webhooks) deliver an
**envelope**: the event-specific fields are nested under `data`:

```json
{
  "event": "job.completed",
  "data": {
    "job_id": "job_abc123",
    "summary": { "valid": 800, "invalid": 150, "risky": 40, "unknown": 10 },
    "download_url": "https://api.kawaa.com/v1/jobs/job_abc123/download"
  },
  "webhook_id": "wh_abc123",
  "timestamp": "2026-01-01T00:30:01Z"
}
```

Supported events: `verification.completed`, `verification.failed`,
`job.completed`, `job.failed`, `credits.low`, `credits.exhausted`,
`blacklist.detected`, `dmarc.failure`. `parse_webhook` returns a
`WebhookEvent` envelope with `event`, `data` (raw dict), `webhook_id`,
`timestamp`, `test`, and typed per-event properties (`job_completed`,
`verification_completed`, ...) that return a dataclass when the event
matches, else `None`. The legacy per-job `webhook_url` callback (flat body)
is automatically normalized into a `job.completed`-style envelope.

Verification fails closed, and checks the time as well as the signature.
`parse_webhook`, `flask_webhook_handler` and `fastapi_webhook_handler` check
the `X-Kawaa-Signature-V2` header: its signature must match the raw body and
its `t` must be within 300 seconds of your clock, so a captured delivery
cannot be replayed later (see [Replay Protection](#replay-protection-x-kawaa-signature-v2)).
`parse_webhook` raises `ValidationError` when the signature is missing,
wrong or outside the window, and when the secret is `None` or blank (an
unset `KAWAA_WEBHOOK_SECRET` never means "skip the check"). The framework
helpers answer 401 for any of those, malformed or non-ASCII headers
included, and 400 for a signed body that is not JSON. They raise
`ValueError` when you create them with a missing or blank secret, so the app
stops at startup instead of accepting forged requests.

The legacy `X-Kawaa-Signature` header is refused by these helpers unless you
pass `allow_legacy_signature=True` (see below). `verify_signature` still
checks it, and returns `False` for a blank secret or a malformed signature.

A per-job `webhook_url` callback created without a `webhook_secret` is sent
unsigned. To read one, call `parse_webhook_unverified(payload)`, and remember
that anyone who knows the URL can send a body that parses. Prefer passing a
`webhook_secret` with the batch.

```python
from kawaa.webhooks import parse_webhook

# Raises ValidationError unless X-Kawaa-Signature-V2 matches the raw body
# and was signed within the last 300 seconds.
event = parse_webhook(
    payload=request.body,  # the raw bytes, never re-serialized JSON
    signature=request.headers.get("X-Kawaa-Signature-V2"),
    secret="your-webhook-secret",
)

if job := event.job_completed:
    print(f"Job {job.job_id} completed!")
    print(f"Valid: {job.summary.valid}, Invalid: {job.summary.invalid}")
    print(f"Download: {job.download_url}")
elif verification := event.verification_completed:
    print(f"Verification {verification.job_id}: {verification.status}")
elif credits := event.credits_low:
    print(f"Credits low: {credits.credits_remaining} remaining")
else:
    print(f"Unhandled event {event.event}: {event.data}")
```

### Flask Integration

```python
import os

from flask import Flask
from kawaa.webhooks import flask_webhook_handler

app = Flask(__name__)

@app.route("/webhook", methods=["POST"])
@flask_webhook_handler(os.environ.get("KAWAA_WEBHOOK_SECRET"))  # ValueError if unset
def handle_webhook(event):
    if job := event.job_completed:
        print(f"Job {job.job_id} completed!")
    return "", 200
```

### FastAPI Integration

Read the raw body in your route and pass it to `parse_webhook`. This works
with every 0.2.x version:

```python
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"]  # KeyError at startup if unset

@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")
    if job := event.job_completed:
        print(f"Job {job.job_id} completed!")
    return {"status": "ok"}
```

`fastapi_webhook_handler()` gives the same check as a dependency. Use it only
with version 0.2.2 or later: in 0.2.0, FastAPI reads the dependency's
argument as a required query parameter and answers every delivery `422`
before the signature is checked, and Kawaa does not retry a `4xx`, so those
deliveries are lost.

```python
import os

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

app = FastAPI()
get_event = fastapi_webhook_handler(os.environ.get("KAWAA_WEBHOOK_SECRET"))  # ValueError if unset

@app.post("/webhook")
async def handle_webhook(event: WebhookEvent = Depends(get_event)):
    if job := event.job_completed:
        print(f"Job {job.job_id} completed!")
    return {"status": "ok"}
```

### Replay Protection (X-Kawaa-Signature-V2)

Every signed delivery also carries an `X-Kawaa-Signature-V2` header:

```
X-Kawaa-Signature-V2: t=1790000000,v1=3320cbcf2db86beb8caacccbd9b8ced9bff1f758481fe87976d99e4c3a0f7f95
```

`t` is the Unix time (in seconds) of that HTTP attempt, and `v1` is an
HMAC-SHA256, keyed with the same webhook secret, over `t`, a `.`, and the raw
body. Every attempt (automatic retries, a manual retry from the delivery log
and test deliveries) gets a fresh `t` and a fresh signature, while the body
stays the same. `parse_webhook`, the Flask and FastAPI helpers and
`verify_signature_v2` check the signature and reject a delivery whose `t` is
more than 300 seconds from your server's clock in either direction, so a
captured request cannot be replayed later.

To check the header yourself, without the helpers:

```python
import os

from flask import Flask, abort, request
from kawaa.webhooks import parse_webhook_unverified, verify_signature_v2

app = Flask(__name__)

@app.route("/webhook", methods=["POST"])
def handle_webhook():
    body = request.get_data()  # the raw bytes, never re-serialized JSON
    if not verify_signature_v2(
        body,
        request.headers.get("X-Kawaa-Signature-V2"),  # None when absent: not valid
        os.environ.get("KAWAA_WEBHOOK_SECRET"),
    ):
        abort(401)  # missing, malformed, wrong, or older than the window

    event = parse_webhook_unverified(body)  # the signature was checked above
    print(event.event)
    return "", 200
```

- The window defaults to `DEFAULT_TOLERANCE_SECONDS` (300). Change it with
  `tolerance_seconds=600` on `verify_signature_v2`, `parse_webhook`,
  `flask_webhook_handler` or `fastapi_webhook_handler`; a negative, NaN or
  infinite value raises `ValueError`, and a value that is not a number
  (`None` included) raises `TypeError`. The framework helpers check it when
  you create them.
- `payload` is the raw body as `bytes` (a `str` is hashed as its UTF-8
  bytes). It returns `False` (and never raises) for a missing or malformed
  header, a missing or blank secret, a stale or future `t`, or a signature
  that does not match. Keep your server clock in sync (NTP).
- A delivery replayed inside the window still verifies, and retries are
  legitimately the same event, so keep your handler idempotent.
- Every Kawaa delivery still carries the legacy `X-Kawaa-Signature` header
  too. It signs the body alone, so a captured request verifies against it
  forever. `parse_webhook` refuses a legacy value, and the Flask and FastAPI
  helpers refuse a request that carries only the legacy header, unless you
  pass `allow_legacy_signature=True`. Turn that on only for a sender that
  cannot send `X-Kawaa-Signature-V2`, knowing such requests can be
  replays. A request that carries `X-Kawaa-Signature-V2` is always checked
  against it, whatever that option says. `verify_signature` still checks the
  legacy header on its own.
- Upgrading from 0.1.x: pass `request.headers.get("X-Kawaa-Signature-V2")`
  to `parse_webhook` where you passed `X-Kawaa-Signature`. The framework
  helpers need no change.
- A per-job `webhook_url` callback created without a `webhook_secret` is
  unsigned and carries neither header.

## Account Information

```python
account = client.get_account()
print(f"Credits remaining: {account.credits_remaining}")  # e.g. 260.25 (credits are fractional)
print(f"Plan: {account.plan}")

if account.credits_used is None:
    # The API withholds usage from a key without the analytics:read scope
    # (account.usage_withheld is True). It is not a zero.
    print("Usage: not available to this API key")
elif account.allowance_basis == "billing_period" and account.next_reset:
    print(f"Used {account.credits_used} of {account.monthly_allowance} this billing period")
else:
    print(f"Used {account.credits_used} in total")  # nothing resets this on a schedule
```

`credits_used` is `None` when the API did not send it. It leaves the figure
out for a key that has `account:read` but not `analytics:read`, and then
`usage_withheld` is `True`. Read `None` as "not reported", never as 0.

`allowance_basis` says whether the allowance renews. `"billing_period"`
means `credits_used` is spend in the current billing period and `next_reset`
says when it refills. `"one_time_grant"` (the Free plan's credits, granted
once), `"no_billing_period"` (a paid plan with no billing period on file) and
`"provider_allocation"` (a white-label sub-account) mean nothing resets it on a
schedule, so `credits_used` is cumulative and `monthly_allowance` is not a
monthly figure. The API sends the field for ordinary accounts; for a
sub-account it sends none, so the SDK supplies `"provider_allocation"` from
`plan == "sub_account"`. `None` means the API did not say: treat it as
unknown, not as renewing. Take `"billing_period"` as renewing only when `next_reset` is set too: an API build from
before 2026-09-30 reported a paid plan with no billing period on file as
`"billing_period"` with no `next_reset`. The values are exported as the `ALLOWANCE_*`
constants.

## Error Handling

```python
from kawaa import Kawaa
from kawaa import (
    KawaaError,
    AuthenticationError,
    RateLimitError,
    InsufficientCreditsError,
    ValidationError,
    NotFoundError,
    TimeoutError,
)

client = Kawaa("your-api-key")

try:
    result = client.verify("user@example.com")
except AuthenticationError:
    print("Invalid API key")
except RateLimitError as e:
    print(f"Rate limited. Retry after {e.retry_after}s")
except InsufficientCreditsError:
    print("Not enough credits")
except ValidationError as e:
    print(f"Invalid request: {e.message}")
except TimeoutError:
    print("Request timed out")
except KawaaError as e:
    print(f"API error: {e.message}")
```

Every error carries the API's retry guidance when the response had it:

| Attribute | Type | Meaning |
|---|---|---|
| `code` | Optional[str] | The API's stable error code, such as `RATE_LIMITED` or `CONFLICT` |
| `request_id` | Optional[str] | The API's request id; quote it to support |
| `retryable` | Optional[bool] | `True` when the same request can succeed after a wait, `False` when it cannot, `None` when the API did not say |
| `retry_after_seconds` | Optional[float] | How long the API asked you to wait, as it sent it |
| `retry_requires_same_idempotency_key` | bool | Repeat this call only with `idempotency_key`: a repeat under it is replayed, not charged again |
| `retry_may_charge_again` | bool | The call may have been charged and has no key to repeat it under: check your balance before repeating |
| `idempotency_key` | Optional[str] | The key `verify`, `verify_batch` or `find` sent on every attempt (`None` for other calls) |

`verify`, `verify_batch` and `find` charge credits. When one of them fails after
the client has stopped retrying (a 5xx, a timeout, a dropped connection, a `409`
that outlasted the retries), repeat it with the key from the error. The API
answers a repeat under the same key from the first attempt, so it is not
charged twice. A new key is a new purchase.

```python
try:
    found = client.find("Ada", "Lovelace", "example.org")
except KawaaError as e:
    if not e.idempotency_key or e.retryable is False:
        raise
    found = client.find("Ada", "Lovelace", "example.org", idempotency_key=e.idempotency_key)
```

## Configuration

```python
client = Kawaa(
    api_key="your-api-key",
    base_url="https://api.kawaa.com",  # Custom API URL
    timeout=30.0,                       # Request timeout in seconds
    max_retries=3,                      # Retries after the first attempt (0 sends one request)
)
```

The API key is checked when the client is created. `Kawaa(api_key)` and
`AsyncKawaa(api_key)` raise `AuthenticationError("API key is required")` if
the key is `None`, empty, only whitespace, or not a `str`, so
`Kawaa(os.environ.get("KAWAA_API_KEY"))` with the variable unset fails where
the client is created, not on the first call. Decode a `bytes` key first.

## Context Manager

The client can be used as a context manager for automatic cleanup:

```python
with Kawaa("your-api-key") as client:
    result = client.verify("user@example.com")
# Connection closed automatically
```

## Models Reference

### VerificationResult

| Field | Type | Description |
|-------|------|-------------|
| `email` | str | The verified email |
| `status` | VerificationStatus | valid, invalid, risky, unknown, spam_trap, disposable, role, catch_all, pending |
| `score` | int | Quality score 0-100 |
| `flags` | VerificationFlags | Email characteristics |
| `suggestion` | str | AI-suggested correction |
| `verification` | VerificationDetails | Technical details |
| `job_id` | Optional[str] | The verification's job; poll it with `get_job_status` when `status` is pending |
| `credits_used` | Optional[float] | What this verification cost: 1 fresh, 0.5 cached |
| `credits_remaining` | Optional[float] | Balance after the charge; `None` when the API did not state it |
| `credits_held` | Optional[float] | Credits held rather than settled while `status` is pending |
| `idempotent_replay` | Optional[bool] | `True` when this answered a repeat of an already-accepted request, which charged nothing more (see Features) |

### VerificationFlags

| Field | Type | Description |
|-------|------|-------------|
| `disposable` | bool | Temporary email service |
| `role` | bool | Role account (info@, support@) |
| `free_provider` | bool | Free email provider |
| `catch_all` | bool | Domain accepts all emails |

### JobStatusResult

| Field | Type | Description |
|-------|------|-------------|
| `job_id` | str | Unique job identifier |
| `status` | JobStatus | pending, processing, completed, failed, cancelled, enqueue_unknown (accepted, but some queue sends had an unknown outcome; keep polling) |
| `total_emails` | int | Total emails in job |
| `processed_emails` | int | Emails processed so far |
| `valid_count` | int | Valid email count |
| `invalid_count` | int | Invalid email count |
| `risky_count` | int | Risky email count |
| `progress_percent` | float | Completion percentage |
| `results` | List[VerificationResult] | Individual results |
| `credits_charged` | Optional[float] | Credits the job was charged, when reported |
| `credits_refunded` | Optional[float] | Credits refunded so far; net cost is `credits_charged - credits_refunded` |

## License

MIT License - see [LICENSE](LICENSE) for details.

## Support

- Documentation: https://kawaa.com/docs
- Email: support@kawaa.com
- Support: https://kawaa.com/support
