Skip to main content
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

Not yet published to PyPI — install from the monorepo path or via git. Requires Python 3.10+.
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

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:
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.
# 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:
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):
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

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

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 in the monorepo.