> ## Documentation Index
> Fetch the complete documentation index at: /docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Go SDK errors

> How the Go seller SDK separates decisions from errors: the Error type, error codes, verification failure reasons, and outage classification.

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](/docs/sdks/go/api.md).

## The Error type

```go theme={null}
type Error struct {
	Code      ErrorCode // stable classification; switch on this, not the message
	Operation string    // the SDK call that failed: "new", "guard", "guard_identity",
	                    // "check_allowance", "report_usage"
	Status    int       // HTTP status from the ZeroClick API, or 0 if no response arrived
	Reason    string    // the API's own denial code, when it gave one
	Cause     error     // the underlying transport or decoding failure, if any
}

func (e *Error) Error() string
func (e *Error) Unwrap() error
```

The message renders every populated field, for example:

```text theme={null}
zeroclick: api_status_error during report_usage (status 402) (reason usage_exhausted)
```

`Unwrap` returns `Cause`, so `errors.Is` and `errors.As` see through to the underlying failure.

## Error codes

The core package defines four codes:

| Code | Meaning |
| - | - |
| `malformed_input` | You handed the SDK something invalid: a config `New` refuses, a usage list with a duplicate meter or with both `Quantity` and `MaxQuantity` set, a missing service slug or idempotency key. `Cause` explains what. |
| `api_transport_error` | The call to the ZeroClick API never completed: timeout, DNS failure, connection refused, cancelled context. `Cause` holds the transport error. |
| `api_status_error` | The API answered with a 4xx or 5xx. `Status` holds the code. For usage reports refused with `402`, `404`, or `409`, `Reason` carries the API's machine code: `402` for `access_inactive`, `plan_expired`, or `usage_exhausted`; `409` for `meter_not_priced`; otherwise `404`. |
| `api_response_invalid` | The API answered, but with a body the SDK could not read as the documented shape: unparseable JSON, a missing field, an unrecognized error code on a typed status. |

See [errors](/docs/resources/errors.md) 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`:

| Code | Meaning |
| - | - |
| `invalid_compact_jwe` | The body is not a well-formed five-segment Compact JWE: wrong segment count, an empty segment, an undecodable protected header, or a parse failure. |
| `unsupported_jwe_suite` | The header names an algorithm outside the pinned suite (ECDH-ES+A256KW key management, A256GCM content encryption). |
| `jwe_kid_required` | The protected header carries no `kid`, so no private key can be selected. |
| `invalid_reply_jwk` | The buyer's reply key is malformed: not an EC P-256 public key, unexpected members, or bad coordinates. |
| `private_reply_jwk` | The reply key carries private key material. A protocol violation, refused outright rather than quietly used. |
| `private_key_not_found` | Your resolver does not know the header's `kid`. `Reason` carries the kid. |
| `private_key_resolution_failed` | The private-key lookup itself failed: a vault error, not a missing key. `Reason` carries the kid. |
| `decryption_failed` | The suite and key were right, but decryption failed. `Reason` carries the kid. Also returned for an encrypted request whose plaintext is empty, a known Go JOSE-library limitation; see the [API reference](/docs/sdks/go/api.md). |
| `encryption_failed` | Encrypting the reply to the buyer's key failed. |

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:

| Reason | The request was refused because |
| - | - |
| `missing_signature` | No `zc-signature` header at all, which is typical for traffic that did not come through ZeroClick. |
| `malformed_signature` | The header does not parse as `t=…,kid=…,v1=…`: a missing or duplicate member, a malformed timestamp, or a value that is not 64 lowercase hex characters. |
| `stale_timestamp` | The signature's timestamp drifted more than `ToleranceSeconds` (default 300) from your clock, in either direction. |
| `missing_request_id` | The `zc-request-id` header is absent. |
| `unknown_kid` | Your secrets hold no entry for the signature's key id: commonly a secret rotated away too early, or an environment mismatch. |
| `invalid_signature` | The HMAC did not match: the body, path, method, or ids differ from what ZeroClick signed, or the secret is wrong. A proxy that rewrites the raw request target is a common cause; see [Go SDK middleware](/docs/sdks/go/middleware.md). |

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:

```go theme={null}
var ErrSecretResolution = errors.New("zeroclick: signing secret could not be resolved")
```

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](/docs/concepts/usage-and-allowances.md) 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](/docs/sdks/go/configuration.md)). Everything else fails loudly, because conflating the cases all failed in the same direction: serving unbilled work.

```go theme={null}
func IsAllowanceUnavailable(err error) bool
```

| Condition | Classification |
| - | - |
| Timeout, DNS failure, connection refused (`api_transport_error`) | No answer; the policy applies. |
| `5xx`, `429`, or `408` from the API (`api_status_error`) | No answer: transient, genuinely worth failing open on. The policy applies. |
| A response the SDK cannot read (`api_response_invalid`) | No answer: an answer that cannot be understood is no answer. The policy applies. |
| Any other `4xx` (`api_status_error`): a revoked or wrong-environment API key, a suspended account, an unknown service | **Always an error.** Permanent, never self-heals, and every second of failing open makes the loss larger. |
| `malformed_input` | **Always an error.** Your bug, not an outage. |
| A cancelled context (`errors.Is(err, context.Canceled)`, at any depth) | **Not an outage.** The caller went away; authorizing work because nobody is listening is backwards. `Guard` runs the check on its own timeout regardless, so its real answer stands. |

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:

```go theme={null}
var zcErr *sellers.Error
if errors.As(err, &zcErr) {
	log.Printf("zeroclick %s failed: code=%s status=%d reason=%s",
		zcErr.Operation, zcErr.Code, zcErr.Status, zcErr.Reason)
}
```

`Unwrap` lets `errors.Is` reach the cause:

```go theme={null}
if errors.Is(err, context.DeadlineExceeded) {
	// The check timeout (default 1.5 s) elapsed.
}
if errors.Is(err, sellers.ErrSecretResolution) {
	// The signing-secret lookup failed: alert on infrastructure, not on buyers.
}
```

When you drive the check yourself, classify before deciding. This is exactly what `Guard` does internally with `Config.Policy`:

```go theme={null}
decision, err := seller.CheckAllowance(ctx, zc.RequestID, "product-watch",
	[]sellers.UsageItem{{MeterSlug: "requests", Quantity: 1}})
switch {
case err == nil:
	// A real answer: decision.Allowed, decision.Reason.
case sellers.IsAllowanceUnavailable(err):
	// No answer. Apply your own outage policy.
default:
	// Misconfiguration or a permanent API refusal. Fix it; do not fail open.
	log.Printf("allowance check broken: %v", err)
}
```

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](/docs/integrate/settle-usage.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.