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

# REST walkthrough

> Implement the ZeroClick seller contract with plain HTTP in any language: verify the signature, check the allowance, serve, and settle usage.

The [seller SDKs](/sdks/overview) implement the billing guard for TypeScript, Python, and Go. If your backend runs on anything else, or you want zero dependencies, the contract is small enough to implement directly: verify one request header, call one HTTPS endpoint before the work, and settle usage after it. This page walks the whole contract with your language's standard library and `curl`.

<Info>
  You need your seller's signing secret (`zcsec_…`) with its key id (`hsec_…`), a usage read key (`zc_…`, scope `usage:read`), and a usage write key (`zc_…`, scope `usage:write`). One API key with both usage scopes also works. See [keys and secrets](/integrate/keys-and-secrets).
</Info>

The examples use the seller Acme with service `product-watch` and meters `requests` and `output_tokens`.

<Steps>
  <Step title="Read the three request headers">
    Every request ZeroClick forwards to your API carries three headers:

    | Header          | Example                                   | Meaning                                                                                                                                        |
    | --------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
    | `zc-request-id` | `zcreq_8h2m4x0q9k1f`                      | Correlation id for this request. Echo it in the allowance check.                                                                               |
    | `zc-agent-id`   | `agt_x7f2kq93bh0d`                        | The buying agent. Absent on signed anonymous probes, which ZeroClick uses to price pay-as-you-go challenges. This state is expected and valid. |
    | `zc-signature`  | `t=1785258000,kid=hsec_k5nq0v7m3d8p,v1=…` | Proves the request came from ZeroClick and was not altered.                                                                                    |

    ZeroClick strips any `zc-*` headers the agent sent before forwarding and sets its own. Once the signature verifies, these values are ZeroClick's. Capture the raw body bytes and the raw request target (path plus query, still percent-encoded) before your framework decodes them; both feed verification. The [headers reference](/resources/headers) lists every header on the wire.
  </Step>

  <Step title="Verify the signature">
    `zc-signature` is an HMAC-SHA256 over six newline-joined fields, keyed by the signing secret whose id matches the header's `kid`. The fields are the timestamp, the uppercased method, the raw path and query, the SHA-256 of the body bytes, the request id, and the agent id (or empty string). Verify the timestamp is within 300 seconds of your clock. Compare the digest in constant time. The [zc-signature specification](/integrate/signature-spec) defines every rule byte by byte, with test vectors.

    A compact verifier in standard-library Python:

    ```python theme={null}
    import hashlib, hmac, os, re, time

    SIGNING_SECRETS = {"hsec_k5nq0v7m3d8p": os.environ["ZEROCLICK_SIGNING_SECRET"]}
    TOLERANCE_SECONDS = 300

    def verify(method, raw_path_and_query, raw_body, headers):
        """Returns the verified context, or None -> respond 401."""
        header = headers.get("zc-signature")
        if header is None:
            return None
        members = {}
        for entry in header.split(","):
            key, sep, value = entry.partition("=")
            key = key.strip()
            if not sep or not key or key in members:  # malformed or duplicate member
                return None
            members[key] = value.strip()              # unknown members are ignored
        t, kid, v1 = members.get("t"), members.get("kid"), members.get("v1")
        if not (t and kid and v1):
            return None
        if not re.fullmatch(r"\d{1,20}", t) or not re.fullmatch(r"[0-9a-f]{64}", v1):
            return None
        if abs(int(time.time()) - int(t)) > TOLERANCE_SECONDS:
            return None
        request_id = headers.get("zc-request-id")
        if not request_id:
            return None
        agent_id = headers.get("zc-agent-id") or ""
        secret = SIGNING_SECRETS.get(kid)
        if secret is None:
            return None
        canonical = "\n".join([
            t,
            method.upper(),
            raw_path_and_query,                       # as sent, never URL-decoded
            hashlib.sha256(raw_body).hexdigest(),
            request_id,
            agent_id,
        ])
        expected = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
        if not hmac.compare_digest(expected, v1):
            return None
        return {"zc_request_id": request_id, "zc_agent_id": agent_id or None}
    ```

    <Warning>
      Sign over the request target exactly as it arrived. Most frameworks hand you a decoded path (`%2F` becomes `/`), and the decoded form hashes to a different string. Read the raw request line. Hash the exact body bytes, not re-serialized JSON.
    </Warning>
  </Step>

  <Step title="Check the allowance">
    Before you do the work, ask ZeroClick whether this agent's plan covers it. The check is bound to that request, so send the `zc-request-id` you received:

    ```sh theme={null}
    curl -sS https://api.zeroclick.io/v1/usage/check \
      -H "Authorization: Bearer $ZEROCLICK_USAGE_READ_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "zcRequestId": "zcreq_8h2m4x0q9k1f",
        "serviceSlug": "product-watch",
        "usage": [{ "meterSlug": "requests", "quantity": 1 }]
      }'
    ```

    `usage` takes 1 to 20 items, one per meter, no duplicates. Each item declares `quantity` when the amount is known, or `maxQuantity` as a ceiling when it is not (see [charge up to a maximum](/integrate/charge-up-to-a-maximum)). An item that declares neither gates against the meter's configured default ceiling. An item may not declare both.

    The response is one of two shapes:

    ```json theme={null}
    { "allowed": true, "reason": null }
    ```

    ```json theme={null}
    { "allowed": false, "reason": "usage_exhausted" }
    ```

    `reason` is one of `service_not_found`, `access_not_found`, `access_inactive`, `plan_expired`, `meter_not_found`, `meter_not_priced`, or `usage_exhausted`. See [check allowances](/integrate/check-allowances). Checking records nothing and burns no credit, so repeating a check is harmless.

    Two failure modes need different handling. No answer at all (a timeout, a connection failure, a 5xx) means you choose a policy: fail open and serve, or fail closed with the `503` below. The SDKs default to fail open, with a 1.5 second timeout. A 4xx from the API (a revoked key, a key missing `usage:read`) is a configuration error on your side. Fix it. Never fail open on it.
  </Step>

  <Step title="Return the right refusal">
    Your API returns three refusals, each with an exact body ZeroClick recognizes.

    A missing, malformed, stale, or invalid signature gets a `401`. The body is the same for every signature failure. Keep the specific reason in your logs:

    ```json theme={null}
    { "error": "invalid_zeroclick_signature" }
    ```

    `"allowed": false` gets a `402` naming what the request would cost. ZeroClick prices it and answers the agent with a payment challenge, so return the refusal before you do the work:

    ```json theme={null}
    {
      "error": "payment_required",
      "serviceSlug": "product-watch",
      "usage": [{ "meterSlug": "requests", "quantity": 1 }]
    }
    ```

    An empty `"usage": []` marks a free, buyer-scoped endpoint that needs to know its caller. See [free and identity endpoints](/integrate/free-and-identity-endpoints).

    An unreachable allowance API under a fail-closed policy gets a `503`:

    ```json theme={null}
    { "error": "allowance_unavailable" }
    ```
  </Step>

  <Step title="Serve and settle">
    On success, do the work. Settle usage synchronously by setting the `zc-usage` response header: a JSON array of what the response actually consumed.

    ```text theme={null}
    zc-usage: [{"serviceSlug":"product-watch","meterSlug":"requests","quantity":1}]
    ```

    Set it on 2xx responses only. A refusal must never carry usage. ZeroClick reads the header, records the usage, settles payment, and strips the header before the agent sees the response. This is the preferred settlement path; see [settle usage](/integrate/settle-usage).
  </Step>

  <Step title="Report async usage">
    When you know the amount only after the response has gone out (a background job, a stream's final token count), report it to the usage API with the write key:

    ```sh theme={null}
    curl -sS https://api.zeroclick.io/v1/usage \
      -H "Authorization: Bearer $ZEROCLICK_USAGE_WRITE_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "zcAgentId": "agt_x7f2kq93bh0d",
        "idempotencyKey": "zcreq_8h2m4x0q9k1f_output_tokens",
        "serviceSlug": "product-watch",
        "meterSlug": "output_tokens",
        "quantity": 1834
      }'
    ```

    `quantity` is an integer from 1 to 2,147,483,647. `occurredAt` (ISO 8601) is optional; when omitted, the server sets it to now.

    ```json theme={null}
    {
      "recorded": true,
      "duplicate": false,
      "usageEvent": {
        "id": "use_2q9d5k7x0mfb",
        "zcAgentId": "agt_x7f2kq93bh0d",
        "zcRequestId": null,
        "idempotencyKey": "zcreq_8h2m4x0q9k1f_output_tokens",
        "meterSlug": "output_tokens",
        "quantity": 1834,
        "totalCostUsd": "0.001834",
        "source": "async",
        "occurredAt": "2026-07-28T17:03:41.512Z"
      }
    }
    ```

    Reports are idempotent per service on `idempotencyKey`. Derive the key from what it bills (`<zcRequestId>_<meterSlug>`, as above). Never generate it randomly: a random key turns a retried report into a second charge, while a derived key makes the retry a replay. A replay returns `"duplicate": true` with the stored event; that is a success, not an error.

    Denials use the [error envelope](/resources/errors): `402` for `access_inactive`, `plan_expired`, and `usage_exhausted`; `409` for `meter_not_priced`; `404` for `service_not_found`, `access_not_found`, and `meter_not_found`.
  </Step>
</Steps>

## Ordering rules

Three rules hold the contract together:

1. **Verify before everything.** Nothing runs on an unverified request: not the allowance check, not your handler. The signature is the only thing that makes `zc-request-id` and `zc-agent-id` trustworthy.
2. **Never bill a non-2xx.** `zc-usage` belongs on successful responses only. The `402` refusal comes before the work, not after it.
3. **Echo the received `zc-request-id` into the check.** The id ties the challenge, the payment, and the usage record to one request; an invented id fails the check.

<Columns cols={2}>
  <Card title="zc-signature specification" icon="file-key" href="/integrate/signature-spec">
    The byte-level spec: grammar, canonical string, and test vectors.
  </Card>

  <Card title="Errors" icon="octagon-alert" href="/resources/errors">
    Every error code the platform returns, with statuses.
  </Card>
</Columns>
