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, it already implements every rule here. See verify requests. For the surrounding flow, see the REST walkthrough.
Header grammar
The value is a comma-separated list ofkey=value members:
sb=1:
Parsing rules:
- Split the value on
,. Each member iskey=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=1is one such member), so a parser that demands exactly three members fails all sandbox traffic. - After parsing,
t,kid, andv1must 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:
- Timestamp: the header’s
tvalue byte-for-byte as received. Never re-parse and re-format it. - Method: the HTTP method, uppercased.
- Request target: the raw percent-encoded path plus query exactly as sent, with no scheme or host.
/v1/items/a%2Fb%20cstays/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. - 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. - Request id: the
zc-request-idheader value. - Agent id: the
zc-agent-idheader 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.
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
- Read
zc-signature. Absent →missing_signature. - Parse it per the grammar above. Any violation →
malformed_signature. - Parse
tas an integer. If|now − t|exceeds the tolerance (300 seconds by default, applied in both directions) →stale_timestamp. - Read
zc-request-id. Absent or empty →missing_request_id. - Read
zc-agent-id; treat absent as the empty string. - 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. - Compute the expected HMAC over the canonical string. Compare it to
v1in constant time (hmac.compare_digest,crypto.timingSafeEqual,hmac.Equal). Mismatch →invalid_signature. - Otherwise the request is verified: trust
zc-request-idandzc-agent-id.
Failure taxonomy
Every reason maps to the same seller response, one
401:
Worked test vector
This vector comes from the cross-SDK vector suite (theidentified_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:
b07172b06310bf8dc76e9427aa33af6c61894b2a2abc1524a980c2b6bb7bd35e, giving the canonical string:
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.