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

# zc-signature specification

> The byte-level specification of the zc-signature request header: grammar, canonical string, verification algorithm, failure taxonomy, and a worked test vector.

Every request ZeroClick forwards to a seller's API carries a `zc-signature` header: an HMAC-SHA256 over the request, keyed by the seller's signing secret. This page is the normative specification for implementers writing their own verifier. If you use a [seller SDK](/sdks/overview), it already implements every rule here. See [verify requests](/integrate/verify-requests). For the surrounding flow, see the [REST walkthrough](/integrate/rest-walkthrough).

## Header grammar

The value is a comma-separated list of `key=value` members:

```text theme={null}
zc-signature: t=<unix seconds>,kid=<signing secret key id>,v1=<64 lowercase hex>
```

Sandbox traffic appends `sb=1`:

```text theme={null}
zc-signature: t=1760000000,kid=hsec_k5nq0v7m3d8p,v1=25e2005f3229a60c8accc0d92576fc89f590533de7cd0ba880e820777ee6faa7,sb=1
```

| Member | Value                                                                                                                             | Validation                                                        |
| ------ | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `t`    | Signing timestamp in unix seconds.                                                                                                | 1 to 20 ASCII digits.                                             |
| `kid`  | Id of the signing secret that keyed the HMAC (`hsec_…`). Selects the right secret during [rotation](/integrate/keys-and-secrets). | Non-empty, no surrounding whitespace.                             |
| `v1`   | The hex-encoded HMAC-SHA256 signature.                                                                                            | Exactly 64 lowercase hex characters.                              |
| `sb`   | `1` on sandbox traffic.                                                                                                           | Informational; the canonical string is byte-identical either way. |

Parsing rules:

* Split the value on `,`. Each member is `key=value`. Trim whitespace around a key or value.
* Reject a member with no `=` or an empty key as malformed.
* **Reject duplicate member keys as malformed.** Do not skip this rule.
* **Ignore unrecognized member keys.** ZeroClick extends this header additively (`sb=1` is one such member), so a parser that demands exactly three members fails all sandbox traffic.
* After parsing, `t`, `kid`, and `v1` must all be present and pass their validations; anything else is malformed.

## Canonical string

The signed bytes are six fields joined by `\n` (LF, `0x0A`), with no trailing newline:

```text theme={null}
<t>\n<METHOD>\n<path and query>\n<sha256 of body>\n<zc-request-id>\n<zc-agent-id or empty>
```

1. **Timestamp**: the header's `t` value byte-for-byte as received. Never re-parse and re-format it.
2. **Method**: the HTTP method, uppercased.
3. **Request target**: the raw percent-encoded path plus query exactly as sent, with no scheme or host. `/v1/items/a%2Fb%20c` stays `/v1/items/a%2Fb%20c`. Never the decoded path: frameworks commonly hand you one, and it hashes to a different string. Preserve query order and repeated keys verbatim.
4. **Body digest**: lowercase hex SHA-256 of the exact body bytes as received. An empty body hashes to `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`. Re-serialized JSON will not match: the digest covers bytes, not meaning. Hash an encrypted body as the ciphertext it arrived as. Verify before you decrypt.
5. **Request id**: the `zc-request-id` header value.
6. **Agent id**: the `zc-agent-id` header value, or the empty string when the header is absent. An absent header and a present-but-empty one sign identically; both mean a signed anonymous probe, which is valid.

The signature is `HMAC-SHA256(secret, canonical string)`, lowercase hex, with the secret and canonical string both UTF-8 encoded. The secret is the one whose id equals `kid`.

## Verification algorithm

1. Read `zc-signature`. Absent → `missing_signature`.
2. Parse it per the grammar above. Any violation → `malformed_signature`.
3. Parse `t` as an integer. If `|now − t|` exceeds the tolerance (300 seconds by default, applied in both directions) → `stale_timestamp`.
4. Read `zc-request-id`. Absent or empty → `missing_request_id`.
5. Read `zc-agent-id`; treat absent as the empty string.
6. Resolve the secret for `kid`. No secret with that id → `unknown_kid`. Run steps 1 to 5 first so an unauthenticated request never reaches your key store. A lookup that *fails* (a vault being unreachable) is an operational error, not a refusal.
7. Compute the expected HMAC over the canonical string. Compare it to `v1` **in constant time** (`hmac.compare_digest`, `crypto.timingSafeEqual`, `hmac.Equal`). Mismatch → `invalid_signature`.
8. Otherwise the request is verified: trust `zc-request-id` and `zc-agent-id`.

## Failure taxonomy

| Reason                | Trigger                                                                                                        |
| --------------------- | -------------------------------------------------------------------------------------------------------------- |
| `missing_signature`   | No `zc-signature` header.                                                                                      |
| `malformed_signature` | Grammar violation: missing or duplicate member, non-numeric or over-long timestamp, `v1` not 64 lowercase hex. |
| `stale_timestamp`     | `t` outside the clock tolerance, past or future.                                                               |
| `missing_request_id`  | No `zc-request-id` header.                                                                                     |
| `unknown_kid`         | No configured signing secret with the header's `kid`.                                                          |
| `invalid_signature`   | The HMAC comparison failed: wrong secret, tampered body, altered target or headers.                            |

Every reason maps to the same seller response, one `401`:

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

The body never says which check failed. Keep the reason in your own logs.

## Worked test vector

This vector comes from the cross-SDK vector suite (the `identified_buyer` case), which every seller SDK executes. The full suite ships with the SDKs as `signing-vectors.json`. The ids and secret below are test fixtures. Production values look like `hsec_…`, `zcreq_…`, and `agt_…`.

Signing secret:

```text theme={null}
kid:    zcsec_vector_1
secret: zcsec_vector_secret_do_not_use_in_production
```

Request:

```text theme={null}
POST /v1/product-watch
zc-request-id: zcreq_vector
zc-agent-id: zcagent_vector
zc-signature: t=1760000000,kid=zcsec_vector_1,v1=25e2005f3229a60c8accc0d92576fc89f590533de7cd0ba880e820777ee6faa7
```

Body (exactly these 36 bytes, no trailing newline):

```json theme={null}
{"productId":"sku_42","window":"7d"}
```

Its SHA-256 is `b07172b06310bf8dc76e9427aa33af6c61894b2a2abc1524a980c2b6bb7bd35e`, giving the canonical string:

```text theme={null}
1760000000
POST
/v1/product-watch
b07172b06310bf8dc76e9427aa33af6c61894b2a2abc1524a980c2b6bb7bd35e
zcreq_vector
zcagent_vector
```

Expected result: with the verifier's clock pinned to `1760000000` and tolerance `300`, the HMAC-SHA256 of the canonical string with the secret equals the header's `v1`. Verification succeeds with request id `zcreq_vector` and agent id `zcagent_vector`.

Your implementation must also pass two checks. With `zc-agent-id` absent, the same request verifies only against `v1=326b303be6b1ca89598b3b572961bce3b999c00cbf8333a71cc1c7f9f3d6566c` (the empty-string sixth field). Your verifier must reject a header that carries `v1` twice as malformed, even when both copies are correct.
