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

# Python SDK

> knoxcall — the official KnoxCall client for Python. One import, sync or async; OAuth 2.1 + DPoP under the hood.

`knoxcall` is the official KnoxCall client for Python. One import gives you a sync client (thread- and fork-safe, share it as a module-level singleton) or an async client for FastAPI and friends. Every method's return shape is a `TypedDict` in `knoxcall.types`, so IDEs and mypy see the exact server fields.

## Install

<Note>
  Not yet published to PyPI — install from the monorepo path or via git. Requires Python 3.10+.
</Note>

```bash theme={"dark"}
pip install path/to/KnoxCall/sdk/knoxcall-python
# or
pip install "knoxcall @ git+https://github.com/knoxcall/knoxcall#subdirectory=sdk/knoxcall-python"
```

## Create a client

```python theme={"dark"}
from knoxcall import KnoxCall

# Credentials inline — the tenant is discovered from the credential
client = KnoxCall(client_id="tk_xxxxxxxx", client_secret="...")

# Or zero-arg with the environment configured
# (KNOXCALL_CLIENT_ID, KNOXCALL_CLIENT_SECRET)
client = KnoxCall()

# A pre-acquired key also works (kc_… token or legacy tk_… / AKE… key)
client = KnoxCall(api_key="tk_live_...")

# Test mode: sandbox host + tk_test_ key
sandbox = KnoxCall(api_key="tk_test_...", sandbox=True)

# Async — use in FastAPI, async frameworks, etc.
async with KnoxCall(sync=False) as client:
    routes = await client.routes.list()
```

With no explicit credentials, the SDK auto-detects workload identity (GitHub Actions, GCP, AWS IRSA, Azure Managed Identity, Vercel, CircleCI) — no stored secrets in CI.

## Manage resources

Create a route, then list with pagination — single-object methods return `data` unwrapped; paginated lists return the full `{"data": [...], "meta": {...}}` page; `iterate()` walks every page:

```python theme={"dark"}
new = await client.routes.create(name="my-api", target_base_url="https://api.example.com")

page = await client.routes.list(page=2, per_page=50)
print(page["meta"]["total_pages"], [r["name"] for r in page["data"]])

async for route in client.routes.iterate():
    print(route["name"])
```

The sync client exposes the identical surface without `await`. The same pattern covers every resource: `secrets`, `webhooks`, `clients`, `oauth_clients`, `environments`, `api_keys`, `account`, `audit_logs`, `agents`, `crypto`, `pki`, `vaults`, and `dynamic_db`.

## Call routes through the proxy

`client.call()` proxies a request through a KnoxCall route to your upstream and returns the raw `httpx.Response` — the upstream's HTTP status belongs to you; the SDK never turns it into an error. Reference routes by **slug** (write-once, rename-proof); UUIDs also work.

```python theme={"dark"}
# GET
resp = await client.call("payments-stripe", path="/users")
users = resp.json()

# POST with body
resp = await client.call("payments-stripe", method="POST", path="/v1/charges",
                          body={"amount": 2000, "currency": "usd"})

# Target a specific environment
resp = await client.call("payments-stripe", path="/data", environment="staging")
```

### Bound routes

State the route (and optional defaults) once with `client.route()`, then use plain HTTP verbs:

```python theme={"dark"}
printnode = client.route("ops-printnode", environment="production")

computers = printnode.get("/computers").json()
printnode.post("/printjobs", body={"printerId": 1, "title": "Invoice"})
printnode.request("DELETE", "/printjobs/42")
# per-call kwargs still override the bound defaults:
printnode.get("/computers", environment="staging")
```

Works identically on the async client (`await printnode.get(...)`). The handle holds no state beyond the defaults — retries, token refresh, and 401 re-mint behave exactly as on `call()`.

## Verify webhooks

`construct_webhook_event` verifies a delivery AND returns the parsed, typed event in one step. Pass the RAW request body (never re-serialized JSON):

```python theme={"dark"}
from knoxcall import construct_webhook_event, WebhookSignatureVerificationError

try:
    event = construct_webhook_event(request.body, request.headers, "whsec_...")
except WebhookSignatureVerificationError:
    return 400

if event["event"] == "request.server_error":
    alert(event["data"]["route_name"], event["data"]["response"]["status"])
```

`format=` matches the webhook's configured `hmac_format`: `"legacy"` (default), `"stripe"`, `"github"`, `"slack"`, `"aws-sns"`, or `"custom"` (pass `header_name=`). `tolerance_seconds` (default 300) bounds replay; pass `None` to disable. Also available as `client.webhooks.construct_event()` on both facades.

## Handle errors

```python theme={"dark"}
from knoxcall import RateLimitError, ValidationError, AuthenticationError, NotFoundError

try:
    await client.routes.create(name="x", target_base_url="https://api.example.com")
except ValidationError as e:
    print(e.fields)
except RateLimitError as e:
    await asyncio.sleep(e.retry_after or 1)
except AuthenticationError:
    await client.authenticate()
except NotFoundError:
    pass
```

403 maps to `PermissionDeniedError`. Transport failures map to `APIConnectionError` / `APIConnectionTimeoutError`. Every API error carries the server's `request_id` — quote it when contacting support.

## Retries and idempotency

Management requests retry automatically on 408/429/500/502/503/504 with exponential backoff + jitter (3 attempts by default; never 409; `Retry-After` honored on 429, capped at 30s). Mutating requests get an auto-generated ULID idempotency key so retries are safe. A 401 triggers one transparent token re-mint. Data-plane transport failures retry only when safe: connection-refused always, later failures (read timeout, keepalive reset) only for GET/HEAD — a mutating request is never replayed.

## DPoP

```python theme={"dark"}
client = KnoxCall(tenant="acme", dpop="always")
```

The SDK generates an ES256 keypair, binds the access token via `cnf.jkt`, and signs a fresh proof on every request — both management calls and proxy `call()` requests. In the default `"auto"` mode it upgrades automatically when the OAuth client requires DPoP.

## Full reference

The package README documents every resource method, the ephemeral proxy, Redis token store, request-body encoding (`datetime` / `Decimal` / `UUID` / `set` out of the box), thread-safety details, and credential-less `signup()` / `signup_sync()`: [`sdk/knoxcall-python/README.md`](https://github.com/knoxcall/knoxcall/tree/main/sdk/knoxcall-python) in the monorepo.
