> ## 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 quickstart

> Install zeroclick-sellers, pick the sync or async client, and guard a FastAPI, Flask, or Django route.

The [`zeroclick-sellers`](https://pypi.org/project/zeroclick-sellers/) package implements the ZeroClick billing guard for Python backends: it verifies that each request came from ZeroClick, checks the buyer's [allowance](/concepts/usage-and-allowances) before you do the work, returns the refusal responses ZeroClick expects, and settles or reports what was used. This page wires the guard into FastAPI, Flask, and Django. The [integration overview](/integrate/overview) describes the contract it implements.

## Install

```sh theme={null}
pip install zeroclick-sellers
```

The package requires Python 3.10 or later and is fully typed (it ships `py.typed`, so type checkers see every signature). Its HTTP calls go through `httpx`.

<Info>
  You need three values from the [dashboard](https://dashboard.zeroclick.io): your signing secret (`zcsec_…`), its key id (`hsec_…`), and an API key (`zc_…`) with the `usage:read` and `usage:write` scopes. See [keys and secrets](/integrate/keys-and-secrets).

  ```sh theme={null}
  ZEROCLICK_SIGNING_SECRET_KID=hsec_k5nq0v7m3d8p
  ZEROCLICK_SIGNING_SECRET=zcsec_…
  ZEROCLICK_API_KEY=zc_…
  ```
</Info>

## Pick the right client

| Your framework            | Client                | Guard call                   |
| ------------------------- | --------------------- | ---------------------------- |
| FastAPI, Starlette (ASGI) | `create_async_seller` | `await zeroclick.guard(...)` |
| Flask, Django (WSGI)      | `create_seller`       | `zeroclick.guard(...)`       |

Use the async client in ASGI apps. The blocking client would stall the event loop during every allowance check. The two clients accept the same options and make the same decisions; they differ only in how they perform IO.

## Guard a FastAPI route

```python theme={null}
import os
from fastapi import FastAPI, Request
from fastapi.responses import Response

from zeroclick_sellers import SyncUsageItem, UsageItem, ZcResponse, create_async_seller
from zeroclick_sellers.adapters import zc_request_from_asgi_scope

zeroclick = create_async_seller(
    signing_secrets={
        os.environ["ZEROCLICK_SIGNING_SECRET_KID"]: os.environ[
            "ZEROCLICK_SIGNING_SECRET"
        ]
    },
    api_key=os.environ["ZEROCLICK_API_KEY"],
)
app = FastAPI()


def to_fastapi(response: ZcResponse) -> Response:
    return Response(
        content=response.body,
        status_code=response.status,
        headers=dict(response.headers),
    )


@app.post("/v1/product-watch")
async def product_watch(request: Request) -> Response:
    zc_request = zc_request_from_asgi_scope(request.scope, await request.body())

    decision = await zeroclick.guard(
        zc_request,
        service_slug="product-watch",
        usage=[UsageItem(meter_slug="requests", quantity=1)],
    )
    if decision.action == "deny":
        return to_fastapi(decision.response)

    result = do_the_work(owner=decision.context.zc_agent_id)

    return to_fastapi(
        zeroclick.with_usage(
            ZcResponse.json(result),
            [
                SyncUsageItem(
                    service_slug="product-watch", meter_slug="requests", quantity=1
                )
            ],
        )
    )
```

The handler is four moves:

1. **Adapt.** `zc_request_from_asgi_scope` turns the framework request into a `ZcRequest` built from the raw path and raw body bytes.
2. **Guard.** `guard` verifies the `zc-signature` header before it calls the allowance API, and returns a decision, not an exception.
3. **Deny.** A deny decision carries the exact response to return: `401` for a bad signature, the `402 payment_required` body ZeroClick converts into a payment challenge, or `503` under a fail-closed outage policy. An allow decision carries the verified context: `decision.context.zc_agent_id` identifies the buyer.
4. **Settle.** `with_usage` stamps the `zc-usage` response header with the actual usage. ZeroClick records that usage and strips the header before the agent sees the response.

## Flask and Django

The handler keeps the same shape: `create_seller`, no `await`, and the WSGI adapter in place of the ASGI one.

<CodeGroup>
  ```python Flask theme={null}
  import os
  from flask import Flask, Response, request

  from zeroclick_sellers import SyncUsageItem, UsageItem, ZcResponse, create_seller
  from zeroclick_sellers.adapters import zc_request_from_wsgi_environ

  zeroclick = create_seller(
      signing_secrets={
          os.environ["ZEROCLICK_SIGNING_SECRET_KID"]: os.environ[
              "ZEROCLICK_SIGNING_SECRET"
          ]
      },
      api_key=os.environ["ZEROCLICK_API_KEY"],
  )
  app = Flask(__name__)


  def to_flask(response: ZcResponse) -> Response:
      return Response(
          response=response.body,
          status=response.status,
          headers=dict(response.headers),
      )


  @app.post("/v1/product-watch")
  def product_watch() -> Response:
      # request.get_data() returns the raw body without consuming it for later
      # handlers; never use request.json here, which would re-serialise.
      zc_request = zc_request_from_wsgi_environ(request.environ, request.get_data())

      decision = zeroclick.guard(
          zc_request,
          service_slug="product-watch",
          usage=[UsageItem(meter_slug="requests", quantity=1)],
      )
      if decision.action == "deny":
          return to_flask(decision.response)

      result = do_the_work(owner=decision.context.zc_agent_id)

      return to_flask(
          zeroclick.with_usage(
              ZcResponse.json(result),
              [
                  SyncUsageItem(
                      service_slug="product-watch", meter_slug="requests", quantity=1
                  )
              ],
          )
      )
  ```

  ```python Django theme={null}
  import os
  from django.http import HttpRequest, HttpResponse
  from django.urls import path

  from zeroclick_sellers import SyncUsageItem, UsageItem, ZcResponse, create_seller
  from zeroclick_sellers.adapters import zc_request_from_wsgi_environ

  zeroclick = create_seller(
      signing_secrets={
          os.environ["ZEROCLICK_SIGNING_SECRET_KID"]: os.environ[
              "ZEROCLICK_SIGNING_SECRET"
          ]
      },
      api_key=os.environ["ZEROCLICK_API_KEY"],
  )


  def to_django(response: ZcResponse) -> HttpResponse:
      django_response = HttpResponse(
          content=response.body,
          status=response.status,
          content_type=response.headers.get("content-type", "application/json"),
      )
      for key, value in response.headers.items():
          if key.lower() != "content-type":
              django_response[key] = value
      return django_response


  def product_watch(request: HttpRequest) -> HttpResponse:
      # request.body is the raw bytes. request.META is the WSGI environ, which
      # is where the raw target lives; request.path is already decoded.
      zc_request = zc_request_from_wsgi_environ(request.META, request.body)

      decision = zeroclick.guard(
          zc_request,
          service_slug="product-watch",
          usage=[UsageItem(meter_slug="requests", quantity=1)],
      )
      if decision.action == "deny":
          return to_django(decision.response)

      result = do_the_work(owner=decision.context.zc_agent_id)

      return to_django(
          zeroclick.with_usage(
              ZcResponse.json(result),
              [
                  SyncUsageItem(
                      service_slug="product-watch", meter_slug="requests", quantity=1
                  )
              ],
          )
      )


  urlpatterns = [path("v1/product-watch", product_watch)]
  ```
</CodeGroup>

## Why the adapters exist

The signature covers the request target exactly as ZeroClick sent it, so `ZcRequest.path_and_query` must be the **raw, percent-encoded** path and query. Every framework hands you a decoded one. These are the observed values for `GET /v1/items/a%2Fb%20c`:

|                 | Decoded (unusable)                  | Raw (correct)                               |
| --------------- | ----------------------------------- | ------------------------------------------- |
| ASGI / uvicorn  | `scope["path"]` → `/v1/items/a/b c` | `scope["raw_path"]` → `/v1/items/a%2Fb%20c` |
| WSGI / werkzeug | `PATH_INFO` → `/v1/items/a/b c`     | `RAW_URI` → `/v1/items/a%2Fb%20c?…`         |

Using the decoded path produces a different canonical string and fails verification. The adapters handle this. They also cover a spec difference between the two interfaces: ASGI's `raw_path` excludes the query string (the adapter appends `scope["query_string"]`), while WSGI's `RAW_URI` and `REQUEST_URI` already include it.

The body has the same rule: pass the raw bytes exactly as received, before any framework parses or re-serializes them. Use `await request.body()` in FastAPI, `request.get_data()` in Flask, and `request.body` in Django.

<Warning>
  On WSGI, if the server sets neither `RAW_URI` nor `REQUEST_URI`, the SDK cannot recover an encoded separator. WSGI decodes `%2F` to `/` before the SDK runs, and nothing can tell it from a literal `/`. gunicorn, werkzeug, uWSGI, and nginx all set one of them.
</Warning>

## Test the guard

The guard must deny a plain request with no ZeroClick headers. Only signed requests from ZeroClick reach your handler:

```sh theme={null}
curl -i -X POST http://localhost:8000/v1/product-watch
# HTTP/1.1 401
# {"error":"invalid_zeroclick_signature"}
```

The [quickstart](/quickstart) covers the rest of the loop: deploying the guarded route and running the dashboard's API setup verification against it.

## Next steps

<Columns cols={2}>
  <Card title="Configuration" icon="settings" href="/sdks/python/configuration">
    Every constructor option: secrets, split usage keys, outage policy, timeouts.
  </Card>

  <Card title="API reference" icon="braces" href="/sdks/python/api">
    Every public export, from guard to the encryption helpers.
  </Card>

  <Card title="Errors" icon="triangle-alert" href="/sdks/python/errors">
    Decisions versus exceptions, and every ZCError code.
  </Card>

  <Card title="Charge up to a maximum" icon="gauge" href="/integrate/charge-up-to-a-maximum">
    Bill work you cannot size up front, like output tokens.
  </Card>
</Columns>
