Skip to main content
knoxcall-go is the official KnoxCall client for Go — stdlib only, context-first, goroutine-safe (the token store uses single-flight refresh).

Install

Not yet published — install from the monorepo path with a replace directive in your go.mod, or via a git checkout.
replace github.com/knoxcall/knoxcall-go => ../knoxcall-go

Create a client

import "github.com/knoxcall/knoxcall-go/knoxcall"

// Credentials inline — tenant auto-discovered from the credential.
client, err := knoxcall.New(knoxcall.Options{ClientID: "tk_xxxxxxxx", ClientSecret: "..."})

// Or zero-option with the environment configured
// (KNOXCALL_CLIENT_ID, KNOXCALL_CLIENT_SECRET):
client, err = knoxcall.New(knoxcall.Options{})

// A pre-acquired key also works (kc_… token or legacy tk_… / AKE… key):
client, err = knoxcall.New(knoxcall.Options{APIKey: os.Getenv("KNOXCALL_API_KEY")})
Set 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. APIKey and ClientID) is a construction error; explicit options always beat the environment.
DPoP-bound OAuth clients are not yet supported by the Go SDK — if the server issues a DPoP-bound token, the SDK fails fast with a clear error 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 object; paginated lists return a typed Page[T]; ListAll walks every page:
ctx := context.Background()

route, err := client.Routes.Create(ctx, knoxcall.CreateRouteInput{Name: "orders", TargetBaseURL: "https://api.example.com"})

page, err := client.Routes.List(ctx, &knoxcall.ListParams{Page: 2, PerPage: 50})
fmt.Println(page.Meta.Total, page.Meta.TotalPages, page.Meta.RequestID)

// Walk every page in one call:
all, err := client.Secrets.ListAll(ctx, nil)
The same pattern covers every resource: Secrets, Webhooks, Clients, OAuthClients, Environments, APIKeys, Account, AuditLogs, Agents, Crypto, PKI, Vaults, and DynamicDB. All methods take a context.Context first.

Call routes through the proxy

client.Call() proxies a request through a KnoxCall route to your upstream and returns the raw *http.Response — 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
resp, err := client.Call(ctx, "api-orders", &knoxcall.CallOptions{Path: "/users"})
defer resp.Body.Close()

// POST with a body, targeting a specific environment
resp, err = client.Call(ctx, "api-orders", &knoxcall.CallOptions{
    Method:      "POST",
    Path:        "/v1/charges",
    Body:        map[string]any{"amount": 2000, "currency": "usd"},
    Environment: "staging",
})
For a per-call timeout, pass a context with a deadline (context.WithTimeout).

Bound routes

State the route (and optional defaults) once with client.Route(), then use plain HTTP verbs:
printnode := client.Route("api-printnode", &knoxcall.BoundRouteOptions{Environment: "production"})

resp, err := printnode.Get(ctx, "/computers", nil)
resp, err = printnode.Post(ctx, "/printjobs", &knoxcall.CallOptions{Body: job})
resp, err = printnode.Request(ctx, "DELETE", "/printjobs/42", nil)
// per-call options still override the bound defaults:
resp, err = printnode.Get(ctx, "/computers", &knoxcall.CallOptions{Environment: "staging"})
The handle holds no state beyond the defaults — retries, token refresh, and 401 re-mint behave exactly as on Call().

Verify webhooks

ConstructWebhookEvent (also available as client.Webhooks.ConstructEvent) verifies the delivery’s HMAC-SHA256 signature and parses it into a typed event in one step. Pass the raw body bytes — never re-serialized JSON:
func handler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    event, err := knoxcall.ConstructWebhookEvent(body, r.Header, endpointSecret, nil)
    if err != nil {
        var ve *knoxcall.WebhookSignatureVerificationError
        if errors.As(err, &ve) {
            http.Error(w, "bad signature", http.StatusBadRequest)
            return
        }
    }
    switch event.Event {
    case "audit.event":
        audit, _ := event.AuditData()
        log.Println("audit:", audit.Action)
    default: // request.*
        data, _ := event.RequestData()
        log.Println(data.RouteName, data.Response.Status)
    }
}
Formats beyond the default legacy header (stripe, github, slack, aws-sns, custom) are selected with &knoxcall.ConstructEventOptions{Format: "stripe"}; the replay window defaults to 300s; custom requires HeaderName.

Handle errors

All API failures are typed and unwrap to *APIError (status, machine-readable type, human message, RequestID for support):
_, err := client.Routes.Create(ctx, input)
var rl *knoxcall.RateLimitError
var ve *knoxcall.ValidationError
switch {
case errors.As(err, &rl):
    time.Sleep(time.Duration(rl.RetryAfter) * time.Second)
case errors.As(err, &ve):
    log.Println("validation failed:", ve.Fields)
}
Hierarchy: AuthenticationError (401), PermissionDeniedError (403), NotFoundError (404), ConflictError (409), ValidationError (422), RateLimitError (429), ServerError (5xx), plus SignupError, WebhookSignatureVerificationError, and ConnectionError / 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. Tune with Options{RetryMaxAttempts, RetryBaseDelay, RetryMaxDelay}.

Full reference

The package README documents every resource method, the ephemeral proxy, field-actions, crypto/PKI/vault operations, and credential-less Signup(): sdk/knoxcall-go/README.md in the monorepo.