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

# Ruby SDK

> knoxcall — the official KnoxCall client for Ruby 3.1+. OAuth 2.1 under the hood, zero dependencies (stdlib only).

`knoxcall` is the official KnoxCall client for Ruby — stdlib only, Ruby >= 3.1. The client is Mutex-safe: share a single instance across Puma / Sidekiq threads.

## Install

<Note>
  Not yet published to RubyGems — install from the monorepo path or via a git checkout.
</Note>

```ruby theme={"dark"}
# Gemfile
gem "knoxcall", path: "../knoxcall-ruby"
# or from git:
gem "knoxcall", git: "...", glob: "sdk/knoxcall-ruby/*.gemspec"
```

## Create a client

```ruby theme={"dark"}
require "knoxcall"

# Credentials inline — tenant auto-discovered from the credential.
client = KnoxCall::Client.new(client_id: "tk_xxxxxxxx", client_secret: "...")

# Or zero-arg with the environment configured
# (KNOXCALL_CLIENT_ID, KNOXCALL_CLIENT_SECRET):
client = KnoxCall::Client.new

# A pre-acquired key also works (kc_… token or legacy tk_… / AKE… key):
client = KnoxCall::Client.new(api_key: ENV["KNOXCALL_API_KEY"])
```

Pass `sandbox: true` to target the isolated Test data plane (`sandbox.knoxcall.com` management host, `sandbox-{tenant}.knoxcall.com` proxy host) with a `tk_test_` key. Passing conflicting credential options (e.g. `api_key:` and `client_id:`) raises `ArgumentError` at construction.

<Note>
  DPoP-bound OAuth clients are not yet supported by the Ruby SDK — the SDK fails fast with a clear `TokenError` rather than mis-authenticating. Use a Bearer OAuth client, or the Node.js / Python SDK.
</Note>

## Manage resources

Create a route, then list with pagination — single-object methods return the unwrapped Hash; paginated lists return the `{data, meta}` envelope; `each` walks every page transparently (block form or lazy Enumerator):

```ruby theme={"dark"}
route = client.routes.create(name: "orders", target_base_url: "https://api.example.com")

page = client.secrets.list(page: 2, per_page: 50)
page["data"]                  # => [...]
page["meta"]["total"]         # => 137
page["meta"]["total_pages"]   # => 3

client.secrets.each { |secret| puts secret["name"] }
first_ten = client.routes.each(per_page: 5).take(10) # fetches only 2 pages
```

Sub-lists have `each_*` twins: `routes.each_log(id)`, `webhooks.each_log(id)`, `vaults.each_token(name)`. The same pattern covers every resource: `secrets`, `webhooks`, `clients`, `oauth_clients`, `environments`, `api_keys`, `account`, `audit_logs`, `agents`, `crypto`, `pki`, `vaults`, and `dynamic_db`.

## Call routes through the proxy

`client.call` proxies a request through a KnoxCall route to your upstream and returns the raw `Net::HTTPResponse` — the upstream's status belongs to you; the SDK never turns it into an error. Reference routes by **slug** (write-once, rename-proof); UUIDs also work.

```ruby theme={"dark"}
# GET
res = client.call("api-orders", path: "/users")

# POST with a body, targeting a specific environment
res = client.call("api-orders",
                  method: "POST",
                  path: "/v1/charges",
                  body: { amount: 2000, currency: "usd" },
                  environment: "staging")

res.code       # => "201"
res.body       # raw body string
```

A per-call `timeout:` overrides the client default.

### Bound routes

State the route (and optional defaults) once with `client.route`, then use plain HTTP verbs:

```ruby theme={"dark"}
printnode = client.route("api-printnode", environment: "production")

res = printnode.get("/computers")
res = printnode.post("/printjobs", body: job)
res = printnode.request("DELETE", "/printjobs/42")
# per-call options still override the bound defaults:
res = printnode.get("/computers", environment: "staging")
```

The handle holds no state beyond the defaults — retries, token refresh, and 401 re-mint behave exactly as on `call`.

## Verify webhooks

`KnoxCall::Client.construct_event` (also available as `client.construct_event` and `client.webhooks.construct_event`) verifies the delivery's HMAC-SHA256 signature and parses it into an event Hash in one step. Pass the raw body string — never re-serialized JSON:

```ruby theme={"dark"}
post "/webhooks/knoxcall" do
  event = KnoxCall::Client.construct_event(request.body.read, request.env, ENDPOINT_SECRET)

  case event["event"]
  when "audit.event"
    log.info "audit: #{event['data']['action']}"
  else # request.*
    log.info "#{event['data']['route_name']} -> #{event['data']['response']['status']}"
  end
  200
rescue KnoxCall::WebhookSignatureVerificationError
  halt 400, "bad signature"
end
```

Formats beyond the default `legacy` header (`stripe`, `github`, `slack`, `aws-sns`, `custom`) are selected with `format:`; the replay window defaults to 300s (`tolerance_seconds: nil` disables it); `custom` requires `header_name:`. All comparisons are constant-time.

## Handle errors

All API failures are typed subclasses of `KnoxCall::Error`:

```ruby theme={"dark"}
begin
  client.routes.create(name: "orders", target_base_url: url)
rescue KnoxCall::RateLimitError => e
  sleep e.retry_after || 1
rescue KnoxCall::APIError => e
  logger.error "#{e.status_code}: #{e.message}" # message carries the server's request_id context
end
```

Hierarchy: `AuthenticationError` (401), `PermissionDeniedError` (403), `NotFoundError` (404), `RateLimitError` (429), `ServerError` (5xx), all under `APIError` (with `status_code` / `headers` / `body`); plus `SignupError` (with `status_code` / `error_type` / `request_id`), `WebhookSignatureVerificationError`, `TokenError`, and `NetworkError` / `ConnectionTimeoutError` for transport failures.

## Retries and idempotency

Management requests retry automatically on transport errors and HTTP 408/429/500/502/503/504 (never 409), with exponential half-jitter backoff and `Retry-After` honored up to 30s. Every mutating request carries a ULID `X-Idempotency-Key` that stays stable across retries. A 401 purges the cached token and retries once with fresh credentials. Data-plane calls never replay a mutating request after it may have reached the wire. Tune with `retry_max_attempts:`, `retry_base_delay:`, `retry_max_delay:`.

## Full reference

The package README documents every resource method, the ephemeral proxy, crypto/PKI/vault operations, and credential-less `KnoxCall.signup`: [`sdk/knoxcall-ruby/README.md`](https://github.com/knoxcall/knoxcall/tree/main/sdk/knoxcall-ruby) in the monorepo.
