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

# Verify requests

> Prove each forwarded request came from ZeroClick: what the SDKs check, why anonymous probes are valid, and the raw-path rule that trips up reverse proxies.

Your upstream URL is reachable from the internet; the `zc-signature` header is the gate. Verification proves a request came through ZeroClick (payment handled, buyer resolved) and that nothing changed in transit. Every SDK guard verifies before it does anything else. A request that fails must get `401 {"error":"invalid_zeroclick_signature"}`, never your handler.

## What the SDKs check

Verification runs entirely inside your process, with no network call:

1. Parse `zc-signature` (`t=<unix seconds>,kid=<hsec_…>,v1=<64 lowercase hex>`). Ignore unknown fields: ZeroClick appends fields additively, such as `,sb=1` on sandbox traffic.
2. Reject timestamps outside the clock tolerance: 300 seconds by default, in either direction.
3. Look up the signing secret by the signature's `kid`. An unknown kid is a refusal, so [rotation](/integrate/keys-and-secrets#rotate-a-signing-secret) means holding both kids.
4. Recompute the HMAC-SHA256 over the canonical string and compare in constant time. The canonical string is the timestamp, the uppercased method, the raw path and query, the SHA-256 of the raw body bytes, `zc-request-id`, and `zc-agent-id` (empty string when absent).

The [signature spec](/integrate/signature-spec) specifies the exact bytes. Failures are decisions, not exceptions: the guard returns a deny carrying the `401`, with a machine-readable reason (`missing_signature`, `malformed_signature`, `stale_timestamp`, `missing_request_id`, `unknown_kid`, or `invalid_signature`) for your logs. The `401` body itself says nothing about why the request failed.

## Signed anonymous probes are valid

A verified request with no `zc-agent-id` (or an empty one) is a **signed anonymous probe**, and it is how pay-as-you-go pricing works. For an unpaid call, ZeroClick forwards a probe to your API. Your guard denies it with the `402 payment_required` usage body. ZeroClick re-prices that refusal into the one priced challenge the agent pays. Probes are expected traffic, not an attack, and only the paid retry carries the agent id.

<CodeGroup>
  ```ts TypeScript theme={null}
  if (!decision.context.zcAgentId) {
    // Anonymous probe: zcAgentId is null.
  }
  ```

  ```python Python theme={null}
  # Test with truthiness, not `is None`: an absent zc-agent-id header gives
  # None, but a present-but-empty one gives ""; both mean anonymous.
  if not decision.context.zc_agent_id:
      ...
  ```

  ```go Go theme={null}
  if zc.AgentID == "" {
  	// Anonymous probe. Absent and empty headers sign identically.
  }
  ```
</CodeGroup>

## The signature covers the raw path

The canonical string contains the **raw, percent-encoded** path and query exactly as ZeroClick sent it. Every web framework hands you a decoded path, and a decoded path hashes differently. For `GET /v1/items/a%2Fb%20c`:

| Source                                                      | Value                 | Verifies |
| ----------------------------------------------------------- | --------------------- | -------- |
| ASGI `scope["path"]`, WSGI `PATH_INFO`, Go `r.URL.Path`     | `/v1/items/a/b c`     | No       |
| ASGI `scope["raw_path"]`, WSGI `RAW_URI`, Go `r.RequestURI` | `/v1/items/a%2Fb%20c` | Yes      |

The SDKs handle this at the boundary. The TypeScript SDK reads the web-standard `Request` URL, which preserves encoding. The Python adapters (`zc_request_from_asgi_scope`, `zc_request_from_wsgi_environ`) read the raw target and handle the difference: ASGI's `raw_path` excludes the query string, while WSGI's `RAW_URI` includes it. Go's `FromHTTP` reads `r.RequestURI`, which Go leaves untouched.

<Warning>
  A reverse proxy, ingress controller, or managed load balancer in front of your server may normalize the path (`%2F` becoming `/`) before your server sees it. If that happens, every request with an encoded path segment fails verification. If you route through nginx, an ingress, or a load balancer, confirm it passes the request target through unmodified.
</Warning>

On WSGI, if the server sets neither `RAW_URI` nor `REQUEST_URI`, an encoded separator cannot be recovered: the server decodes `%2F` before the SDK runs. gunicorn, werkzeug, uWSGI, and nginx all set one of them.

## Verify the raw body bytes

The body digest covers the bytes exactly as they arrived, before any parsing or transformation. Adapt framework requests without re-encoding the body. If your service opts into encrypted bodies, verify first: the signature covers the ciphertext, so decryption comes after the guard. In Go, the `Meter` and `Identify` middleware read the body for verification and then restore `r.Body`, so your handler reads it normally.

## Lower-level verification

`guard` is the right entry point for a billable route. When verification and allowance checking live in separate middleware layers (verify once at the edge, check allowances per route), use the verify-only entry point. In TypeScript, it reads a clone of the `Request`, so the body stays available to your handler. In Python and Go, you hand it the raw bytes you already hold.

<CodeGroup>
  ```ts TypeScript theme={null}
  const result = await zeroClick.verifyRequest(request);
  if (!result.ok) return result.response; // the 401; result.reason for logs

  // result.context: { zcRequestId, zcAgentId, timestamp, kid }
  console.log(result.context.zcRequestId);
  ```

  ```python Python theme={null}
  result = zeroclick.verify_request(zc_request)
  if not result.ok:
      return to_fastapi(result.response)  # the 401; result.reason for logs

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

  ```go Go theme={null}
  result, err := seller.Verify(sellers.FromHTTP(r, body))
  if err != nil {
  	// Secret resolution failed: a vault outage, not a bad request.
  	http.Error(w, `{"error":"internal_error"}`, http.StatusInternalServerError)
  	return
  }
  if !result.OK {
  	result.Response.WriteTo(w) // the 401; result.Reason for logs
  	return
  }
  // result.Context: RequestID, AgentID, Timestamp, KID
  ```
</CodeGroup>

The verified context carries `zcRequestId` and `zcAgentId`: everything the [allowance check](/integrate/check-allowances) and [usage settlement](/integrate/settle-usage) need.

## Who called, and who they belong to

`zcAgentId` is always the agent that made this call. When ZeroClick knows the owner behind that agent, the request also carries `zc-buyer-id` (`byr_…`), exposed by the TypeScript and Python SDKs as `zcBuyerId` / `zc_buyer_id`. One buyer can hold several agents, and they all share the buyer's entitlements, so the buyer id is what stays stable when a customer rotates or replaces an agent.

Key durable per-customer records on the buyer id when it is present and per-caller state on the agent id. An absent buyer id means an anonymous agent: still identified and billable, just not yet attached to an owner. Note that the signature covers `zc-agent-id` and not `zc-buyer-id`; the [headers reference](/resources/headers) has the full matrix.
