Skip to main content
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

Not yet published to RubyGems — install from the monorepo path or via a git checkout.
# Gemfile
gem "knoxcall", path: "../knoxcall-ruby"
# or from git:
gem "knoxcall", git: "...", glob: "sdk/knoxcall-ruby/*.gemspec"

Create a client

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

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):
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.
# 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:
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:
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:
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 in the monorepo.