> ## 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 API reference

> Every public export of zeroclick-sellers: client methods, decision and data types, response builders, adapters, module defaults, and encryption helpers.

Everything on this page imports from `zeroclick_sellers`, except the framework adapters, which live in `zeroclick_sellers.adapters`. See [configuration](/sdks/python/configuration) for constructor options and [errors](/sdks/python/errors) for error codes.

`create_seller(**options)` returns a `SellerClient` (blocking, for WSGI); `create_async_seller(**options)` returns an `AsyncSellerClient` (non-blocking, for ASGI). The SDK exports both classes for type annotations. The methods below have identical signatures and semantics on both clients; on `AsyncSellerClient`, `guard`, `check_allowance`, and `report_usage` are coroutines you `await`. `guard_identity` and `verify_request` make no network call, so they are synchronous on both clients.

## Guard methods

### guard

```python theme={null}
def guard(
    request: ZcRequest,
    *,
    service_slug: str,
    usage: Iterable[UsageItem],
    plan_slug: str | None = None,
) -> GuardResult
```

The whole billing guard in one call. It verifies the `zc-signature` header first, and only then checks the declared usage against the buyer's allowance with `POST /v1/usage/check`. The SDK never consults the allowance API for an unverified request. The result is a decision, not an exception:

* **Deny, `401`**: a missing, malformed, stale, or invalid signature.
* **Deny, `402`**: the allowance API answered `allowed: false`. The response is the exact `payment_required` body ZeroClick converts into a payment challenge, built from `service_slug`, `usage`, and `plan_slug`.
* **Deny, `503`**: the allowance API gave no answer and the client's policy is `"deny"`.
* **Allow**: carries the verified `context` and an `allowance` of `"allowed"` or `"unavailable"`.

`usage` must contain at least one `UsageItem` and no duplicate meters; the allowance API accepts at most 20 items per check. When set, `plan_slug` appears in a denial's `402` body as `planSlug`. `guard` raises `ZCError` only for malformed input, a failing secret resolver, or an allowance outage under the `"throw"` policy.

```python theme={null}
decision = await zeroclick.guard(  # no await on the sync client
    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)
```

### guard\_identity

```python theme={null}
def guard_identity(request: ZcRequest, *, service_slug: str) -> GuardResult
```

Guards a [free, identity-scoped endpoint](/integrate/free-and-identity-endpoints): one that costs nothing but must know which buyer is calling. It verifies the signature exactly like `guard`, makes no allowance call (so it is synchronous on both clients), and:

* denies a bad signature with the `401` response,
* denies a verified request without a buyer identity (reason `identity_required`) with the `402 payment_required` body carrying `usage: []`, which ZeroClick answers with a free identity challenge,
* allows an identified buyer with `allowance == "not_required"`, and in that case `decision.context.zc_agent_id` is always non-empty.

```python theme={null}
decision = zeroclick.guard_identity(zc_request, service_slug="product-watch")
if decision.action == "deny":
    return to_fastapi(decision.response)

owner = decision.context.zc_agent_id
```

### verify\_request

```python theme={null}
def verify_request(request: ZcRequest) -> VerifyResult
```

Signature verification alone, using the client's secrets, tolerance, and clock. This is the first half of `guard`, for when you want to run the allowance check separately or not at all. It returns `VerifyOk` with the proven context, or `VerifyFailure` with a `reason` and the ready-to-return `401` response. The failure reasons are `missing_signature`, `malformed_signature`, `stale_timestamp`, `missing_request_id`, `unknown_kid`, and `invalid_signature`; every one maps to the same `401 {"error":"invalid_zeroclick_signature"}` body, so the reason is for your logs, not the caller. The HMAC comparison is constant-time.

```python theme={null}
verification = zeroclick.verify_request(zc_request)
if not verification.ok:
    return to_fastapi(verification.response)

context = verification.context  # zc_request_id, zc_agent_id, timestamp, kid
```

`verify_request` is also a standalone function for use without a client. It takes the same request plus its own configuration. Provide exactly one of `signing_secrets` or `resolve_signing_secret`, and optional `tolerance_seconds` (default 300) and `clock`:

```python theme={null}
from zeroclick_sellers import verify_request

verification = verify_request(
    zc_request,
    signing_secrets={"hsec_k5nq0v7m3d8p": os.environ["ZEROCLICK_SIGNING_SECRET"]},
)
```

## Usage API methods

### check\_allowance

```python theme={null}
def check_allowance(
    *, zc_request_id: str, service_slug: str, usage: Iterable[UsageItem]
) -> AllowanceDecision
```

The [allowance check](/integrate/check-allowances) alone, without verification or response-building. Use it for a second check partway through a multi-stage request, or when you have already verified. `zc_request_id` must be the id from the verified request's context. Checking records nothing and burns no credit. Unlike `guard`, any API failure raises `ZCError`: the outage policy applies only inside `guard`.

```python theme={null}
decision = await zeroclick.check_allowance(
    zc_request_id=context.zc_request_id,
    service_slug="product-watch",
    usage=[UsageItem(meter_slug="output_tokens", max_quantity=100_000)],
)
if not decision.allowed:
    ...  # decision.reason, for example "usage_exhausted"
```

### report\_usage

```python theme={null}
def report_usage(
    *,
    zc_agent_id: str,
    idempotency_key: str,
    service_slug: str,
    meter_slug: str,
    quantity: int,
    occurred_at: str | None = None,
) -> ReportUsageResult
```

Reports usage asynchronously with `POST /v1/usage`, for work that finishes after the response has gone out. This is the alternative to [settling with the `zc-usage` header](/integrate/settle-usage). All arguments are keyword-only. `quantity` is an integer of at least 1 (at most 2,147,483,647); `occurred_at` is an optional ISO 8601 timestamp.

Reporting is idempotent per service on `idempotency_key`. Derive the key from stable facts (`zcreq_8h2m4x0q9k1f_output_tokens`, not a random value) so a retry of your own job cannot double-bill. `report_usage` does not generate idempotency keys and does not retry. A result with `duplicate=True` means the key already landed and the API returned the stored event. That is a success, not an error. A rejected report raises `ZCError`; see [errors](/sdks/python/errors) for reading the status and reason.

```python theme={null}
result = 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,
)
result.recorded   # True
result.duplicate  # True when this key was already reported: still a success
```

## Response builders

### payment\_required

```python theme={null}
def payment_required(
    *, service_slug: str, usage: Iterable[UsageItem], plan_slug: str | None = None
) -> ZcResponse
```

Builds the exact `402` refusal ZeroClick converts into a payment challenge, the same response a `guard` denial carries. Use it to deny for business reasons of your own. An empty `usage` is meaningful, not a mistake: it is the free identity-scoped refusal, which ZeroClick answers with a \$0 identity challenge. It is available as a static method on both clients and as a top-level import.

```python theme={null}
response = zeroclick.payment_required(
    service_slug="product-watch",
    usage=[UsageItem(meter_slug="requests", quantity=1)],
)
response.status       # 402
response.json_body()  # {"error": "payment_required", "serviceSlug": "product-watch",
                      #  "usage": [{"meterSlug": "requests", "quantity": 1}]}
```

### with\_usage

```python theme={null}
def with_usage(response: ZcResponse, usage: Iterable[SyncUsageItem]) -> ZcResponse
```

Returns a copy of the response with the `zc-usage` header attached: the synchronous settlement path. Attach it to `2xx` responses only; ZeroClick records the usage and strips the header before the agent sees the response. It is available as a static method on both clients and as a top-level import.

```python theme={null}
stamped = zeroclick.with_usage(
    ZcResponse.json({"watching": True}),
    [SyncUsageItem(service_slug="product-watch", meter_slug="requests", quantity=1)],
)
stamped.headers["zc-usage"]
# '[{"serviceSlug":"product-watch","meterSlug":"requests","quantity":1}]'
```

### Other builders

* `usage_header(usage: Iterable[SyncUsageItem]) -> str`: the `zc-usage` header value alone, for stamping a framework response directly instead of going through `ZcResponse`.
* `invalid_zeroclick_signature() -> ZcResponse`: the `401 {"error":"invalid_zeroclick_signature"}` response. Deny decisions already carry it; the export is for manual flows.
* `allowance_unavailable() -> ZcResponse`: the `503 {"error":"allowance_unavailable"}` response the fail-closed outage policy uses.

## Request and response types

All SDK types are frozen dataclasses: immutable after construction, validated in `__post_init__`, and comparable by value.

### ZcRequest

```python theme={null}
@dataclass(frozen=True)
class ZcRequest:
    method: str
    path_and_query: str
    headers: Mapping[str, str] = field(default_factory=dict)
    body: bytes = b""
```

The framework-neutral view of an inbound request that every verification entry point consumes. Two rules matter:

* `body` must be the raw bytes exactly as received. Passing anything else raises `ZCError` (`malformed_input`, "body must be bytes; decode nothing before verifying").
* `path_and_query` must be the raw, percent-encoded request target. Decoded paths fail verification; the [adapters](#framework-adapters) recover the raw target for you.

The constructor lowercases header names, and `request.header(name)` looks up case-insensitively:

```python theme={null}
zc_request = ZcRequest(
    method="POST",
    path_and_query="/v1/product-watch?fast=1",
    headers={"ZC-Request-Id": "zcreq_8h2m4x0q9k1f"},
    body=b'{"productId":"prod_9d4k"}',
)
zc_request.header("zc-request-id")  # "zcreq_8h2m4x0q9k1f"
```

### ZcResponse

```python theme={null}
@dataclass(frozen=True)
class ZcResponse:
    status: int
    body: bytes
    headers: Mapping[str, str] = field(default_factory=dict)
```

The framework-neutral response the SDK hands back; your adapter turns it into a real framework response. `ZcResponse.json(payload, *, status=200, headers=None)` builds one with compact JSON and a `content-type: application/json` header; `response.json_body()` parses the body back.

```python theme={null}
response = ZcResponse.json({"watching": True})
response.status   # 200
response.body     # b'{"watching":true}'
```

## Decision types

### GuardResult

```python theme={null}
@dataclass(frozen=True)
class Allow:
    context: ZeroClickContext
    allowance: Literal["allowed", "unavailable", "not_required"]
    action: Literal["allow"] = "allow"

@dataclass(frozen=True)
class Deny:
    reason: str
    response: ZcResponse
    action: Literal["deny"] = "deny"

GuardResult = Allow | Deny
```

Discriminate on `action` (or match on the class). `Allow.allowance` tells you how the guard cleared the request:

* `"allowed"`: the allowance API confirmed coverage.
* `"unavailable"`: the allowance API gave no answer and the `"allow"` policy let the request through.
* `"not_required"`: `guard_identity` allowed the request and made no allowance check.

`Deny.reason` is one of the six verification failures (response `401`); one of the seven allowance denial reasons (`service_not_found`, `access_not_found`, `access_inactive`, `plan_expired`, `meter_not_found`, `meter_not_priced`, `usage_exhausted`) or `allowance_denied` when the API denies without a reason (response `402`); `allowance_unavailable` under the fail-closed policy (response `503`); or `identity_required` from `guard_identity` (response `402`). `Deny.response` is always ready to return as-is.

### VerifyResult

```python theme={null}
@dataclass(frozen=True)
class VerifyOk:
    context: ZeroClickContext
    ok: Literal[True] = True

@dataclass(frozen=True)
class VerifyFailure:
    reason: VerifyFailureReason
    response: ZcResponse
    ok: Literal[False] = False

VerifyResult = VerifyOk | VerifyFailure
```

`verify_request` returns this type. Discriminate on `ok`.

### ZeroClickContext

```python theme={null}
@dataclass(frozen=True)
class ZeroClickContext:
    zc_request_id: str
    zc_agent_id: str | None
    timestamp: int
    kid: str
    zc_anonymous_id: str | None = None
    zc_buyer_id: str | None = None
```

Proven facts about a verified request: the correlation id (`zcreq_…`), the id of the agent that made the call (`agt_…`), the signature's timestamp in Unix seconds, and the key id that signed it.

`zc_anonymous_id` repeats `zc_agent_id` under the name that will eventually replace `zc-agent-id`. `zc_buyer_id` (`byr_…`) is the buyer that agent belongs to, and is `None` for an anonymous agent; unlike the fields above it is not covered by the signature. See [agents and access](/concepts/agents-and-access).

<Warning>
  A verified request without a buyer identity is a **signed anonymous probe**. ZeroClick forwards one to price a pay-as-you-go `402`, so it is valid, expected traffic. Test for it with truthiness, not `is None`: an absent `zc-agent-id` header gives `None`, but a present-but-empty one gives `""`, and both mean the same thing.

  ```python theme={null}
  if not decision.context.zc_agent_id:
      ...  # anonymous
  ```

  `guard_identity` already handles both.
</Warning>

### AllowanceDecision

```python theme={null}
@dataclass(frozen=True)
class AllowanceDecision:
    allowed: bool
    reason: str | None
```

`check_allowance` returns this type. `reason` is `None` when allowed, otherwise one of the seven allowance denial reasons.

### ReportUsageResult

```python theme={null}
@dataclass(frozen=True)
class ReportUsageResult:
    recorded: bool
    duplicate: bool
    usage_event: Mapping[str, Any]
```

`report_usage` returns this type. `usage_event` is the API's recorded event: `id`, `zcAgentId`, `zcRequestId`, `idempotencyKey`, `meterSlug`, `quantity`, `totalCostUsd`, `source`, and `occurredAt`.

## Usage items

### UsageItem

```python theme={null}
@dataclass(frozen=True)
class UsageItem:
    meter_slug: str
    quantity: int | None = None
    max_quantity: int | None = None
```

What a request will be charged for, used by `guard`, `check_allowance`, and `payment_required`. Declare at most one of `quantity` (a known amount) or `max_quantity` (a [ceiling](/integrate/charge-up-to-a-maximum) for work you cannot size up front). Declaring both raises `ZCError`. Declaring neither defers to the meter's configured per-request ceiling; if the meter has none, the API denies the check with `meter_not_priced`. Quantities must be positive integers.

```python theme={null}
usage = [
    UsageItem(meter_slug="requests", quantity=1),
    UsageItem(meter_slug="output_tokens", max_quantity=100_000),
]
```

The buyer authorizes up to a ceiling and settles at the actual amount you report, so a ceiling never overcharges.

### SyncUsageItem

```python theme={null}
@dataclass(frozen=True)
class SyncUsageItem:
    service_slug: str
    meter_slug: str
    quantity: int
```

Actual usage settled alongside a successful response, used by `with_usage` and `usage_header`. All fields are required; `quantity` must be a positive integer.

## Standalone verification

`canonical_string` builds the exact string ZeroClick signs: six fields joined by newlines. You need it only to debug a verification mismatch or implement verification elsewhere; the [signature spec](/integrate/signature-spec) specifies the format.

```python theme={null}
def canonical_string(
    *,
    timestamp: str,
    method: str,
    path_and_query: str,
    body: bytes,
    zc_request_id: str,
    zc_agent_id: str | None,
) -> str
```

## Framework adapters

```python theme={null}
from zeroclick_sellers.adapters import (
    asgi_path_and_query,
    wsgi_path_and_query,
    zc_request_from_asgi_scope,
    zc_request_from_wsgi_environ,
)
```

* `zc_request_from_asgi_scope(scope, body) -> ZcRequest`: builds a request from an ASGI scope and the already-read raw body (FastAPI, Starlette).
* `zc_request_from_wsgi_environ(environ, body) -> ZcRequest`: the same for a WSGI environ (Flask, Django).
* `asgi_path_and_query(scope) -> str` and `wsgi_path_and_query(environ) -> str`: the raw request-target recovery alone, for custom integrations.

The adapters' whole job is recovering the raw, percent-encoded request target that the signature covers; see [why the adapters exist](/sdks/python/quickstart#why-the-adapters-exist). In both cases `body` must be the raw bytes, read before any framework parses or re-serializes them.

## Module defaults

| Constant                        | Value                                  |
| ------------------------------- | -------------------------------------- |
| `DEFAULT_API_BASE_URL`          | `"https://api.zeroclick.io"`           |
| `DEFAULT_TOLERANCE_SECONDS`     | `300`                                  |
| `DEFAULT_CHECK_TIMEOUT_SECONDS` | `1.5`                                  |
| `COMPACT_JWE_ALG`               | `"ECDH-ES+A256KW"`                     |
| `COMPACT_JWE_ENC`               | `"A256GCM"`                            |
| `REPLY_JWK_PARAM`               | `"https://zeroclick.io/jwe/reply-jwk"` |

## Encryption

For services that opt into body encryption, the request body arrives as a Compact JWE and the reply goes back the same way. The signature covers the **ciphertext**, so `guard` runs first and unchanged; decrypt only after an allow decision.

```python theme={null}
def decrypt_request(
    body: bytes | str, *, resolve_private_key: Callable[[str], Any]
) -> DecryptedEnvelope

def encrypt_response(response: ZcResponse, envelope: DecryptedEnvelope) -> ZcResponse
```

`resolve_private_key` receives the `kid` from the JWE protected header and returns that private key (a JWK mapping or a `joserfc` `ECKey`), or `None` if it is unknown. `DecryptedEnvelope` carries the `plaintext` bytes plus what `encrypt_response` needs to encrypt the reply (`protected_header`, `cty`, `reply_jwk`). `encrypt_response` returns the response unchanged when the request carried no reply key, so the same handler serves encrypted and plaintext buyers.

```python theme={null}
import json

from zeroclick_sellers import decrypt_request, encrypt_response

raw_body = await request.body()
zc_request = zc_request_from_asgi_scope(request.scope, raw_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)

envelope = decrypt_request(raw_body, resolve_private_key=lookup_private_key)
payload = json.loads(envelope.plaintext)

# ... do the work, build the response, stamp usage with with_usage ...

return to_fastapi(encrypt_response(stamped_response, envelope))
```

The suite is fixed at `ECDH-ES+A256KW` key management with `A256GCM` content encryption; the SDK rejects anything else. If a reply key arrives with private key material, the SDK rejects it outright rather than using it. Failures raise `ZCError` with JWE-specific string codes.

## Errors

`ZCError` and `is_zc_error(error, code=None)` are the last two exports. Guard outcomes are decisions; `ZCError` is reserved for malformed input and ZeroClick API failures. The [errors page](/sdks/python/errors) covers every code and how to handle them.
