@knoxcall/sdk is the official KnoxCall client for Node.js and TypeScript. Every method is fully typed against the live /v1 response shapes.
Install
Not yet published to npm — install from a monorepo checkout by path, or via a git subdirectory reference. Requires Node 18 or newer.
npm install /path/to/KnoxCall/sdk/knoxcall-node
Create a client
import { KnoxCall } from "@knoxcall/sdk";
// Credentials inline — no extra imports; tenant auto-discovered
const client = new KnoxCall({ clientId: "tk_xxxxxxxx", clientSecret: "..." });
// Or zero-arg with the environment configured
// (KNOXCALL_CLIENT_ID, KNOXCALL_CLIENT_SECRET)
const client2 = new KnoxCall();
// A pre-acquired key also works (kc_… token or legacy tk_… / AKE… key)
const client3 = new KnoxCall({ apiKey: process.env.KNOXCALL_API_KEY });
// Test mode: sandbox host + tk_test_ key
const sandbox = new KnoxCall({ apiKey: "tk_test_...", sandbox: true });
With no explicit credentials, the SDK auto-detects workload identity (GitHub Actions, GCP, AWS, Azure, Vercel) and exchanges the platform’s OIDC token for a KnoxCall access token — no stored secrets in CI.
Manage resources
Create a route, then list with pagination — the server’s {data, meta} envelope is unwrapped for single objects, and iterate() walks every page:
const route = await client.routes.create({
name: "Orders API",
slug: "orders-api",
target_base_url: "https://api.example.com",
});
const page = await client.routes.list({ page: 2, per_page: 50 });
console.log(page.meta.total_pages);
for await (const route of client.routes.iterate()) {
console.log(route.slug ?? route.id);
}
The same pattern covers every resource: secrets, webhooks, clients, oauthClients, environments, apiKeys, account, auditLogs, agents, crypto, pki, vaults, and dynamicDb.
Call routes through the proxy
client.call() proxies a request through a KnoxCall route to your upstream and returns the raw Response — the upstream’s HTTP status belongs to you; the SDK never turns it into an error. Reference routes by slug (write-once, rename-proof); UUIDs also work.
// GET
const resp = await client.call("orders-api", { path: "/users" });
const users = await resp.json();
// POST with body, targeting a specific environment
await client.call("orders-api", {
method: "POST",
path: "/v1/charges",
body: { amount: 2000, currency: "usd" },
environment: "staging",
});
Bound routes
State the route (and optional defaults) once with client.route(), then use plain HTTP verbs:
const printnode = client.route("printnode", { environment: "production" });
const computers = await (await printnode.get("/computers")).json();
await printnode.post("/printjobs", { body: { printerId: 1, title: "Invoice" } });
await printnode.request("DELETE", "/printjobs/42");
// per-call options still override the bound defaults:
await 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
constructWebhookEvent verifies the delivery signature AND parses it into a typed event in one step. Pass the raw body bytes (never re-serialized JSON):
import { constructWebhookEvent, WebhookSignatureVerificationError } from "@knoxcall/sdk";
app.post("/webhooks/knoxcall", express.raw({ type: "application/json" }), async (req, res) => {
try {
const event = await constructWebhookEvent({
rawBody: req.body, // Buffer — the raw bytes
headers: req.headers,
secret: process.env.KNOXCALL_WEBHOOK_SECRET!,
// format: "stripe" | "github" | "slack" | "aws-sns" | "custom" — match your webhook's hmac_format (default "legacy")
});
if (event.event === "request.server_error") {
console.error(`${event.data.route_name} returned ${event.data.response.status}`);
}
res.sendStatus(200);
} catch (e) {
if (e instanceof WebhookSignatureVerificationError) return res.sendStatus(400);
throw e;
}
});
Also available as client.webhooks.constructEvent(). Timestamps are replay-checked (default tolerance 300s; pass toleranceSeconds: null to disable).
Handle errors
All errors extend KnoxCallError and carry .status, .code, .headers, .body, and .requestId:
import { RateLimitError, ValidationError, AuthenticationError } from "@knoxcall/sdk";
try {
await client.routes.create({ name: "x", target_base_url: "https://y" });
} catch (e) {
if (e instanceof RateLimitError) {
await sleep((e.retryAfter ?? 1) * 1000);
// retry
} else if (e instanceof ValidationError) {
console.error("Validation failed:", e.fields, e.requestId);
} else if (e instanceof AuthenticationError) {
console.error("Auth failed:", e.message);
} else {
throw e;
}
}
Hierarchy: APIConnectionError / APIConnectionTimeoutError / APIUserAbortError (transport), AuthenticationError (401), PermissionDeniedError (403), NotFoundError (404), ConflictError (409), ValidationError (422, with .fields), RateLimitError (429, with .retryAfter), ServerError (5xx), plus WebhookSignatureVerificationError, SignupError, and BootstrapError.
Retries and idempotency
Management requests retry automatically on network errors and HTTP 408/429/500/502/503/504 with exponential jittered backoff (never 409; Retry-After honored on 429, capped at 30s). Mutating requests carry a ULID X-Idempotency-Key that stays stable across retries — pass { idempotencyKey } on any mutating method to supply your own. Tune with:
new KnoxCall({
tenant: "acme",
retry: { maxAttempts: 5, baseDelayMs: 200, maxDelayMs: 10000 },
});
DPoP
For higher-security tenants, enable sender-constrained tokens (RFC 9449) — the SDK generates an ES256 keypair, binds the access token via cnf.jkt, and signs a fresh proof per request:
const client = new KnoxCall({ tenant: "acme", dpop: "always" });
In the default "auto" mode the SDK upgrades to DPoP automatically when the OAuth client record requires it.
Full reference
The package README documents every resource method, token stores (FileTokenStore, RedisTokenStore), telemetry hooks, the ephemeral proxy, the low-level Session, and credential-less signup(): sdk/knoxcall-node/README.md in the monorepo.