Skip to main content
The Go SDK splits outcomes into decisions and errors, and the split is load-bearing.
  • 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-return Response. 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.Error values with a stable machine-readable code.
This page covers the error side; the decision flow is on the API reference.

The Error type

The message renders every populated field, for example:
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

The jwe 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:
Test for it with 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:
When you drive the check yourself, classify before deciding. This is exactly what Guard does internally with Config.Policy:
A failed 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.