# TypeScript SDK

> Reference for @vmyard/sdk, the TypeScript client for Node.js 20+ with no runtime dependencies.

`@vmyard/sdk` is the TypeScript client for the VMYard API. It runs on Node.js 20 or newer, ships ESM and CommonJS builds, and has no runtime dependencies (it uses the global `fetch`).

> [!NOTE]
> The SDK is in private preview and is not published to npm. Build it from the [erphq/vmyard-sdk](https://github.com/erphq/vmyard-sdk) repository (ask your VMYard contact for access), then add the local package to your project:

```sh
git clone https://github.com/erphq/vmyard-sdk.git
cd vmyard-sdk && npm ci && npm run build
cd your-project && npm install ../vmyard-sdk/typescript
```

## Create a client

```ts
import { VMYard } from "@vmyard/sdk";

const vy = new VMYard({
  apiKey: process.env.VMYARD_API_KEY, // default: VMYARD_API_KEY
  baseUrl: "https://api.example.com", // default: VMYARD_API_URL
});
```

| Option | Default | Meaning |
| --- | --- | --- |
| `apiKey` | `VMYARD_API_KEY` | A `vmy_…` key. Required. |
| `baseUrl` | `VMYARD_API_URL` | Your API origin. Required: there is no default host. |
| `organizationId` | `VMYARD_ORGANIZATION_ID` | Sent as `X-Organization-ID`. Optional with an API key; must match the key. |
| `maxRetries` | 2 | Retries for retryable failures. |
| `timeoutMs` | 60000 | Timeout per HTTP attempt. |
| `fetch` | `globalThis.fetch` | A custom fetch implementation. |
| `userAgent` | — | Extra `User-Agent` text. |

The client never follows redirects, so the key is never sent to another origin.

## Machines

```ts
const machine = await vy.machines.create({ name: "dev", plan: "small", autoStopAfterSeconds: 7200 });
const same = await vy.machines.find("dev"); // id, tenant alias, or unique display name
const page = await vy.machines.list({ limit: 20 }); // { items, nextCursor }
const all = await vy.machines.listAll();

await machine.update({ name: "api" });
await machine.stop();
await machine.start();
await machine.restart();
await machine.refresh(); // reload machine.data
await machine.delete();
```

| Call | Route | Notes |
| --- | --- | --- |
| `vy.machines.create(params)` | `POST /v1/machines` | Resolves plan, image, and region names or IDs to exact versions; waits for the operation; returns a `Machine`. |
| `vy.machines.beginCreate(params)` | `POST /v1/machines` | Submits without waiting; returns the accepted operation. |
| `vy.machines.buildCreateRequest(params)` | — | The exact request `create` would send. |
| `vy.machines.list({ limit, cursor })` | `GET /v1/machines` | One page. `listAll()` and `iterate()` follow every page. |
| `vy.machines.get(id)` / `find(idOrName)` | `GET /v1/machines/{id}` | `find` accepts an ID, tenant alias, or unique display name. |
| `vy.machines.update(id, params)` / `machine.update(params)` | `PATCH /v1/machines/{id}` | `name`, `autoStopAfterSeconds` (`null` = never), `neverAutoStop`. |
| `vy.machines.start/stop/restart(id, { wait })` | `POST …/{action}` | Returns the refreshed `Machine`. |
| `vy.machines.delete(id, { wait })` | `POST …/delete` | Returns the operation. |
| `vy.machines.usage(id, { since, until })` / `machine.usage()` | `GET …/usage` | Seconds and micros; defaults to the machine's lifetime. |

`create` params: `name`, `alias` (the immutable tenant alias; defaults to `name`, else `vy-xxxxxxxx`), `plan`, `image`, `region` (name, ID, or `{id, version}`; default the first in the catalog), `resources`, `backend`, `autoStopAfterSeconds`, `neverAutoStop`, and `setupScript`.

`machine.data` is the wire `MachineDto`. Pass `{ wait: false }` to any lifecycle call to get the accepted operation instead of waiting.

## Commands

```ts
const result = await machine.exec("uname -a", { timeoutMs: 30_000 });
// { operationId, exitCode, stdout, stderr, stdoutBytes, stderrBytes, timedOut, cancelled, truncated }

const stream = machine.exec(["npm", "test"], { stream: true });
for await (const frame of stream) {
  // started | stdout | stderr | exit | error; stdout/stderr carry decoded bytes in `data`
}
```

A string runs under `/bin/sh -c`; an array runs as argv. A non-zero exit, timeout, or cancellation raises `OperationError` (see [Running commands](/docs/guides/commands#the-result)). Streams skip frame types they don't know.

## Background commands

```ts
const { command_id } = await machine.commands.start("npm run dev");
const status = await machine.commands.status(command_id, { logOffset: 0, logLimit: 65536 });
const finished = await machine.commands.wait(command_id); // exited, killed, or failed_to_start
await machine.commands.kill(command_id);
```

## Files

```ts
await machine.files.write("app/config.json", JSON.stringify({ debug: true })); // string or bytes
const text = await machine.files.read("app/config.json");
const bytes = await machine.files.readBytes("out/image.png");
```

Reads follow a queued `202` answer and reassemble `file_ranges` for you.

## Operations

```ts
const operation = await vy.operations.get(operationId);
const done = await vy.operations.wait(operationId); // throws OperationError unless it succeeds
for await (const event of vy.operations.events(operationId)) console.log(event);
```

Waiting polls every 250 ms, growing 1.5× up to 2 s, for at most 10 minutes; past that it throws `OperationTimeoutError` and the operation keeps going. `events` streams server-sent events and resumes with `Last-Event-ID`.

## Catalog, identity, billing, webhooks

| Call | Route |
| --- | --- |
| `vy.me()` | `GET /v1/me` |
| `vy.catalog.plans()` / `images()` / `regions()` | `GET /v1/plans`, `/v1/images`, `/v1/regions` |
| `vy.billing.get()` / `limits()` / `invoicePreview()` | `GET /v1/billing`, `/v1/limits`, `/v1/billing/invoice-preview` |
| `vy.billing.usage({ since, until })` | `GET /v1/usage` |
| `vy.webhooks.list()` / `get(id)` | `GET /v1/webhooks[/{id}]` |
| `vy.webhooks.create({ name, url, events, enabled })` | `POST /v1/webhooks`; returns `{ webhook, secret }` |
| `vy.webhooks.update(id, params)` / `delete(id)` / `rotateSecret(id)` | `PATCH`/`DELETE /v1/webhooks/{id}`, `POST …/rotate-secret` |
| `vy.webhooks.deliveries(id, { limit })` | `GET /v1/webhooks/{id}/deliveries` |

The SDK reads billing and can change it too: spend caps, checkout, the payment portal, the trial, the plan, and automatic refill (`billing.update`, `billing.checkout`, `billing.portal`, and others). `vy.apiKeys.list()` exists, but the server refuses API keys on key routes (`403 session_required`).

## Device sign-in

```ts
import { DeviceAuthorization } from "@vmyard/sdk";

const device = new DeviceAuthorization({ baseUrl: process.env.VMYARD_API_URL });
const code = await device.requestCode({ clientName: "my-tool on build-host" });
console.log(`Open ${code.verification_uri} and enter ${code.user_code}`);
const token = await device.waitForToken(code); // token.access_token is the new API key
```

`waitForToken` waits `interval` seconds between polls, adds 5 seconds on `slow_down`, and raises `DeviceAuthorizationError` on `access_denied`, `expired_token`, or `invalid_grant`. See [CLI device login](/docs/guides/cli-login#the-protocol).

## Idempotency and retries

Every mutation gets a fresh UUID `Idempotency-Key` unless you pass `idempotencyKey`. Reads retry on connection errors and `408`, `429`, `502`, `503`, and `504`; mutations retry only on `429` and `503`, with the same key. Retries back off exponentially with jitter and honour `Retry-After`. A `Retry-After` longer than 60 seconds is raised at once with `retryAfterSeconds` set, so you can schedule it, and a `429 limit_reached` for concurrent machines is raised without retrying.

## Errors

| Class | When |
| --- | --- |
| `APIError` | Any problem-details answer. Subclasses: `BadRequestError` 400, `AuthenticationError` 401, `PaymentRequiredError` 402, `PermissionDeniedError` 403, `NotFoundError` 404, `ConflictError` 409, `PayloadTooLargeError` 413, `UnprocessableEntityError` 422, `RateLimitError` 429, `ServerError` 5xx. |
| `BillingRequiredError`, `SpendCapReachedError` | `402` billing admission. |
| `TrialMachineClassNotAllowedError` | `403` trial size refusal. |
| `LimitReachedError` | `429 limit_reached`, with `limit` and `retryAfter`. |
| `OperationError` | An operation ended `failed`, `uncertain`, or `correlation_miss`; has `operation`, `status`, `errorCode`. |
| `OperationTimeoutError` | Waiting ran out of time; the operation continues. |
| `ExecStreamError` | A streamed exec ended with an `error` frame or broke off. |
| `DeviceAuthorizationError` | Device sign-in was denied, expired, or invalid. |
| `ConnectionError`, `ConfigurationError`, `ResolutionError` | Network failure, missing key or URL, or a catalog name that didn't resolve. |

All of them extend `VMYardError`. `APIError` carries `status`, `code`, `title`, `message`, `remediation`, `correlationId`, `fieldErrors`, `retryAfterSeconds`, `problem`, and the raw `body`.

## More of the SDK

The SDK also covers ports and port tokens (`machine.ports`), SSH keys (`machine.setSshKey`), internet access (`machine.setNetwork`), snapshots, forks and restores, templates, environments, billing changes, metrics, command history, and organizations; this page shows the common calls. For anything else, call the [HTTP API](/docs/api) directly; wire types such as `MachineDto` and `OperationDto` are exported for that.
