- Decisions are values. An unsigned request, a bad signature, a buyer who cannot pay: these are expected events on a public endpoint, not faults in your program. They come back as results (
VerifyResult,GuardResult,AllowanceDecision) carrying the ready-to-returnResponse. Nothing to catch, nothing to recover. - Errors are reserved for what you got wrong (malformed input, bad configuration) and what the environment did (the ZeroClick API unreachable or answering nonsense). They are
*sellers.Errorvalues with a stable machine-readable code.
The Error type
Unwrap returns Cause, so errors.Is and errors.As see through to the underlying failure.
Error codes
The core package defines four codes:
See errors for the control-plane error envelope these map from.
jwe error codes
Thejwe subpackage (encrypted request bodies) adds its own codes, on the same *sellers.Error type with Operation set to decrypt_request or encrypt_response:
The SDK reports a nil private-key resolver as the core
malformed_input.
Verification refusals are decisions, not errors
When a signature does not verify,Verify and Guard return a deny decision, not an error. Reason carries a FailureReason value saying why:
Every one of these surfaces to the caller as the same
401 {"error":"invalid_zeroclick_signature"} response. The body deliberately says nothing about why. The specific reason rides on VerifyResult.Reason and GuardResult.Reason for your logs.
One verification path does produce an error rather than a refusal: a signing-secret resolver fault. If your Resolve function fails (a vault unreachable) or resolves an empty secret, Verify returns an error wrapping ErrSecretResolution. An infrastructure fault must not be silently read as a forged request:
errors.Is(err, sellers.ErrSecretResolution). An unknown kid, by contrast, is an ordinary unknown_kid refusal.
Allowance denials are decisions too
allowed: false from the allowance API is always a 402 decision, never an error: Guard returns a deny result carrying the priced payment_required response, with Reason set to the denial code. The SDK exports the codes as the Denial* constants: service_not_found, access_not_found, access_inactive, plan_expired, meter_not_found, meter_not_priced, usage_exhausted. See usage and allowances for what each means. The SDK still honors a denial whose reason it does not recognize: the decision stands, and the unfamiliar reason passes through for you to log.
Outage classification
IsAllowanceUnavailable answers one question: did the allowance API give no answer? Only that condition is subject to the configured outage Policy (see configuration). Everything else fails loudly, because conflating the cases all failed in the same direction: serving unbilled work.
A readable decision with an unfamiliar reason never reaches this classification at all: the SDK honors it as a
402, per the previous section.
The guard consults the policy only after a signature verifies, so a fail-open allowance policy never becomes a fail-open signature policy.
Patterns
Inspect any SDK error by code:Unwrap lets errors.Is reach the cause:
Guard does internally with Config.Policy:
ReportUsage deserves the same care in the other direction: the buyer already has their result. Log the error with its Reason, and retry later with the same derived idempotency key rather than failing the delivered request. A replay is a harmless Duplicate: true. See settle usage.