diff --git a/config/navigation.json b/config/navigation.json
index 4c53f95..8d8427a 100644
--- a/config/navigation.json
+++ b/config/navigation.json
@@ -431,6 +431,27 @@
}
]
},
+ {
+ "item": "API Reference",
+ "icon": "code",
+ "openapi": "https://app.kosli.com/api/v2/openapi.json"
+ },
+ {
+ "item": "SDKs",
+ "icon": "puzzle-piece",
+ "groups": [
+ {
+ "group": "TypeScript",
+ "icon": "js",
+ "pages": [
+ "typescript-sdk/index",
+ "typescript-sdk/retries-and-timeouts",
+ "typescript-sdk/pagination",
+ "typescript-sdk/error-handling"
+ ]
+ }
+ ]
+ },
{
"item": "Template Reference",
"icon": "file-code",
@@ -457,11 +478,6 @@
}
]
},
- {
- "item": "API Reference",
- "icon": "code",
- "openapi": "https://app.kosli.com/api/v2/openapi.json"
- },
{
"item": "Helm Reference",
"icon": "layer-group",
diff --git a/typescript-sdk/error-handling.mdx b/typescript-sdk/error-handling.mdx
new file mode 100644
index 0000000..bfc0c9f
--- /dev/null
+++ b/typescript-sdk/error-handling.mdx
@@ -0,0 +1,101 @@
+---
+title: Error handling
+description: How to catch and inspect errors thrown by the Kosli TypeScript SDK.
+---
+
+All HTTP errors from the SDK are instances of `KosliError` or one of its subclasses. Network-level failures throw one of the client error classes listed below.
+
+## KosliError
+
+`KosliError` is the base class for any non-2xx HTTP response. It exposes:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `message` | `string` | Error message |
+| `statusCode` | `number` | HTTP status code (e.g. `404`, `403`) |
+| `headers` | `Headers` | Response headers |
+| `body` | `string` | Raw response body (may be empty) |
+| `rawResponse` | `Response` | The full HTTP response object |
+| `data$` | varies | Structured error data for specific error types (see below) |
+
+## HTTP error classes
+
+These named subclasses cover the most common API errors:
+
+| Class | Status | Description |
+|-------|--------|-------------|
+| `BadRequestError` | `400` | Invalid request |
+| `UnauthorizedError` | `401` | Permission denied or not authenticated |
+| `NotFoundError` | `404` | Resource not found |
+| `RateLimitedError` | `429` | Rate limit exceeded |
+
+```typescript
+import { Kosli } from "@kosli/sdk";
+import * as errors from "@kosli/sdk/models/errors";
+
+const kosli = new Kosli({
+ httpBearer: process.env["KOSLI_API_KEY"] ?? "",
+});
+
+try {
+ const trail = await kosli.trails.get({}, {
+ org: "my-org",
+ flowName: "my-flow",
+ trailName: "my-trail",
+ });
+} catch (error) {
+ if (error instanceof errors.NotFoundError) {
+ console.error("Trail not found");
+ } else if (error instanceof errors.UnauthorizedError) {
+ console.error("Access denied:", error.data$.message);
+ } else if (error instanceof errors.RateLimitedError) {
+ console.error("Rate limit hit - back off and retry");
+ } else if (error instanceof errors.KosliError) {
+ console.error(`HTTP ${error.statusCode}: ${error.message}`);
+ }
+}
+```
+
+## Network errors
+
+These errors are thrown when the request cannot be completed at the transport level:
+
+| Class | Description |
+|-------|-------------|
+| `ConnectionError` | The HTTP client could not reach the server |
+| `RequestTimeoutError` | The request was aborted by an `AbortSignal` timeout |
+| `RequestAbortedError` | The request was cancelled by the caller |
+| `InvalidRequestError` | The request could not be constructed (invalid input) |
+| `UnexpectedClientError` | An unrecognised error occurred in the HTTP client |
+
+Network errors do not have a `statusCode` - handle them separately from `KosliError`:
+
+```typescript
+import * as errors from "@kosli/sdk/models/errors";
+
+try {
+ const result = await kosli.trails.list({}, { org: "my-org", flowName: "my-flow" });
+} catch (error) {
+ if (error instanceof errors.ConnectionError) {
+ console.error("Could not connect to Kosli - check your network");
+ } else if (error instanceof errors.RequestTimeoutError) {
+ console.error("Request timed out");
+ } else if (error instanceof errors.KosliError) {
+ console.error(`API error ${error.statusCode}: ${error.message}`);
+ } else {
+ throw error;
+ }
+}
+```
+
+## Other errors
+
+A small number of operations can throw additional error types for specific conditions:
+
+| Class | Status | Description |
+|-------|--------|-------------|
+| `ConflictResponseModelError` | `409` | Conflict (e.g. resource already exists) |
+| `RequestEntityTooLargeResponseModelError` | `413` | Request body too large |
+| `ResponseValidationError` | - | Response from server did not match the expected schema; inspect `error.rawValue` or call `error.pretty()` |
+
+Check the method's documentation to see which errors apply to a specific operation.
diff --git a/typescript-sdk/index.mdx b/typescript-sdk/index.mdx
new file mode 100644
index 0000000..22ddd4f
--- /dev/null
+++ b/typescript-sdk/index.mdx
@@ -0,0 +1,182 @@
+---
+title: Overview
+description: Use the Kosli TypeScript SDK to interact with the Kosli API from Node.js, Bun, Deno, and browser environments.
+---
+
+
+**Early access.** The TypeScript SDK is an unsupported work in progress and may change or be removed without notice. It is provided for evaluation only - do not use it in production.
+
+
+The `@kosli/sdk` package provides a type-safe TypeScript client for the Kosli API. It supports Node.js, Bun, Deno, and all modern browsers.
+
+## Installation
+
+Install using your preferred package manager:
+
+
+```bash npm
+npm add @kosli/sdk
+```
+
+```bash pnpm
+pnpm add @kosli/sdk
+```
+
+```bash bun
+bun add @kosli/sdk
+```
+
+```bash yarn
+yarn add @kosli/sdk
+```
+
+
+
+The package is published as an ES Module (ESM) only. CommonJS projects must use dynamic import:
+
+```typescript
+const { Kosli } = await import("@kosli/sdk");
+```
+
+
+## Quick start
+
+```typescript
+import { Kosli } from "@kosli/sdk";
+
+const kosli = new Kosli({
+ httpBearer: process.env["KOSLI_API_KEY"] ?? "",
+});
+
+const trails = await kosli.trails.list({}, {
+ org: "my-org",
+ flowName: "my-flow",
+});
+
+console.log(trails);
+```
+
+## Authentication
+
+Set your API key when constructing the client. This applies it to all requests:
+
+```typescript
+import { Kosli } from "@kosli/sdk";
+
+const kosli = new Kosli({
+ httpBearer: process.env["KOSLI_API_KEY"] ?? "",
+});
+```
+
+Some operations accept the API key at the call site instead. Use this when you need per-request credentials:
+
+```typescript
+const result = await kosli.trails.list(
+ { httpBearer: process.env["KOSLI_API_KEY"] ?? "" }, // security
+ { org: "my-org", flowName: "my-flow" }, // request params
+);
+```
+
+See [personal API keys](/user/personal_api_keys) and [service accounts](/administration/authentication/service_accounts) for how to create API keys.
+
+## Server selection
+
+By default the SDK targets the EU region. Pass `server: "us"` to use the US region:
+
+```typescript
+const kosli = new Kosli({
+ httpBearer: process.env["KOSLI_API_KEY"] ?? "",
+ server: "us",
+});
+```
+
+| Name | URL | Region |
+|------|-----|--------|
+| `"eu"` | `https://app.kosli.com/api/v2` | EU (default) |
+| `"us"` | `https://app.us.kosli.com/api/v2` | US |
+
+To point at a custom URL (e.g. a proxy or local instance), use `serverURL` instead:
+
+```typescript
+const kosli = new Kosli({
+ httpBearer: process.env["KOSLI_API_KEY"] ?? "",
+ serverURL: "https://app.kosli.com/api/v2",
+});
+```
+
+## Available resources
+
+The client exposes the following resource groups:
+
+| Resource | Description |
+|----------|-------------|
+| `kosli.actions` | Environment and flow actions |
+| `kosli.allowlists` | Artifact allowlists |
+| `kosli.approvals` | Approvals |
+| `kosli.artifacts` | Artifact reporting and lookup |
+| `kosli.attestations` | Attestations (custom, JUnit, Jira, Snyk, Sonar, pull request) |
+| `kosli.builds` | Builds |
+| `kosli.customAttestationTypes` | Custom attestation type management |
+| `kosli.deployments` | Deployments |
+| `kosli.environments` | Environment management and reporting |
+| `kosli.flows` | Flow management |
+| `kosli.organizations` | Organization settings and notifications |
+| `kosli.policies` | Policy management |
+| `kosli.repos` | Repository live artifact lookup |
+| `kosli.schemas` | Policy and template schemas |
+| `kosli.search` | Artifact search by SHA or fingerprint |
+| `kosli.serviceAccounts` | Service account API key management |
+| `kosli.snapshots` | Environment snapshots |
+| `kosli.tags` | Tag management |
+| `kosli.trails` | Trail management |
+| `kosli.user` | User settings |
+
+## Standalone functions
+
+All methods are also available as individually importable functions, which allows bundlers to tree-shake unused code:
+
+```typescript
+import { KosliCore } from "@kosli/sdk/core.js";
+import { trailsList } from "@kosli/sdk/funcs/trails-list.js";
+
+const kosli = new KosliCore({
+ httpBearer: process.env["KOSLI_API_KEY"] ?? "",
+});
+
+const result = await trailsList(kosli, {}, {
+ org: "my-org",
+ flowName: "my-flow",
+});
+```
+
+## Supported runtimes
+
+The SDK requires ECMAScript 2020 or later and the Web Fetch API. Supported environments:
+
+- **Browsers**: Chrome, Safari, Edge, Firefox (evergreen)
+- **Node.js**: active and maintenance LTS releases (v18, v20+)
+- **Bun**: v1 and above
+- **Deno**: v1.39+
+
+For TypeScript projects, add `"lib": ["es2020", "dom", "dom.iterable"]` to your `tsconfig.json` to get full type support for async iterables, streams, and fetch-related APIs.
+
+## Debugging
+
+Pass a logger to emit debug output for all requests and responses:
+
+```typescript
+const kosli = new Kosli({
+ httpBearer: process.env["KOSLI_API_KEY"] ?? "",
+ debugLogger: console,
+});
+```
+
+Alternatively, set the environment variable `KOSLI_DEBUG=true`.
+
+
+Debug logging prints headers including your API key in plain text. Use it only during local development.
+
+
+## Package
+
+The SDK is published on npm as [`@kosli/sdk`](https://www.npmjs.com/package/@kosli/sdk).
diff --git a/typescript-sdk/pagination.mdx b/typescript-sdk/pagination.mdx
new file mode 100644
index 0000000..e219b66
--- /dev/null
+++ b/typescript-sdk/pagination.mdx
@@ -0,0 +1,106 @@
+---
+title: Pagination
+description: How to work with paginated responses in the Kosli TypeScript SDK.
+---
+
+List operations in the Kosli API return results in pages. The SDK exposes the underlying page controls directly - there is no automatic iterator; you request each page explicitly.
+
+## Request parameters
+
+Paginated operations accept `page` and `perPage` query parameters:
+
+| Parameter | Type | Description |
+|-----------|------|-------------|
+| `page` | `number` | Page number, starting at `1` |
+| `perPage` | `number` | Number of results per page |
+
+```typescript
+import { Kosli } from "@kosli/sdk";
+
+const kosli = new Kosli({
+ httpBearer: process.env["KOSLI_API_KEY"] ?? "",
+});
+
+const result = await kosli.trails.list({}, {
+ org: "my-org",
+ flowName: "my-flow",
+ page: 1,
+ perPage: 20,
+});
+```
+
+## Response formats
+
+Different endpoints return pagination metadata in different ways.
+
+### Pagination object in the response body
+
+Some operations return a `pagination` field alongside the results. It is omitted when all results fit on one page.
+
+```typescript
+const result = await kosli.trails.list({}, {
+ org: "my-org",
+ flowName: "my-flow",
+ page: 1,
+ perPage: 20,
+});
+
+if (result.pagination) {
+ console.log(`Page ${result.pagination.page} of ${result.pagination.pageCount}`);
+ console.log(`${result.pagination.total} total trails`);
+}
+```
+
+The `pagination` object contains:
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `total` | `number` | Total number of results across all pages |
+| `page` | `number` | Current page number |
+| `perPage` | `number` | Results per page |
+| `pageCount` | `number` | Total number of pages |
+
+### Link header
+
+Some operations return pagination links as a `Link` response header. Parse these to navigate to the next page without constructing URLs manually.
+
+```
+; rel="first",
+; rel="prev",
+; rel="next",
+; rel="last"
+```
+
+The `Link` header is only present when there are multiple pages. The SDK does not parse this header automatically - access it via the raw response if needed.
+
+## Iterating all pages
+
+Fetch pages in a loop until there are no more results:
+
+```typescript
+import { Kosli } from "@kosli/sdk";
+
+const kosli = new Kosli({
+ httpBearer: process.env["KOSLI_API_KEY"] ?? "",
+});
+
+async function fetchAllTrails(org: string, flowName: string) {
+ const allTrails = [];
+ let page = 1;
+ const perPage = 50;
+
+ while (true) {
+ const result = await kosli.trails.list({}, { org, flowName, page, perPage });
+
+ const trails = Array.isArray(result) ? result : result.data ?? [];
+ allTrails.push(...trails);
+
+ if (!result.pagination || page >= result.pagination.pageCount) {
+ break;
+ }
+ page++;
+ }
+
+ return allTrails;
+}
+```
diff --git a/typescript-sdk/retries-and-timeouts.mdx b/typescript-sdk/retries-and-timeouts.mdx
new file mode 100644
index 0000000..f66e82a
--- /dev/null
+++ b/typescript-sdk/retries-and-timeouts.mdx
@@ -0,0 +1,100 @@
+---
+title: Retries and timeouts
+description: How the Kosli TypeScript SDK handles automatic retries and request timeouts.
+---
+
+## Retries
+
+Many SDK operations retry automatically on transient failures. The default policy retries on:
+
+- HTTP `5XX` responses (server errors)
+- HTTP `429` responses (rate limiting)
+- HTTP `409` responses (lock conflict)
+- Network connection errors
+
+Retries use an exponential backoff: 1s initial interval, 10s maximum interval, approximately 30s total budget.
+
+### Override retries for a single call
+
+Pass a `retries` object in the call options to change the retry behavior for one request:
+
+```typescript
+import { Kosli } from "@kosli/sdk";
+
+const kosli = new Kosli({
+ httpBearer: process.env["KOSLI_API_KEY"] ?? "",
+});
+
+const result = await kosli.trails.list({}, { org: "my-org", flowName: "my-flow" }, {
+ retries: {
+ strategy: "backoff",
+ backoff: {
+ initialInterval: 500, // ms before first retry
+ maxInterval: 30_000, // ms cap per interval
+ exponent: 1.5,
+ maxElapsedTime: 60_000, // ms total budget
+ },
+ retryConnectionErrors: true,
+ },
+});
+```
+
+### Override retries for all calls
+
+Set `retryConfig` in the constructor to apply a retry policy to all requests:
+
+```typescript
+import { Kosli } from "@kosli/sdk";
+
+const kosli = new Kosli({
+ httpBearer: process.env["KOSLI_API_KEY"] ?? "",
+ retryConfig: {
+ strategy: "backoff",
+ backoff: {
+ initialInterval: 500,
+ maxInterval: 30_000,
+ exponent: 1.5,
+ maxElapsedTime: 60_000,
+ },
+ retryConnectionErrors: true,
+ },
+});
+```
+
+### Disable retries
+
+Set `strategy: "none"` to disable retries entirely:
+
+```typescript
+const result = await kosli.trails.list({}, { org: "my-org", flowName: "my-flow" }, {
+ retries: { strategy: "none" },
+});
+```
+
+## Timeouts
+
+The SDK does not have a built-in timeout parameter. Set timeouts via the `AbortSignal` API when constructing a custom HTTP client.
+
+```typescript
+import { Kosli } from "@kosli/sdk";
+import { HTTPClient } from "@kosli/sdk/lib/http";
+
+const httpClient = new HTTPClient();
+
+httpClient.addHook("beforeRequest", (request) => {
+ return new Request(request, {
+ signal: request.signal ?? AbortSignal.timeout(10_000), // 10s timeout
+ });
+});
+
+const kosli = new Kosli({
+ httpBearer: process.env["KOSLI_API_KEY"] ?? "",
+ httpClient,
+});
+```
+
+A timed-out request throws a `RequestTimeoutError`. See [error handling](/typescript-sdk/error-handling) for details.
+
+
+`AbortSignal.timeout()` is available in Node.js v17.3+, Bun v1+, and modern browsers. For older Node.js, use `AbortController` with `setTimeout` instead.
+