> ## Documentation Index
> Fetch the complete documentation index at: https://docs.idyl.network/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript SDK

> Create and manage IDYL resources from Node.js, browsers, and edge runtimes with @idyl-labs/sdk.

`@idyl-labs/sdk` is the official TypeScript SDK for IDYL. It is one package
with one entry point, the `Idyl` client, and no runtime dependencies. It runs
on Node.js 22 and later, in evergreen browsers, and in Workers-class edge
runtimes from a single build, using only web-standard APIs: `fetch`,
`AbortSignal`, `ReadableStream`, `TextEncoder`, `TextDecoder`, and `URL`. The
package ships ESM and CommonJS builds with type declarations.

The SDK implements the model the [Go SDK](https://github.com/idyl-labs/sdk-go)
defines: the same resources, the same names, and the same wire format, held by
a conformance suite against fixtures taken from the Go SDK. Use the
[repository](https://github.com/idyl-labs/sdk-typescript) and the
[package page](https://www.npmjs.com/package/@idyl-labs/sdk) as the source and
API reference.

## Installation

```bash npm theme={null}
npm install @idyl-labs/sdk
```

```bash pnpm theme={null}
pnpm add @idyl-labs/sdk
```

Import the package as ESM or CommonJS. Type declarations ship with both builds.

```ts theme={null}
import { Idyl } from "@idyl-labs/sdk";
```

## Configure a client

Everything the client needs is in one `ClientConfig` value. The SDK reads no
environment variable, configuration file, or ambient credential store, and
substitutes nothing for a field left unset. Invalid configuration throws a
`TypeError` naming the field.

| Field | Type | Description |
| - | - | - |
| `network` | `string` | The Network domain. The public endpoints derive from it: `https://api.<network>`, `https://identity.<network>`, and `https://meta.<network>`. Omit it only when `endpoints.api` is set. |
| `endpoints` | `Endpoints` | Explicit `api`, `identity`, and `meta` origins. Each entry overrides the value derived from `network`. An endpoint is an absolute `https:` origin with no path, query, fragment, or user information. |
| `accountId` | `string` | The twelve-digit Account ID that every account-scoped and Namespace-scoped operation addresses. Required. |
| `namespace` | `string` | The Namespace that Namespace-scoped operations run in. Required for Workload operations. |
| `placement` | `Placement` | Where Workloads run. Build it with `onSubnet`, `onFleet`, `onNode`, or `onNodeInSubnet`. Required for Workload operations. |
| `credential` | `CredentialSource` | Consulted for every authenticated request. Required. |
| `fetch` | `typeof fetch` | Performs every request the client makes. TLS, proxy, and dialing behavior belong to it. Defaults to `globalThis.fetch`. |
| `webSocket` | `WebSocketFactory` | Opens every WebSocket the client attaches for log and shell sessions. Defaults to `globalThis.WebSocket`. |
| `requestTimeoutMs` | `number` | Bounds each request, in milliseconds. Defaults to `30000`. A request that exceeds it rejects with a `TimeoutError` `DOMException`. |
| `userAgent` | `string` | Overrides the request `User-Agent`. |
| `allowHttp` | `boolean` | Permits cleartext `http:` endpoints for local development and tests. Cleartext exposes the bearer credential to the network path. |

`credential` is a `CredentialSource`. Its `token` method is called for every
authenticated request rather than once at construction, so a source that
refreshes or rotates a short-lived credential is adopted without rebuilding the
client or disturbing clients scoped from it. Every token is opaque to the SDK:
it is sent as a bearer credential and never inspected. `staticToken` wraps a
fixed token. `credentialSourceFunc` adapts a function that receives the
request's `AbortSignal`, so a cancelled request never leaves a token exchange
running.

```ts theme={null}
import { credentialSourceFunc, staticToken } from "@idyl-labs/sdk";

staticToken("<token>");
credentialSourceFunc(async ({ signal }) => await exchangeToken({ signal }));
```

## Quick start

Create a Sandbox, wait until it is ready, and delete it:

```ts theme={null}
import { Idyl, onSubnet, staticToken } from "@idyl-labs/sdk";

const idyl = new Idyl({
  network: "idyl.network",
  accountId: "<account-id>",
  namespace: "<namespace>",
  placement: onSubnet("<subnet>"),
  credential: staticToken("<token>"),
});

const sandbox = await idyl.sandboxes.createAndWait({
  image: "ubuntu:24.04",
  ttlMs: 30 * 60_000,
});
await sandbox.delete();
```

`createAndWait` takes an optional second argument,
`{ timeoutMs, pollIntervalMs, signal }`. Left out, the wait allows ten minutes
and polls every second. A Sandbox with neither `cpu` nor `memory` set is
limited to one CPU and `1Gi` of memory; setting either overrides the default.
The handle it returns is an immutable snapshot of the ready Sandbox.

The examples on the rest of this page use the `idyl` client configured here.

## Scope

A client is immutable. Every scoping method returns a copy and leaves the
receiver unchanged, so a broadly configured client can be narrowed per call
site while the original stays valid. Copies share one transport, which is safe
for concurrent use.

```ts theme={null}
import { onFleet } from "@idyl-labs/sdk";

const namespaced = idyl.forNamespace("<namespace>");
const onFleetClient = namespaced.withPlacement(onFleet("<fleet-id>"));

namespaced.scope;
// { accountId, namespace: "<namespace>", placement }
```

| Method | Returns |
| - | - |
| `forNamespace(namespace)` | A client in an Account-owned Namespace. |
| `withPlacement(placement)` | A client whose Workload operations use `placement`. |
| `withSubnetEndpoint(subnet, apiEndpoint)` | A client bound to an already-resolved public Subnet API endpoint. See [Placement](#placement). |

Direct Workload operations need only the Namespace and placement.
Computer operations take a Computer name or ID through
`idyl.account.computers`. `scope` reports the frozen Account, Namespace, and
placement a client runs in.

## Placement

Placement says where Workloads run. Values come from four constructors, are
frozen, and are validated when they are built:

```ts theme={null}
import { onFleet, onNode, onNodeInSubnet, onSubnet } from "@idyl-labs/sdk";

onSubnet("<subnet>");
onFleet("<fleet-id>");
onNode("<node-id>");
onNodeInSubnet("<node-id>", "<subnet>");
```

Resolution happens through public APIs, once per operation, and is never
cached. A Fleet or Node placement is resolved to its Subnet through the
Account's deployment-target API. Every placement is then checked against the
Subnets the Account may deploy to, and the request goes to the public API host
advertised for that Subnet. A Fleet or Node placement also becomes the
scheduling constraint on every manifest the client submits.

A Subnet that cannot accept the request fails with a
`PlacementUnavailableError` whose `reason` names the classification:

| Reason | Meaning |
| - | - |
| `not_visible` | The Subnet is not visible to the Account. |
| `not_approved` | The Subnet is not approved for the Account. |
| `no_api_host` | The Subnet does not advertise a public API host. |
| `not_active` | The Subnet is not `active`. Creates require it; `subnetStatus` carries the status the Subnet reported. |
| `region_not_enabled` | The Subnet's region is not enabled for the Account. Creates require it. |
| `not_admitted` | The caller is not admitted to the Subnet. Creates require it. |

`reason` is a closed vocabulary that only grows: a value is never renamed or
removed, and a new value is a new case to handle.

A caller that has already resolved and authorized a Subnet route can bind it
instead. A bound client addresses that endpoint directly and repeats no
discovery. Binding changes neither authentication, scope, nor server-side
authorization:

```ts theme={null}
const bound = idyl.withSubnetEndpoint(
  "<subnet>",
  "https://subnet-api.example.com",
);
```

## Services

The `Idyl` client exposes one service per resource family. Workload services
run in the client's Namespace and resolved Subnet; Account services address the
Account directly.

| Accessor | Manages |
| - | - |
| `idyl.sandboxes` | Sandboxes: create from options or a manifest, get, list, delete, and wait until ready. |
| `idyl.pods` | Pods. |
| `idyl.deployments` | Deployments, including `scale`. |
| `idyl.replicaSets` | ReplicaSets, including `scale`. |
| `idyl.jobs` | Jobs, including `waitUntilComplete`. |
| `idyl.cronJobs` | CronJobs, including `trigger`, which returns a handle to the Job it started. |
| `idyl.virtualMachines` | Virtual machines, created from a full `VirtualMachine` manifest, including `waitUntilRunning`. |
| `idyl.workloads` | One heterogeneous snapshot of every Workload kind in the resolved Subnet, with the `snapshotCursor` a watch resumes from. |
| `idyl.runtimePods` | Read-only observed pod runtime state in the client's Namespace. |
| `idyl.runtimeNodes` | Read-only observed Node runtime state in the resolved Subnet. |
| `idyl.workloadSessions` | Log streaming, shell transports, and the session reservation contract. |
| `idyl.account` | The configured Account and its services: `namespaces`, `computers`, `members`, `groups`, `serviceAccounts`, `policies`, `accessProfiles`, `assignments`, `invitations`, `subnets`, `approvedSubnets`, `deploymentTargets`, `deployableSubnetIdentities`, `fleets`, `fleetLeases`, `cloudAccounts`, `nodes`, `fleetUpdates`, and `rateCard()`. |
| `idyl.accountById(accountId)` | The same services for another Account the credential is authorized to manage. |
| `idyl.iam` | The calling identity's own memberships, personal access tokens, and assumable profiles. |
| `idyl.organizations` | Organizations, their organizational units, and the invitations they send. |
| `idyl.organizationAdmin(organizationId)` | The Accounts within one organization. |
| `idyl.vaultSecrets` | Vault Secrets in the client's Namespace and resolved Subnet. Every read returns the redacted form. |
| `idyl.vaultCertificates` | Vault TLS certificates, under the same posture. |
| `idyl.registryCredentials` | Image-pull credentials for private registries. Every read returns the redacted form. |
| `idyl.gateways`, `idyl.httpRoutes`, `idyl.tlsRoutes`, `idyl.referenceGrants` | Gateways, HTTPRoutes, TLSRoutes, and ReferenceGrants. |
| `idyl.services` | Services, which give one Workload a stable named port. |
| `idyl.webApps` | WebApps, which publish one Service port through a platform-generated HTTPS hostname. |
| `idyl.subnets`, `idyl.regions` | Anonymous public Subnet and region discovery from the Network's metadata endpoint. |

Every Namespace-scoped service also lists its collection account-wide through
`listAllNamespaces`, which spans every Namespace the caller is authorized to
list and needs no Namespace on the client. List options are `limit` and
`offset`; list responses carry `items`, `totalCount`, and `snapshotCursor`.

## Handles and waits

Every Workload operation returns a handle, an immutable snapshot of what the
platform recorded. A handle reads its own state, such as `name` and `phase`,
without touching the network, and `manifest()` returns an independent copy of
the snapshot. `refresh` reads the resource again and returns a new handle;
`update` replaces the definition and returns a new handle; `delete` asks the
platform to remove the resource and resolves once the request is accepted. The
receiver is never changed.

```ts theme={null}
const sandbox = await idyl.sandboxes.get("dev");
const current = await sandbox.refresh();
current.phase; // the phase observed by refresh
sandbox.phase; // the phase observed when this handle was built
```

Native options keep common workflows short, and `createManifest` accepts the
full manifest type when the options cannot express a definition. `createAndWait`
and `createManifestAndWait` submit and wait in one call:

```ts theme={null}
const deployment = await idyl.deployments.createAndWait(
  {
    name: "web",
    replicas: 3,
    container: { image: "nginx:1.27", cpu: "500m", memory: "512Mi" },
  },
  { timeoutMs: 5 * 60_000 },
);

const scaled = await deployment.scale(5);
const job = await idyl.cronJobs.trigger("nightly");
await job.waitUntilComplete();
```

Each handle waits on the conditions its kind can reach. A wait polls until the
condition holds and returns the handle that observed it.

| Kind | Waits |
| - | - |
| Pod | `waitUntilStarted`, `waitUntilReady`, `waitUntilComplete`, `waitUntilDeleted` |
| Deployment, ReplicaSet | `waitUntilStarted`, `waitUntilReady`, `waitUntilDeleted` |
| Job | `waitUntilStarted`, `waitUntilComplete`, `waitUntilDeleted` |
| CronJob | `waitUntilDeleted` |
| Sandbox | `waitUntilReady`, `waitUntilDeleted` |
| VirtualMachine | `waitUntilRunning`, `waitUntilDeleted` |

Every wait takes a `WaitOptions` value, `{ timeoutMs, pollIntervalMs, signal }`.
The defaults are ten minutes and one second. A wait that ends in a state the
resource cannot leave rejects with a `WorkloadTerminalError` carrying `kind`,
`workloadName`, `condition`, and the platform's explanation as `detail` when it
supplied one. A wait that exhausts its timeout rejects with a `TimeoutError`
`DOMException`, and one cancelled through `signal` rejects with the signal's
reason. None of those outcomes deletes anything.

## Identity and discovery

Sign-in is anonymous, so it uses a separate `Identity` client with no Account
or credential:

```ts theme={null}
import { Identity } from "@idyl-labs/sdk";

const identity = new Identity({ network: "idyl.network" });
await identity.startAuth({ email: "<email>" });
const verified = await identity.verifyAuth({ email: "<email>", code: "<code>" });
const token = await identity.getProfileAccessToken(verified.refreshToken, {
  account: "<organization>/<account>",
  profile: "<profile>",
});
```

| Method | Purpose |
| - | - |
| `startAuth` | Requests a one-time authentication code. Anonymous. |
| `resendAuth` | Sends the one-time code again. Anonymous. |
| `verifyAuth` | Verifies a one-time code and returns a refresh token. Anonymous. |
| `getProfileAccessToken` | Exchanges a refresh token for a profile access token. |
| `listAssumableProfiles` | Lists the profiles a refresh token may assume. |
| `signup` | Creates the first Account for an authenticated identity, after explicit terms acceptance. |
| `logout` | Revokes a refresh token. |
| `listTiers` | Lists the public Account tiers the Network offers. Anonymous. |

Every operation that needs a refresh token takes it as an argument and sends it
as the bearer credential of that one request. The client holds no credential of
its own.

Public region and Subnet discovery needs neither an Account nor a credential.
`Discovery` reads a Network's metadata endpoint anonymously. The same reads are
available as `idyl.regions` and `idyl.subnets` on an authenticated client,
where they are anonymous too: a bearer credential never reaches the metadata
endpoint.

```ts theme={null}
import { Discovery } from "@idyl-labs/sdk";

const discovery = new Discovery({ network: "idyl.network" });
const regions = await discovery.regions.list();
const subnets = await discovery.subnets.list();
```

## Logs, shells, and watches

`idyl.workloadSessions.logs` streams a container's logs over a WebSocket
session. The SDK reserves the session, presents its one-use grant as the first
message on the socket, delivers the frames to the sinks it is given in order,
and closes the socket on completion or cancellation:

```ts theme={null}
await idyl.workloadSessions.logs(
  { target: { kind: "Sandbox", name: "dev" }, request: { follow: true } },
  (chunk) => {
    process.stdout.write(chunk);
  },
  (chunk) => {
    process.stderr.write(chunk);
  },
);
```

A sink is a `WritableStream<Uint8Array>` or a function called with each chunk,
and one sink may serve both streams. A session that ends in an error frame
rejects with `WorkloadSessionError`, whose `code` is the session's closed
vocabulary. A transport failure rejects with `WorkloadSessionTransportError`,
whose `code` is the transport's closed vocabulary and whose message is a fixed
sentence. Cancelling the request signal closes the socket and rejects with the
signal's reason.

The SDK speaks no SSH. `openShellTransport` reserves a shell session for the
caller's ephemeral key, receives a one-use certificate bound to that key, and
opens the byte duplex an SSH implementation authenticates over. The caller owns
the key pair and gives the SDK the public key in OpenSSH authorized-key form;
the implementation that attaches owns the key, the handshake, and the host-key
decision:

```ts theme={null}
const shell = await idyl.workloadSessions.openShellTransport({
  target: { kind: "Sandbox", name: "dev" },
  publicKey: sessionPublicKey,
});
try {
  // shell.ssh names the username, the one-use certificate bound to the
  // key, and the host CAs a host key may be accepted from; shell.transport
  // is the readable and writable byte pair an SSH client speaks over.
  await runSshClient(shell.ssh, shell.transport);
} finally {
  await shell.transport.close();
}
```

A watch is an ordered stream of typed events: an async iterable with a `close`.
`watchWorkloads` streams Workload changes in the resolved Subnet, narrowed to
the client's Namespace when it has one. `watchRuntimePods` streams the observed
pods of the client's Namespace and `watchRuntimeNodes` the observed Nodes of
its Subnet. Take a Workload snapshot before watching when a consumer needs a
gap-free initial view, then pass its `snapshotCursor` to the matching watch:
every change after the snapshot is delivered exactly once and nothing before it
is redelivered. Always close a watch a consumer leaves early.

```ts theme={null}
const snapshot = await idyl.workloads.list({ kind: "Deployment" });
const watch = await idyl.watchWorkloads({
  kinds: ["Deployment"],
  cursor: snapshot.snapshotCursor,
});
try {
  for await (const event of watch) {
    handle(event); // a LiveEnvelope; decodeObserved(event.object) types it
  }
} finally {
  await watch.close();
}
```

Every watch is opened before it is handed back, so a refused stream rejects
there with an `ApiError`. Iteration ends when the platform closes the stream,
rejects with `LiveStreamError` on an error frame, and rejects with the request
signal's reason on cancellation. Events of a type the SDK does not name are
ignored, so a newer platform never ends a watch.

## Errors and cancellation

A non-2xx response becomes an `ApiError` carrying `status`, the stable `code`
when the endpoint publishes one, `message`, `details`, `requestId`, and
`retryAfterMs`. Branch on the fields or on the predicates `isApiStatus`,
`isApiCode`, and `isNotFound`. The message is written for logs and for people
and is not a contract.

```ts theme={null}
import {
  ERROR_CODE_NAMESPACE_TERMINATING,
  PlacementUnavailableError,
  isApiCode,
  isApiStatus,
  isNotFound,
} from "@idyl-labs/sdk";

try {
  await idyl.sandboxes.delete("dev");
} catch (error) {
  if (isNotFound(error)) {
    // The Sandbox is already gone.
  } else if (isApiStatus(error, 429)) {
    await new Promise((resolve) => {
      setTimeout(resolve, error.retryAfterMs ?? 1_000);
    });
  } else if (isApiCode(error, ERROR_CODE_NAMESPACE_TERMINATING)) {
    // The Namespace is being deleted and accepts no further writes.
  } else if (error instanceof PlacementUnavailableError) {
    // error.reason names why the Subnet cannot accept the request.
    throw error;
  } else {
    throw error;
  }
}
```

Failures that are not HTTP responses have their own classes, each carrying the
fields to branch on:

| Class | Reports |
| - | - |
| `PlacementUnavailableError` | A Subnet the Account cannot deploy to. `reason` is the classification. |
| `WorkloadTerminalError` | A wait that can no longer be satisfied. `kind`, `workloadName`, `condition`, and `detail`. |
| `WorkloadSessionError` | A failure the Workload session reported. `code` is the session's closed vocabulary. |
| `WorkloadSessionTransportError` | A failure of the transport carrying a session. `code` is the transport's closed vocabulary. |
| `WorkloadSessionReservationError` | A reservation the SDK could not hand back. `sessionId` names the reservation left to expire. |
| `LiveStreamError` | An error frame inside a watch. `code` is the frame's classification. |
| `ExitError` | A non-zero remote exit status, for an attaching shell implementation to report. The SDK never throws it. |

Every class sets `name` and preserves its `cause`. Invalid arguments and
configuration, a malformed option, a manifest whose scope disagrees with the
client's, or a Workload operation on a client with no Namespace or placement,
throw a `TypeError` before any request is made.

```ts theme={null}
import { WorkloadTerminalError } from "@idyl-labs/sdk";

try {
  const job = await idyl.jobs.createAndWait({
    name: "migrate",
    container: { image: "<image>" },
  });
  await job.waitUntilComplete();
} catch (error) {
  if (error instanceof WorkloadTerminalError) {
    // error.kind, error.workloadName, error.condition, and error.detail
    // say which wait can no longer be satisfied and why.
  }
  throw error;
}
```

The SDK issues no automatic retries. A request that fails is returned to the
caller, which knows whether the operation is safe to repeat.

Every operation accepts an `AbortSignal`, as `signal` in the request options
of a call or in the `WaitOptions` of a wait. Cancellation propagates through
the credential lookup, the request, the body read, and any attached stream or
session, and the operation rejects with the signal's reason. Each request is
also bounded by `requestTimeoutMs`, 30 seconds by default; a request that
exceeds it rejects with a `TimeoutError` `DOMException`. Cancelling an
operation never deletes a resource.

```ts theme={null}
const page = await idyl.pods.list(
  { limit: 20 },
  { signal: AbortSignal.timeout(10_000) },
);
```

## Manifests

A manifest is an envelope of `kind`, `metadata`, and `spec`, with `status`
added by the platform on read, and the SDK ships every kind as a plain
read-only type with the same field names and wire spellings as the Go SDK.
Decoding is lenient on read and strict on write. `decodeSubmission` prepares a
manifest for submission: it takes JSON text or a parsed value and refuses an
unknown field, a duplicate key, a value of the wrong type, and a missing
envelope field, throwing a `StrictDecodeError` that names the field by its full
path from the document root. `decodeObserved` reads a manifest the platform
reported: a field the SDK does not declare is carried, a value it cannot read is
carried as it arrived, and a missing field reads as its zero value. Each kind's
`validate*` function, such as `validateSandbox`, holds a manifest to the
contract the Go SDK enforces and throws a `TypeError` naming the first
violation.

```ts theme={null}
import {
  StrictDecodeError,
  decodeSubmission,
  validateSandbox,
} from "@idyl-labs/sdk";

try {
  const sandbox = decodeSubmission(manifestJson, "Sandbox");
  validateSandbox(sandbox);
  await idyl.sandboxes.createManifest(sandbox);
} catch (error) {
  if (error instanceof StrictDecodeError) {
    // error.reason and error.path name the field that was refused,
    // such as "unknown_field" at "spec.template.spec.hostPath".
  }
  throw error;
}
```

## Compatibility

Every exported symbol and every declaration in the package's type declarations
is a long-term commitment. JSON field names are compatibility commitments
shared with the Go SDK: the wire format is the Go SDK's, held by a conformance
suite that decodes and re-encodes the Go SDK's golden fixtures at a pinned
commit. New fields preserve decoding compatibility. A rename, a removal, a
discriminator change, or a change to a semantic default is a versioned contract
decision made in the Go SDK first and then adopted here.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.