# Node SDK

`@koo-io/sdk` is a framework-free, typed Node client for the [Koo API](/docs/developers/api). You create an instance-scoped client with `new KooClient({ token })` — authenticating with a `kc_…` [API token](/docs/developers/api-tokens) — and navigate a typed scope chain from account down to a single service. It ships ESM and CJS builds with full TypeScript types, generated from the same OpenAPI spec as the [API reference](/api), so the SDK surface always matches the API. It has no React dependency and no runtime dependencies at all.

```bash
npm install @koo-io/sdk
```

Requires Node 20.19 or newer.

## Authenticate

The SDK authenticates with a `kc_…` service-account token — read it from the environment, never hard-code it. Use `whoami()` to confirm who you're acting as; unlike the browser's `/me`, it works with a service-account token and tells you the single account the token is confined to.

```ts
import { KooClient } from '@koo-io/sdk';

const koo = new KooClient({
  baseUrl: 'https://api.koo.io',
  token: process.env.KOO_TOKEN, // a kc_… token (or pass getToken: () => Promise<string>)
});

const me = await koo.whoami();
if (me.kind !== 'service_account') throw new Error('Use a kc_ service-account token.');
console.log(`Acting as ${me.name} (${me.role}) on account ${me.accountId}`);
```

## The scope chain

The client mirrors the API's shape: an account holds projects, a project holds environments, and an environment holds services. Descend with `.account(id).project(id).environment(id).service(name)`; each hop returns a typed scope.

```ts
const account = koo.account(me.accountId);

const project = await account.projects.create({ name: 'web' });
const environment = await account.project(project.id).environments.create({ name: 'prod' });
const service = await account
  .project(project.id)
  .environment(environment.id)
  .services.create({
    name: 'api',
    type: 'web',
    cpu: 250,
    memory: 256,
    exposed: true,
    source: { type: 'image', image: { ref: 'ghcr.io/acme/api:latest' } },
  });
```

Every call takes a trailing `{ idempotencyKey?, signal? }` and returns the unwrapped response body. The full surface:

```
koo.whoami() / koo.me()
koo.account(accountId)
    .get() · .tokens{ list, create, revoke } · .projects{ list, create }
    .project(projectId)
        .get() · .environments{ list, create }
        .environment(environmentId)
            .get() · .services{ list, create }
            .service(name)
                .get() · .update() · .deploy() · .rollback() · .deployFromArchive()
                .deployments{ list, buildLogs } · .uploads{ create }
                .variables{ batch, resolved } · .logs() · .metrics()
```

## Deploy and watch it roll out

```ts
const svc = koo.account(me.accountId).project(project.id).environment(environment.id).service('api');

const deployment = await svc.deploy({ image: 'ghcr.io/acme/api:sha' });
// The deployment status advances through a fixed pipeline:
//   queued → building → built → applied   (image deploys skip the build phases)
```

Poll `svc.deployments.list()` until your deployment's `status` is `applied` — that means the release is on the platform. It is **not** a health verdict: read `svc.get()` and watch `status.health` reach `online` (the service is running) — it reads `starting` while the new version rolls out.

## Errors

Every call rejects with a `KooError` carrying a stable `code`, a human `message`, optional `details`, the HTTP `status`, and a `requestId` (from the response's `x-request-id` header) to quote to support.

```ts
import { KooError } from '@koo-io/sdk';

try {
  await svc.deploy({ image: 'ghcr.io/acme/api:sha' });
} catch (error) {
  if (error instanceof KooError) {
    console.error(`[${error.code}] ${error.message} — reference ${error.requestId}`);
  }
}
```

## Retries and idempotency

The SDK retries transient failures for you. Idempotent verbs (`GET`/`HEAD`/`PUT`/`DELETE`) and any `POST` carrying an `Idempotency-Key` are retried on network errors, `429`, and the transient `5xx` statuses (`500`, `502`, `503`, `504`), with full-jitter exponential backoff that honours `Retry-After`. A `4xx` other than `429` — including `401`/`403` — fails fast on the first request, with no loop.

By default the SDK stamps a stable `Idempotency-Key` on `POST`s so they are safe to retry (the server dedups the write). Pass your own with `{ idempotencyKey }` on the call, or turn auto-stamping off for the whole client with `autoIdempotency: false` in the constructor options.

## Prefer the raw API?

The REST API the SDK wraps is live and documented: the [API overview](/docs/developers/api) covers authentication, errors, and pagination, and the [API reference](/api) documents every endpoint. Everything the SDK does is also available over plain HTTPS with a `kc_…` [API token](/docs/developers/api-tokens), and the [CLI](/docs/developers/cli) drives Koo from your terminal.

## Related

- 
- 
-
