> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kuahr.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Python SDK

> The official kua client for Python.

<Card title="View on GitHub" icon="github" href="https://github.com/TRSNg/kua-developer/tree/main/python" horizontal>
  Source, releases, and issues.
</Card>

## Install

```bash theme={null}
pip install kua
```

Requires Python 3.9+. Typed (ships `py.typed`), built on `httpx`.

## Initialize

```python theme={null}
import os
from kua import Kua

kua = Kua(api_key=os.environ["KUA_API_KEY"])
```

Get your key from **Settings → API keys** in the
[dashboard](https://dashboard.kuahr.com/settings/api-keys). Use a `kua_test_…`
key to work against [sandbox data](/concepts/data-model#test-mode). `Kua` can also
be used as a context manager (`with Kua(...) as kua:`) to close the connection.

## Examples

```python theme={null}
# List payroll runs
runs = kua.payroll_runs.list()

# Add an employee (retry-safe with an idempotency key)
employee = kua.employees.create(
    first_name="Jane",
    last_name="Doe",
    email="jane@acme.com",
    employment_type_id="3f9a0b1c-…",
    employed_at="2026-07-01",
    idempotency_key="employee-jane-2026-07",
)

# Create a draft payroll run
run = kua.payroll_runs.create(year=2026, month=7)

# Poll events incrementally
events = kua.events.list(since="2026-07-25T10:30:00Z", limit=100)
```

## Errors

Non-2xx responses raise `KuaError` with a stable `code` and the HTTP `status`:

```python theme={null}
from kua import KuaError

try:
    kua.payroll_runs.create(year=2026, month=7)
except KuaError as err:
    if err.code == "INSUFFICIENT_SCOPE":
        ...  # the key is missing the payroll:write scope
```

## Webhooks

Register endpoints and verify deliveries against the **raw** request body:

```python theme={null}
import os
from flask import Flask, request, abort
from kua import verify_signature

app = Flask(__name__)

@app.post("/kua/webhooks")
def receive():
    if not verify_signature(
        request.get_data(),  # raw bytes, NOT request.json
        request.headers.get("X-Kua-Signature"),
        os.environ["KUA_WEBHOOK_SECRET"],
    ):
        abort(400)
    event = request.get_json()
    # …handle event (dedupe on event["id"])…
    return "", 200
```

`verify_signature` rejects deliveries older than 5 minutes by default (replay
protection); tune with `tolerance_seconds=`. See
[verifying signatures](/webhooks/verifying-signatures).
