> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zeroclick.ai/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](/sdks/go/api).

## 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](/resources/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`:

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

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](/concepts/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](/sdks/go/configuration)). 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](/integrate/settle-usage).
