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

# Python SDK configuration

> Every option create_seller and create_async_seller accept: signing secrets, usage keys, the outage policy, timeouts, HTTP client injection, and lifecycle.

`create_seller` returns a blocking `SellerClient` for WSGI apps; `create_async_seller` returns a non-blocking `AsyncSellerClient` for ASGI apps. Both accept the same keyword arguments and make the same decisions. Configure one client at startup and reuse it for every request.

The client validates its configuration at construction, not when the first request arrives: any invalid combination raises `ZCError` with code `malformed_input` (see [errors](/sdks/python/errors)).

```python theme={null}
import logging
import os

from zeroclick_sellers import create_async_seller

logger = logging.getLogger("zeroclick")

zeroclick = create_async_seller(
    # Signature verification
    signing_secrets={
        os.environ["ZEROCLICK_SIGNING_SECRET_KID"]: os.environ[
            "ZEROCLICK_SIGNING_SECRET"
        ]
    },
    tolerance_seconds=300,
    # Usage API credentials
    api_key=os.environ["ZEROCLICK_API_KEY"],
    # Allowance behavior
    allowance_unavailable_policy="allow",
    check_timeout_seconds=1.5,
    on_allowance_unavailable=lambda error: logger.warning(
        "allowance check failed: %s", error.code
    ),
)
```

## Options

<ParamField body="signing_secrets" type="Mapping[str, str]">
  Signing secrets keyed by key id: `{"hsec_…": "zcsec_…"}`. Every `zc-signature` header names the `kid` it was signed with, and the SDK uses it to pick the matching secret. The mapping must contain at least one entry. Provide exactly one of `signing_secrets` or `resolve_signing_secret`. Passing both, or neither, raises at construction.
</ParamField>

<ParamField body="resolve_signing_secret" type="Callable[[str], str | None]">
  A lookup function in place of the static mapping, for secrets that live in a secret manager. It receives the `kid` from the request's signature header and returns the secret, or `None` for an unknown kid. If it returns `None`, the SDK denies the request with the `401` response rather than raising. If the function raises, or returns anything other than a non-empty string, the SDK raises `ZCError` with code `signing_secret_resolution_failed`.
</ParamField>

<ParamField body="api_key" type="str">
  An API key carrying both the `usage:read` and `usage:write` scopes. It backfills whichever of the two scoped keys you leave unset, so a single key is enough for the whole client.
</ParamField>

<ParamField body="usage_read_key" type="str">
  A key with the `usage:read` scope, used for allowance checks (`guard` and `check_allowance`). When unset, it falls back to `api_key`.
</ParamField>

<ParamField body="usage_write_key" type="str">
  A key with the `usage:write` scope, used for usage reporting (`report_usage`). This is handy when a separate worker reports usage. When unset, it falls back to `api_key`.
</ParamField>

<ParamField body="api_base_url" type="str" default="https://api.zeroclick.io">
  Base URL for the ZeroClick usage API. Override it only to point the SDK at a test double.
</ParamField>

<ParamField body="tolerance_seconds" type="int" default="300">
  Maximum accepted difference, in seconds, between the timestamp in the `zc-signature` header and the current clock. The SDK denies a request outside the window with the `401` response (reason `stale_timestamp`).
</ParamField>

<ParamField body="allowance_unavailable_policy" type="str" default="allow">
  What `guard` does when the allowance API gives no usable answer after a signature has verified: `"allow"`, `"deny"`, or `"throw"`. Any other value raises at construction. See [outage policy](#outage-policy) below.
</ParamField>

<ParamField body="check_timeout_seconds" type="float" default="1.5">
  Timeout, in seconds, for calls to the ZeroClick usage API, passed to `httpx` per request. Despite the name, the client applies it to both allowance checks and usage reports. The SDK never retries a timed-out call.
</ParamField>

<ParamField body="on_allowance_unavailable" type="Callable[[ZCError], None]">
  Called with the `ZCError` whenever the allowance API fails to answer during `guard`. The hook runs before the SDK applies the policy, whatever the policy is. Use it for operational logging and metrics; it does not change the decision.
</ParamField>

<ParamField body="clock" type="Callable[[], float]" default="time.time">
  The time source for signature freshness checks. Inject a fixed clock in tests to verify recorded requests without patching `time.time`.
</ParamField>

<ParamField body="http_client" type="httpx.Client | httpx.AsyncClient">
  Your own configured `httpx` client (`httpx.Client` for `create_seller`, `httpx.AsyncClient` for `create_async_seller`), for when you need proxies, event hooks, or connection limits. When omitted, the SDK constructs one. Closing the seller client closes whichever `httpx` client it holds, injected or not.
</ParamField>

## Keys: one or two

`api_key` must carry both usage scopes. To follow least privilege, pass two scoped keys instead and omit `api_key`. The read key covers allowance checks, and the write key covers usage reporting:

```python theme={null}
import os

from zeroclick_sellers import create_async_seller

zeroclick = create_async_seller(
    signing_secrets={
        os.environ["ZEROCLICK_SIGNING_SECRET_KID"]: os.environ[
            "ZEROCLICK_SIGNING_SECRET"
        ]
    },
    usage_read_key=os.environ["ZEROCLICK_USAGE_READ_KEY"],
    usage_write_key=os.environ["ZEROCLICK_USAGE_WRITE_KEY"],
)
```

The constructor validates coverage. If neither `api_key` nor the scoped key covers a side, it raises `ZCError` with code `malformed_input` naming the missing key: `Provide usage_read_key (or a both-scopes api_key) for allowance checks`, or `Provide usage_write_key (or a both-scopes api_key) for usage reporting`.

## Rotating signing secrets

`signing_secrets` takes any number of entries, and the `kid` in each request selects the right one, so rotation is zero-downtime. Create the new secret. Deploy with both entries. Once ZeroClick signs only with the new kid, drop the old entry:

```python theme={null}
zeroclick = create_async_seller(
    signing_secrets={
        "hsec_k5nq0v7m3d8p": os.environ["ZEROCLICK_SIGNING_SECRET_CURRENT"],
        "hsec_2r8w1t6y4b0s": os.environ["ZEROCLICK_SIGNING_SECRET_PREVIOUS"],
    },
    api_key=os.environ["ZEROCLICK_API_KEY"],
)
```

The key ids are not secret: they appear in every `zc-signature` header. See [keys and secrets](/integrate/keys-and-secrets) for the rotation workflow in the dashboard.

## Outage policy

`allowance_unavailable_policy` applies only when the allowance API gives no answer (a timeout, a transport failure, an error status, or a malformed response), and only after a signature has verified. It never applies to a missing or invalid signature, and an `allowed: false` answer is a normal `402` denial, not an outage.

* `"allow"` (default): serve the request. The decision is an allow with `allowance == "unavailable"`, so you can tell it apart from a confirmed `"allowed"`.
* `"deny"`: return the SDK's `503 {"error":"allowance_unavailable"}` response.
* `"throw"`: the SDK raises the `ZCError` for your application to handle.

Whatever the policy, the SDK calls `on_allowance_unavailable` first with the underlying error. See [errors](/sdks/python/errors) for which error codes the policy covers.

## Lifecycle

Both clients hold an `httpx` client, so a long-lived module-level instance is the normal shape. Create the client at import time and reuse it for every request, as the [quickstart](/sdks/python/quickstart) examples do. For short-lived processes (scripts, workers, tests), close the client explicitly with `close()` / `aclose()`, or use it as a context manager:

<CodeGroup>
  ```python Sync theme={null}
  import os

  from zeroclick_sellers import create_seller

  with create_seller(
      signing_secrets={
          os.environ["ZEROCLICK_SIGNING_SECRET_KID"]: os.environ[
              "ZEROCLICK_SIGNING_SECRET"
          ]
      },
      api_key=os.environ["ZEROCLICK_API_KEY"],
  ) as zeroclick:
      zeroclick.report_usage(
          zc_agent_id="agt_x7f2kq93bh0d",
          idempotency_key="zcreq_8h2m4x0q9k1f_output_tokens",
          service_slug="product-watch",
          meter_slug="output_tokens",
          quantity=4200,
      )
  ```

  ```python Async theme={null}
  import os

  from zeroclick_sellers import create_async_seller

  async with create_async_seller(
      signing_secrets={
          os.environ["ZEROCLICK_SIGNING_SECRET_KID"]: os.environ[
              "ZEROCLICK_SIGNING_SECRET"
          ]
      },
      api_key=os.environ["ZEROCLICK_API_KEY"],
  ) as zeroclick:
      await zeroclick.report_usage(
          zc_agent_id="agt_x7f2kq93bh0d",
          idempotency_key="zcreq_8h2m4x0q9k1f_output_tokens",
          service_slug="product-watch",
          meter_slug="output_tokens",
          quantity=4200,
      )
  ```
</CodeGroup>
