> ## 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.

# Ruby SDK API

> Every public method and value type in zeroclick-sellers.

## `ZeroClick::Sellers.create`

Builds a client. See [configuration](/sdks/ruby/configuration) for every option.

```ruby theme={null}
SELLER = ZeroClick::Sellers.create(
  api_key: ENV.fetch("ZEROCLICK_API_KEY"),
  signing_secrets: ZeroClick::Sellers.secrets_from_env
)
```

Invalid configuration raises `ZeroClick::Sellers::Error` with code `malformed_input` at construction, not at the first request.

## `#guard`

Verifies the request, then checks the buyer can pay before you do the work.

```ruby theme={null}
decision = SELLER.guard(
  request,
  service_slug: "extractor",
  usage: [ZeroClick::Sellers::UsageItem.new(meter_slug: "requests", quantity: 1)],
  plan_slug: nil
)
```

Returns `Allow` or `Deny` — never raises for an unpayable or unsigned request, because both are expected events.

```ruby theme={null}
if decision.allow?
  decision.context   # ZeroClickContext
  decision.allowance # "allowed" or "unavailable"
else
  decision.reason    # "usage_exhausted", "invalid_signature", …
  decision.response  # ready to return as-is
end
```

## `#guard_identity`

Guards a free endpoint that must still know the caller. Makes no network call.

```ruby theme={null}
decision = SELLER.guard_identity(request, service_slug: "extractor")
```

`Allow#allowance` is `"not_required"`. A signed anonymous probe is denied with reason `identity_required` and a `402` carrying empty usage.

## `#verify_request`

Signature verification alone, with no allowance check.

```ruby theme={null}
result = SELLER.verify_request(request)
result.ok?      # true or false
result.context  # on success
result.reason   # on failure: missing_signature, malformed_signature,
                # stale_timestamp, missing_request_id, unknown_kid, invalid_signature
result.response # on failure: the 401
```

A verified request whose `zc_agent_id` is `nil` is a signed anonymous probe — valid, not a failure.

## `#check_allowance`

The allowance check on its own, without verification.

```ruby theme={null}
decision = SELLER.check_allowance(
  zc_request_id: zc.zc_request_id,
  service_slug: "extractor",
  usage: [ZeroClick::Sellers::UsageItem.new(meter_slug: "requests", quantity: 1)]
)

decision.allowed? # true or false
decision.reason   # a usage denial reason, or nil
```

Unlike `guard`, this raises on an API failure rather than applying the outage policy — it has no verified context to fall back to.

## `#report_usage`

Reports usage asynchronously, after the work is done.

```ruby theme={null}
result = SELLER.report_usage(
  zc_agent_id: zc.zc_agent_id,
  idempotency_key: "extract:#{zc.zc_request_id}",
  service_slug: "extractor",
  meter_slug: "tokens",
  quantity: 4_120,
  occurred_at: nil
)

result.recorded?   # always true on success
result.duplicate?  # true when this key was already recorded
result.usage_event # the recorded event
```

<Warning>
  The idempotency key is **yours** to derive, and must be stable for the request — derive it from `zc_request_id`. The SDK never invents one and never retries on your behalf, because a retry under a changed key double-bills.
</Warning>

A replay of the same key returns `duplicate? == true`. That is a success, not a failure.

## Response helpers

```ruby theme={null}
SELLER.payment_required(
  service_slug: "extractor",
  usage: [ZeroClick::Sellers::UsageItem.new(meter_slug: "requests", quantity: 1)],
  plan_slug: "pro"
)

SELLER.with_usage(response, [
  ZeroClick::Sellers::SyncUsageItem.new(service_slug: "extractor", meter_slug: "requests", quantity: 1)
])
```

`ZeroClick::Sellers::Responses` exposes the same functions plus `invalid_zeroclick_signature`, `allowance_unavailable` and `usage_header`.

## Value types

### `UsageItem`

What a request will be charged for.

```ruby theme={null}
ZeroClick::Sellers::UsageItem.new(meter_slug: "requests", quantity: 1)
ZeroClick::Sellers::UsageItem.new(meter_slug: "tokens", max_quantity: 10_000)
ZeroClick::Sellers::UsageItem.new(meter_slug: "requests")
```

Give at most one of `quantity` or `max_quantity`; neither defers to the meter's configured per-request ceiling. Declaring both raises.

A fixed `quantity` lets ZeroClick offer the `exact` x402 scheme. A `max_quantity` forces `upto`, which fewer clients can pay — prefer a fixed charge and report variable work afterwards.

The ceiling never overcharges: ZeroClick authorises up to it and bills what you settle. But it does **gate the buyer's authorisation**, which is easy to miss. A ceiling of 50 units at `$0.002` plus a `$0.01` request charge makes every call reserve `$0.11`, and a buyer whose per-call cap is below that is refused before any work happens — a client capped at `$0.10` is simply turned away. The unspent remainder never leaves their wallet, but an ambitious ceiling still prices out small callers, so keep it near a realistic worst case.

`Middleware::Meter` accepts **only** items with an explicit `quantity`, because it settles usage automatically. The other two forms are fine for `guard`, which only asks whether the buyer could pay.

### `SyncUsageItem`

Actual usage, reported alongside a successful response via the `zc-usage` header.

```ruby theme={null}
ZeroClick::Sellers::SyncUsageItem.new(service_slug: "extractor", meter_slug: "requests", quantity: 1)
```

### `Request`

A framework-neutral inbound request. Build one from a Rack env with `Middleware.request_from_env(env)`, or directly:

```ruby theme={null}
ZeroClick::Sellers::Request.new(
  method: "POST",
  path_and_query: "/v1/extract?format=json",
  headers: { "zc-signature" => "…" },
  body: raw_bytes
)
```

`path_and_query` must be the **raw, percent-encoded** target, and `body` the raw bytes. Decode nothing before verifying — the signature covers exactly what arrived.

### `Response`

```ruby theme={null}
response.status  # 402
response.headers # { "content-type" => "application/json" }
response.body    # the JSON string
response.json_body
```

### `ZeroClickContext`

```ruby theme={null}
zc.zc_request_id
zc.zc_agent_id
zc.zc_buyer_id
zc.zc_anonymous_id
zc.kid
zc.timestamp
```

## Module helpers

```ruby theme={null}
ZeroClick::Sellers.secrets_from_env      # parses ZEROCLICK_SIGNING_SECRETS
ZeroClick::Sellers.configure(**options)  # store config for .seller
ZeroClick::Sellers.seller                # the lazily built process-wide client
ZeroClick::Sellers.error?(e, "api_status_error")
ZeroClick::Sellers::VERSION
```

## Encrypted request bodies

A buyer can [encrypt its request body end to end](/concepts/end-to-end-encryption) with your public key. The buyer can also request an encrypted reply.

The helpers load only after this separate require:

```ruby theme={null}
require "zeroclick/sellers/encryption"
```

Nothing new is added to your dependency graph. The gem implements the whole suite on the standard library — OpenSSL supplies P-256 ECDH, AES key wrap and AES-256-GCM, and the one missing piece, the NIST SP 800-56A Concat KDF, is a short SHA-256 loop.

```ruby theme={null}
envelope = ZeroClick::Sellers.decrypt_request(
  raw_body,
  resolve_private_key: ->(kid) { my_private_jwks[kid] }
)
envelope.plaintext   # the original request body
envelope.reply_jwk   # non-nil when the buyer asked for an encrypted reply

# Returns the response unchanged when no reply key was sent.
ZeroClick::Sellers.encrypt_response(response, envelope)
```

Verify the signature **before** decrypting: the signature covers the ciphertext as it arrived on the wire, not the plaintext. The cipher suite is pinned — `ECDH-ES+A256KW` key management, `A256GCM` content encryption — so a sender cannot negotiate something weaker.

Two ordering rules are worth stating outright, because both are silently wrong in a way that still returns 200:

* **Attach usage before encrypting.** `zc-usage` is a header, which is what lets an encrypted call settle through a proxy that cannot read the body.
* **Encrypt refusals too.** A buyer who asked for confidentiality does not stop wanting it because the answer is a 4xx.

<Note>
  Unlike the Go SDK, there is no empty-plaintext limitation here: an encrypted request whose plaintext is empty decrypts normally, matching the TypeScript and Python SDKs.
</Note>

The [errors page](/sdks/ruby/errors) lists the encryption error codes.

## The stateful helpers

`require "zeroclick/sellers/stateful"` implements the account routes for [sellers whose purchases create accounts and API keys](/integrate/stateful-sellers):

* `ZeroClick::Sellers::Stateful.handle_access_request(request, on_write:, on_mint:, base_path:, remint_policy:, ...)`: routes, verifies, and validates both account calls, then hands off to your two handlers. Returns `nil` for anything that is not one of its routes.
* `Stateful::Entitlement.should_apply?`, `.derive_credit_delta`, `.period_advanced?`, and `.parse`: the dedup and never-regress arithmetic an account write must be applied with.
* `Stateful::Entitlement.serviceable?(state, now = Time.now)`: the one request-time gate — `true` only while the account is `active` and its period, when it has an `end`, has not lapsed.
* `Stateful::Money.parse_money_usd` and `.format_money_usd`: six-decimal money strings to integer micros and back, so no floating-point math ever touches money.
* `Stateful::Verify.verify_access_signature`, `.canonical_access_string`, and `.sign_access_request`: the purpose-carrying access signature on its own, plus a signer for integration tests.
* `Stateful::Contracts`: the wire shapes — parsed entitlements expose `lifetime_credit_granted_usd` and `lifetime_credit_reversed_usd` (plus `_micros` variants) alongside the period and status.

Do not route these calls through `#guard` or `#verify_request`: account calls carry a different, purpose-separated signature by design. The [account and key reference](/resources/stateful-access-reference) documents every field, status, and retry rule.
