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

> Which operations return decisions and which raise ZCError, every error code and its context, and how to handle usage API failures.

The SDK draws a hard line between expected events and errors. A request with a bad signature or an exhausted allowance is an expected event, not a programming mistake. `guard`, `guard_identity`, and `verify_request` return it as a decision that carries the exact response to send back. `ZCError` is reserved for what the caller got wrong (malformed input) or what the environment did (the ZeroClick API being unreachable or answering nonsense).

## Decisions or exceptions, by operation

| Operation                                 | Returns a decision for                                                                                           | Raises ZCError for                                                                                 |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `guard`                                   | Bad signature (`401`), allowance denied (`402`), allowance unavailable under the `"allow"` and `"deny"` policies | Malformed usage input, a failing secret resolver, allowance unavailable under the `"throw"` policy |
| `guard_identity`                          | Bad signature (`401`), missing buyer identity (`402`)                                                            | A failing secret resolver                                                                          |
| `verify_request`                          | Every signature failure (`401`)                                                                                  | Misconfigured secret sources, a non-`ZcRequest` argument, a failing secret resolver                |
| `check_allowance`                         | An allowed or denied `AllowanceDecision`                                                                         | Every API failure (the outage policy applies only inside `guard`)                                  |
| `report_usage`                            | A duplicate replay (`duplicate=True` is a success)                                                               | Every API failure, including rejected reports                                                      |
| `create_seller`, `create_async_seller`    | None                                                                                                             | Invalid configuration, at construction                                                             |
| `ZcRequest`, `UsageItem`, `SyncUsageItem` | None                                                                                                             | Invalid fields, at construction                                                                    |
| `decrypt_request`, `encrypt_response`     | None                                                                                                             | Every encryption failure                                                                           |

[GuardResult](/sdks/python/api#guardresult) documents the deny reasons that ride on decisions; this page covers the exceptions.

## ZCError

```python theme={null}
class ZCError(Exception):
    code: ZCErrorCode        # stable machine code, for example "api_status_error"
    operation: str           # the operation that raised, for example "check_allowance"
    context: dict[str, Any]  # {"operation": ..., plus code-specific keys}
```

`ZCError(code, message=None, operation=..., **context)` carries a stable machine `code`, the `operation` that raised it, and a `context` dict. The dict holds the operation plus code-specific keys such as `status`, `reason`, `kid`, or `meter_slug`. `str(error)` is a human-readable message; branch on `code` and `context`, never on the message text.

`is_zc_error(error, code=None)` returns `True` when `error` is a `ZCError`, optionally of a specific code. It is useful at boundaries that catch broadly:

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

try:
    process_job()
except Exception as error:
    if is_zc_error(error, "api_transport_error"):
        ...  # the ZeroClick API was unreachable
    raise
```

## Error codes

`ZCErrorCode` is a `Literal` of five values:

| Code                               | Meaning                                                                                                                                                                                                                                  | Context keys                                 |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| `api_response_invalid`             | The ZeroClick API answered, but the body is not the shape the SDK requires: invalid JSON, missing fields, or an unrecognized denial reason.                                                                                              | `status`, sometimes `reason`                 |
| `api_status_error`                 | The ZeroClick API returned a status of 400 or above.                                                                                                                                                                                     | `status`; `reason` on rejected usage reports |
| `api_transport_error`              | The request never completed: timeout, connection failure, DNS. The default check timeout is 1.5 seconds, and the SDK never retries.                                                                                                      | None                                         |
| `malformed_input`                  | You passed the SDK something invalid: at construction (missing usage keys, no signing secret, a bad policy value) or at call time (duplicate meters, both `quantity` and `max_quantity`, a non-bytes body). The message names the field. | varies                                       |
| `signing_secret_resolution_failed` | Your `resolve_signing_secret` callable raised, or returned something other than a non-empty string. An unknown `kid` does not raise: it denies with the `401` response.                                                                  | `kid`                                        |

## The allowance outage policy

Inside `guard`, after a signature has verified, the codes `api_transport_error`, `api_status_error`, and `api_response_invalid` all mean the same thing: the allowance API gave no usable answer. The SDK routes these three, and only these, to the client's `allowance_unavailable_policy` instead of propagating them: `"allow"` serves the request with `allowance == "unavailable"`, `"deny"` returns the SDK's `503 {"error":"allowance_unavailable"}`, and `"throw"` re-raises the error for your application. The `on_allowance_unavailable` hook sees the error first, whatever the policy.

The policy never applies to a missing or invalid signature, and an `allowed: false` answer is not an outage: it is a `402` denial. Outside `guard`, in `check_allowance` and `report_usage`, these codes always raise. See [configuration](/sdks/python/configuration#outage-policy) for choosing a policy.

## Handling api\_status\_error

`api_status_error` carries the HTTP status in `error.context["status"]`. When the API rejects a `report_usage` call with a typed status (`402`, `404`, or `409`), its machine reason also rides in `error.context["reason"]`: the report endpoint answers `402` for `access_inactive`, `plan_expired`, and `usage_exhausted`, `409` for `meter_not_priced`, and `404` otherwise. The [errors reference](/resources/errors) covers the platform-wide error model.

```python theme={null}
import logging

from zeroclick_sellers import ZCError

logger = logging.getLogger("zeroclick")

try:
    result = zeroclick.report_usage(
        zc_agent_id="agt_x7f2kq93bh0d",
        idempotency_key="zcreq_8h2m4x0q9k1f_output_tokens",
        service_slug="product-watch",
        meter_slug="output_tokens",
        quantity=4200,
    )
except ZCError as error:
    if error.code != "api_status_error":
        raise
    status = error.context["status"]      # for example 402
    reason = error.context.get("reason")  # for example "usage_exhausted"
    logger.warning("usage report rejected: %s %s", status, reason)
else:
    if result.duplicate:
        logger.info("already reported; stored event replayed")
```

Because the SDK never retries, retrying is yours to schedule. It is also safe: reporting is idempotent per service on `idempotency_key`, so replaying the same report can never double-bill.

## Encryption error codes

`decrypt_request` and `encrypt_response` raise `ZCError` with JWE-specific string codes that sit outside the five-value `ZCErrorCode` literal. Compare `error.code` directly rather than through `is_zc_error`'s typed `code` parameter:

| Code                            | Meaning                                                                                       |
| ------------------------------- | --------------------------------------------------------------------------------------------- |
| `invalid_compact_jwe`           | The request body is not a valid Compact JWE.                                                  |
| `unsupported_jwe_suite`         | The JWE does not use `ECDH-ES+A256KW` with `A256GCM`, the only accepted suite.                |
| `jwe_kid_required`              | The JWE protected header has no key id.                                                       |
| `invalid_reply_jwk`             | The reply key is not a public P-256 key.                                                      |
| `private_reply_jwk`             | The reply key carries private key material. The SDK rejects it outright rather than using it. |
| `private_key_not_found`         | `resolve_private_key` returned `None` for the JWE's `kid`.                                    |
| `private_key_resolution_failed` | `resolve_private_key` raised.                                                                 |
| `decryption_failed`             | The SDK could not decrypt the encrypted request.                                              |
| `encryption_failed`             | The SDK could not encrypt the response to the reply key.                                      |

`private_key_not_found`, `private_key_resolution_failed`, and `decryption_failed` carry the JWE's `kid` in `error.context`.
