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

# Node.js SDK

> @knoxcall/sdk — the official KnoxCall client for Node.js and TypeScript. OAuth 2.1 + DPoP under the hood; your code just calls methods.

`@knoxcall/sdk` is the official KnoxCall client for Node.js and TypeScript. Every method is fully typed against the live `/v1` response shapes.

## Install

<Note>
  Not yet published to npm — install from a monorepo checkout by path, or via a git subdirectory reference. Requires Node 18 or newer.
</Note>

```bash theme={"dark"}
npm install /path/to/KnoxCall/sdk/knoxcall-node
```

## Create a client

```typescript theme={"dark"}
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:

```typescript theme={"dark"}
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.

```typescript theme={"dark"}
// 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:

```typescript theme={"dark"}
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):

```typescript theme={"dark"}
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`:

```typescript theme={"dark"}
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:

```typescript theme={"dark"}
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:

```typescript theme={"dark"}
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`](https://github.com/knoxcall/knoxcall/tree/main/sdk/knoxcall-node) in the monorepo.
