Skip to main content
sellers.New(sellers.Config{…}) builds the client. The config requires a usage key (or a both-scopes APIKey) and exactly one of SigningSecrets or Resolve. Everything else has a working default. New returns an error rather than defaulting past a bad config. A seller booting with no signing secret would accept nothing, and failing at startup is far cheaper than discovering that in traffic. New refuses a config that leaves either usage direction uncovered, sets both or neither of SigningSecrets and Resolve, contains an empty secret, or names an unknown Policy.
The client is safe for concurrent use; build it once at startup.

Usage keys

string
A single API key (zc_…) carrying both the usage:read and usage:write scopes. It backfills whichever scoped key below is not set.
string
A key with the usage:read scope, used for allowance checks (Guard, CheckAllowance). Falls back to APIKey when empty.
string
A key with the usage:write scope, used for usage reporting (ReportUsage), which often runs in a separate process such as a worker. Falls back to APIKey when empty.
Provide APIKey alone, or provide the scoped keys and omit it. If either direction ends up with no key, New returns an error naming the missing side.
See keys and secrets for how the scopes map to the ZeroClick API.

Service and plan

string
The service these endpoints sell, as configured in ZeroClick. Required by the Meter and Identify middleware, which read it from the client. Guard takes a service slug per call instead, so a multi-service backend can share one client.
string
Optional. When set, the SDK offers this plan on the 402 payment_required response, steering the buyer toward it.

Signing secrets

Provide exactly one of SigningSecrets or Resolve.
map[string]string
A static map from key id (kid, format hsec_…) to secret value (zcsec_…). The kid appears in every request’s zc-signature header, and the verifier uses it to select the secret. The map keys must match the key ids shown in your dashboard exactly. The client copies the map at construction; a later mutation of your map does not change which requests the client accepts.
ResolveSigningSecret
A per-kid lookup for secrets that live in a vault or database rather than a static map:
For a kid you do not recognize, return ok == false; the SDK refuses the request with a 401. Return an error only when the lookup itself failed (the vault was unreachable). It surfaces as an error to your code, not as a refusal, so an infrastructure fault is never silently read as a forged request. Because the client consults Resolve on each request, rotated secrets take effect without a restart.

Loading from the environment

SecretsFromEnv() builds the map from the ZEROCLICK_SIGNING_SECRETS environment variable, which holds <kid>:<secret> pairs, comma-separated. The format carries the key id, so there is nothing to hardcode:
ParseSigningSecrets(raw string) parses the same format from any source, for sellers reading the value from a secret manager instead of the environment. Each entry splits on the first colon only, because secrets may contain colons. Error messages never echo the value, so a config mistake does not put a secret in your logs.

Rotation

Hold both secrets while requests signed with either are still in flight, then drop the old one:
Or, in the one-variable form:
Each request names its kid, so the verifier always picks the right secret. Rotation requires no downtime.

Request body limit

int64
default:"10 MiB"
Caps what a guarded endpoint will buffer. Zero means DefaultMaxBodyBytes (10 MiB).The signature covers the whole body, so the SDK must hold the whole body in memory to verify it. An attacker does not need a valid signature to make you buffer. This is a memory bound against unauthenticated callers, not a politeness limit. The SDK refuses a request over the cap with 413 {"error":"request_body_too_large"} before verification.

Allowance outage policy

If the allowance API gives no answer (a timeout, a transport failure, a 5xx), Policy decides what happens. This is separate from the API answering no, which is always a 402, and from your own misconfiguration, which is always an error. See Go SDK errors for the exact classification.
UnavailablePolicy
default:"PolicyAllow"
The guard consults the policy only after a signature verifies, so a fail-open allowance policy never becomes a fail-open signature policy. But every legitimate buyer request is signed, so PolicyAllow bounds free work to real traffic rather than to a small subset. Set Logger or OnAllowanceUnavailable, because a fail-open that is also silent is invisible unbilled traffic.
time.Duration
default:"1500ms"
Bounds each allowance check (and each ReportUsage call). It sits in front of every billable request, so it is deliberately short: under the outage policy, a slow answer is worse than no answer.
func(error)
Called with the underlying error before the policy applies, so you can alarm on an outage you are choosing to absorb.

Logging

interface{ Printf(string, ...any) }
Receives conditions the SDK absorbs rather than surfaces: an allowance outage served under the allow policy, a guard failure the middleware turns into a 500, a usage report that failed after the response was delivered. *log.Logger satisfies it, so Logger: log.Default() works. Nil discards these conditions, which makes unbilled traffic invisible; set it.

Signature verification

int
default:"300"
How far a signature’s timestamp may drift from your clock, in either direction, before the SDK refuses the request as stale. See the signature spec for what the timestamp protects against.

Plumbing

string
default:"https://api.zeroclick.io"
Base URL for the allowance and usage endpoints. Leave it unset in production.
*http.Client
The client used for ZeroClick API calls. Defaults to a plain &http.Client{}; per-call timeouts come from CheckTimeout.
func() time.Time
Clock override so tests can pin time. Defaults to time.Now.

Defaults at a glance