Skip to content

SDKs and CLI

TypeScript SDK

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

On this page

@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).

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
});
OptionDefaultMeaning
apiKeyVMYARD_API_KEYA vmy_… key. Required.
baseUrlVMYARD_API_URLYour API origin. Required: there is no default host.
organizationIdVMYARD_ORGANIZATION_IDSent as X-Organization-ID. Optional with an API key; must match the key.
maxRetries2Retries for retryable failures.
timeoutMs60000Timeout per HTTP attempt.
fetchglobalThis.fetchA 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();
CallRouteNotes
vy.machines.create(params)POST /v1/machinesResolves plan, image, and region names or IDs to exact versions; waits for the operation; returns a Machine.
vy.machines.beginCreate(params)POST /v1/machinesSubmits without waiting; returns the accepted operation.
vy.machines.buildCreateRequest(params)—The exact request create would send.
vy.machines.list({ limit, cursor })GET /v1/machinesOne 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 …/deleteReturns the operation.
vy.machines.usage(id, { since, until }) / machine.usage()GET …/usageSeconds 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). 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

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

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

ClassWhen
APIErrorAny 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, SpendCapReachedError402 billing admission.
TrialMachineClassNotAllowedError403 trial size refusal.
LimitReachedError429 limit_reached, with limit and retryAfter.
OperationErrorAn operation ended failed, uncertain, or correlation_miss; has operation, status, errorCode.
OperationTimeoutErrorWaiting ran out of time; the operation continues.
ExecStreamErrorA streamed exec ended with an error frame or broke off.
DeviceAuthorizationErrorDevice sign-in was denied, expired, or invalid.
ConnectionError, ConfigurationError, ResolutionErrorNetwork 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 directly; wire types such as MachineDto and OperationDto are exported for that.