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.
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.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.
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 ofSigningSecrets 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: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.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.