npm install @boatdev/sdk
BoatApi, configure auth, and call methods like sandbox.create, sandbox.prompt, and sandbox.events.
Configure
import { BoatApi, Configuration, waitUntilReady, waitForPrompt } from "@boatdev/sdk";
const sandbox = new BoatApi(new Configuration({
basePath: process.env.BOAT_BASE_URL ?? "https://boat.dev/api/v1",
accessToken: process.env.BOAT_API_KEY ?? (() => {
throw new Error("Set BOAT_API_KEY from the Boat dashboard API keys tab.");
})(),
}));
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022", "DOM"],
"types": ["node"],
"strict": true
}
}
fetch. Older runtimes need a fetch polyfill. For ESM examples, set "type": "module" in package.json; CommonJS projects can use the require example below.
Create, prompt, and clean up
import { BoatApi, Configuration } from "@boatdev/sdk";
const sandbox = new BoatApi(new Configuration({
basePath: process.env.BOAT_BASE_URL ?? "https://boat.dev/api/v1",
accessToken: process.env.BOAT_API_KEY ?? (() => {
throw new Error("Set BOAT_API_KEY from the Boat dashboard API keys tab.");
})(),
}));
async function main() {
let sandboxId: string | undefined;
try {
const created = await sandbox.create({
createSandboxRequest: { ttlSeconds: 1800 },
});
sandboxId = created.sandbox.id;
await sandbox.update({
sandboxId,
updateSandboxRequest: { name: "sdk-demo" },
});
await waitUntilReady(sandbox, sandboxId);
const queued = await sandbox.prompt({
sandboxId,
promptRequest: {
provider: "codex",
prompt: "Inspect the repository and summarize the test command.",
},
});
const run = await waitForPrompt(sandbox, sandboxId, queued.promptId);
console.log(run.status);
const events = await sandbox.events({ sandboxId, limit: 50, type: "prompt,response" });
console.log(events.events);
} finally {
if (sandboxId) await sandbox.stop({ sandboxId });
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Use
ttlSeconds when creating a sandbox. Set a friendly name afterward with sandbox.update.Machine size
Passtype to create a bigger Boat. large consumes machine time at 2x, small at half rate. A
fork inherits the source sandbox’s type, and passing type to resume or fork moves a sandbox between
sizes.
const created = await sandbox.create({
createSandboxRequest: { type: "large", ttlSeconds: 3600 },
});
const info = await sandbox.get({ sandboxId: created.sandbox.id });
console.log(info.sandbox.type, info.sandbox.vcpu, info.sandbox.memoryGB);
Per-sandbox environment variables
Passenv to inject variables into a single sandbox, on top of your account-level secrets
(per-sandbox values win on name conflicts). At most 100 variables, 64KB total.
const created = await sandbox.create({
createSandboxRequest: {
ttlSeconds: 3600,
env: { DATABASE_URL: "postgres://user:pass@host:5432/app", FEATURE_FLAG: "1" },
},
});
env unless the fork supplies its own.
No-env sandboxes
When you hand a sandbox to your own end users, create it withnoEnv: true so none of your
account secrets reach it. Model keys, GitHub token, dashboard environment variables, SSH
identity, and secret files are all withheld. SSH, desktop, snapshots, public URLs, and
forks still work. A fork of a no-env Boat is always no-env and cannot be downgraded.
const created = await sandbox.create({
createSandboxRequest: { ttlSeconds: 1800, noEnv: true },
});
await sandbox.resume({
sandboxId,
resumeRequest: { noEnv: true },
});
const forked = await sandbox.fork({
sandboxId,
forkRequest: { noEnv: true },
});
CommonJS
const { BoatApi, Configuration } = require("@boatdev/sdk");
const sandbox = new BoatApi(new Configuration({
basePath: process.env.BOAT_BASE_URL || "https://boat.dev/api/v1",
accessToken: process.env.BOAT_API_KEY,
}));
Methods
All methods are called onBoatApi. Request bodies are plain objects typed by the exported model interfaces.
| Method | Arguments | Returns | Use |
|---|---|---|---|
me() | none | Promise<MeResponse> | Get the authenticated Boat account user. |
limits() | none | Promise<LimitsResponse> | Check whether the account can create or operate sandboxes before starting work. |
repos({ sync, limit, cursor, sort, q, selected }?) | optional sync, pagination, search, and selected-only filters | Promise<ReposResponse> | List GitHub installations, repositories, and selected repositories. |
selectRepo({ repoSelectionRequest }) | repositoryId, optional baseBranch | Promise<RepoSelectionResponse> | Select a repository for future sandboxes. Use databaseId from repos() as repositoryId. |
apiKeys() | none | Promise<ApiKeysResponse> | List API key metadata, including 30-day request totals and live resource counts. Raw secrets are not returned. |
secrets() | none | Promise<SecretsResponse> | Read the current environment variables and secret files configured for sandboxes. |
updateSecrets({ secretsUpdateRequest }) | envContents, secretFiles | Promise<SecretsResponse> | Replace the complete secret setup. Send every env var and file that should remain. |
sandboxes({ limit, cursor, sort, state }?) | optional pagination and state filter | Promise<SandboxListResponse> | List Sandboxes for the account. |
create({ createSandboxRequest }?) | optional type, ttlSeconds, env, environment, noEnv, setupScript, from, org | Promise<CreateSandboxResponse> | Create a sandbox. Use ttlSeconds: null to disable auto-stop. Set noEnv: true to withhold all account secrets (for sandboxes you give to your users). from starts the sandbox from a named snapshot; org bills it to an organization wallet. |
get({ sandboxId }) | Sandbox id | Promise<SandboxInfoResponse> | Fetch the latest sandbox state and connection fields. |
usage({ sandboxId, since, until }) | Sandbox id, optional window as ISO 8601 or Unix seconds | Promise<SandboxUsageResponse> | Billable machine time one sandbox consumed, and its cost at list price, for per-sandbox billing of your own users. Omit the window for the sandbox’s whole life. See Per-sandbox usage. |
update({ sandboxId, updateSandboxRequest }) | Sandbox id plus name, ttlSeconds, and/or subdomain | Promise<SandboxInfoResponse> | Rename a sandbox, change its auto-stop TTL, or rename its subdomain (re-points every live URL with no downtime). |
stop({ sandboxId, stopRequest }?) | Sandbox id, optional force | Promise<SandboxActionResponse> | Stop/archive a sandbox. A stop snapshots the disk first; if that is failing the stop is refused and the sandbox keeps running (you are not billed for that time). Pass stopRequest: { force: true } to stop anyway and permanently lose everything written since the last successful snapshot. |
resume({ sandboxId, resumeRequest }) | Sandbox id, optional type, env, environment, ttlSeconds, noEnv | Promise<SandboxActionResponse> | Resume an archived sandbox. Set resumeRequest: { noEnv: true } to convert it to no-env while scrubbing inherited owner secrets, or type to resume it onto a different machine size. ttlSeconds is omitted to keep the sandbox’s current auto-stop. Poll get() until it is ready. |
fork({ sandboxId, forkRequest }) | Sandbox id, optional env, environment, type, ttlSeconds, noEnv | Promise<SandboxActionResponse> | Create a new Sandbox from the source sandbox snapshot. Set forkRequest: { noEnv: true } to create a no-env fork, or type to fork onto a different machine size. A fork does not inherit the source’s auto-stop: omit ttlSeconds and it gets the 1 hour default. The source sandbox is never modified. |
deleteSandbox({ sandboxId }) | Sandbox id | Promise<SandboxActionResponse> | Permanently delete a sandbox and its snapshots. Force-stops it, then deletes every snapshot chain only this sandbox uses. It cannot be resumed or forked afterwards and there is no undo, so use stop() if you want the data back later. Snapshot data a fork, a resume or a named snapshot still reads is kept. See Snapshots. |
prompt({ sandboxId, promptRequest }) | Sandbox id plus provider, prompt, optional model, optional reasoningEffort, optional new, optional conversationId | Promise<PromptResponse> | Queue work inside a sandbox. Returns promptId, promptRun.status, and the conversationId it ran in. Set new: true for a new conversation or conversationId to continue one. |
promptRunStatus({ sandboxId, promptId }) | Sandbox id and prompt id | Promise<PromptRunResponse> | Read first-class prompt run status, including its conversationId. |
events({ sandboxId, limit, cursor, sort, type, conversation }) | Sandbox id plus optional pagination/filtering | Promise<EventsResponse> | Read typed event history for a sandbox. Streams all conversations by default; pass conversation to filter. Each event carries conversationId. |
readFile({ sandboxId, path, encoding }) | Sandbox id and relative path | Promise<FileReadResponse> | Deterministically read a text/base64 file from the sandbox work directory. |
writeFile({ sandboxId, fileWriteRequest }) | Sandbox id plus relative path/content | Promise<FileWriteResponse> | Deterministically write a text/base64 file. |
command({ sandboxId, commandRequest }) | Sandbox id plus command/cwd/timeout | Promise<CommandResponse> | Execute a bounded command in the sandbox work directory. |
artifact({ sandboxId, path }) | Sandbox id and relative path | Promise<Blob> | Download an artifact as bytes. |
interrupt({ sandboxId }) | Sandbox id | Promise<SandboxActionResponse> | Interrupt current work in a running sandbox. |
desktop({ sandboxId, vnc, theme, requestBody }) | Sandbox id plus optional desktop parameters. For VNC, send requestBody: { publicAccess: true } to return a URL without _token. | Promise<DesktopResponse> | Create or fetch a desktop streaming URL. Treat returned URLs as secrets. If provisioning is true, poll again. |
sshKey({ sandboxId, sshKeyRequest }) | Sandbox id plus public SSH key | Promise<SshKeyResponse> | Add a public SSH key for sandbox SSH access. |
listSnapshots({ limit, cursor, sort }?) | optional pagination | Promise<SnapshotListResponse> | List completed snapshots across all sandboxes; each item carries its sandboxId. |
listSandboxSnapshots({ sandboxId, limit, cursor, sort }) | Sandbox id plus optional pagination | Promise<SnapshotListResponse> | List completed snapshots for one sandbox. |
getLatestSandboxSnapshot({ sandboxId }) | Sandbox id | Promise<SnapshotLatestResponse> | Most recent completed snapshot for a sandbox, or null. |
getSnapshotTree({ snapshotId }) | Snapshot id | Promise<SnapshotTreeResponse> | Flat file/folder listing with sizes for a snapshot. Works with the sandbox stopped or archived. |
getSnapshotFile({ snapshotId, path }) | Snapshot id plus a path from the tree (empty for the whole snapshot) | Promise<Blob> | Download one file’s bytes, or a folder as a .tar archive, straight from the snapshot. Works with the sandbox stopped or archived. |
getSnapshotDownload({ snapshotId }) | Snapshot id | Promise<SnapshotDownloadResponse> | Signed chunk URLs to rebuild the snapshot’s full filesystem client-side. |
Browse a stopped sandbox’s filesystem
Snapshot reads never touch the machine, so they work while the sandbox is archived:const latest = await sandbox.getLatestSandboxSnapshot({ sandboxId });
const tree = await sandbox.getSnapshotTree({ snapshotId: latest.snapshot.id });
const blob = await sandbox.getSnapshotFile({ snapshotId: latest.snapshot.id, path: "projects/app/.env" });
Waiters and helpers
The package exports first-class waiters and deterministic file/command helper functions:import { waitUntilReady, waitUntilIdle, waitForDesktop, waitForPrompt, waitForPromptDone, streamEvents, streamPrompt, stopAndRemove, readText, writeText, execCommand } from "@boatdev/sdk";
await waitUntilReady(sandbox, sandboxId);
const queued = await sandbox.prompt({ sandboxId, promptRequest: { provider: "codex", prompt: "Run tests" } });
await waitForPrompt(sandbox, sandboxId, queued.promptId);
const publicVnc = await waitForDesktop(sandbox, sandboxId, { publicAccess: true });
await writeText(sandbox, sandboxId, "notes/result.txt", "done\n");
const result = await execCommand(sandbox, sandboxId, "cat notes/result.txt");
await stopAndRemove(sandbox, sandboxId); // stop, keep the snapshots
await stopAndRemove(sandbox, sandboxId, { delete: true }); // delete the sandbox and its snapshots
waitForPrompt/waitForPromptDone instead of inferring completion from sandbox.state plus event polling. Use streamPrompt or streamEvents when you need incremental response/tool-call events as work runs.
Streaming responses and tool calls
The SDK now exportsstreamEvents and streamPrompt for response streaming. They use the sandbox v1 events cursor API under the hood, so no separate SSE or WebSocket endpoint is required. response events carry text in event.data.content; streaming partials set event.data.isStreaming; tool-call events are response events with event.data.tools.
import { BoatApi, Configuration, streamPrompt } from "@boatdev/sdk";
const sandbox = new BoatApi(new Configuration({
basePath: "https://boat.dev/api/v1",
accessToken: process.env.BOAT_API_KEY!,
}));
const stream = streamPrompt(sandbox, sandboxId, {
provider: "codex",
prompt: "Run pwd and ls, then summarize the result.",
});
for await (const event of stream) {
if (event.type !== "response") continue;
const data = event.data;
if (data.tools?.length) console.log("tools", data.tools);
if (data.content) process.stdout.write(data.content);
if (data.isStreaming) process.stdout.write("\n[partial]\n");
}
streamEvents(sandbox, sandboxId, { type: "prompt,response" }) and stop it with an AbortController.
Operation request types
These exported interfaces wrap method parameters forBoatApi methods.
| Type | Fields | Used by |
|---|---|---|
ArtifactRequest | sandboxId, path | artifact() |
SandboxesRequest | limit, cursor, sort, state | sandboxes() |
CommandOperationRequest | sandboxId, commandRequest | command() |
CreateRequest | createSandboxRequest | create() |
DesktopRequest | sandboxId, vnc, theme, requestBody | desktop(); set requestBody.publicAccess for an ungated VNC URL. |
EventsRequest | sandboxId, limit, cursor, sort, type, conversation | events(); conversation filters to one conversation. |
ForkRequest | env, environment, type, ttlSeconds, noEnv | fork() |
GetRequest | sandboxId | get() |
UsageRequest | sandboxId, since, until | usage() |
InterruptRequest | sandboxId, conversation | interrupt(); omit conversation to stop the whole sandbox, or pass one to scope it. |
PromptOperationRequest | sandboxId, promptRequest | prompt() |
PromptRunStatusRequest | sandboxId, promptId | promptRunStatus() |
ReadFileRequest | sandboxId, path, encoding | readFile() |
RemoveRequest | sandboxId | remove() |
ReposRequest | sync, limit, cursor, sort, q, selected | repos() |
ResumeRequest | type, env, environment, ttlSeconds, noEnv | resume() |
SelectRepoRequest | repoSelectionRequest | selectRepo() |
SshKeyOperationRequest | sandboxId, sshKeyRequest | sshKey() |
StopRequest | sandboxId | stop() |
UpdateRequest | sandboxId, updateSandboxRequest | update() |
UpdateSecretsRequest | secretsUpdateRequest | updateSecrets() |
WriteFileRequest | sandboxId, fileWriteRequest | writeFile() |
SandboxesSortEnum | "asc", "desc" | sandboxes({ sort }) |
DesktopVncEnum | 1 | desktop({ vnc }) |
DesktopThemeEnum | "light", "dark" | desktop({ theme }) |
EventsSortEnum | "asc", "desc" | events({ sort }) |
ReadFileEncodingEnum | "utf8", "base64" | readFile({ encoding }) |
ReposSortEnum | "asc", "desc" | repos({ sort }) |
Model types
TypeScript models use camelCase fields.| Type | Fields | Notes |
|---|---|---|
ApiKey | id, name, keyPrefix, keyLastFour, createdAt, lastUsedAt, usage, resources | Metadata only; not the raw secret. Includes 30-day request total and live resource counts. |
ApiKeysResponse | ok, type, apiKeys | API key metadata response. Raw secrets are not returned. |
Sandbox | id, name, state, type, vcpu, memoryGB, billingMultiplier, url, ip, createdAt, updatedAt, archiveAfter, desktopAvailable, desktopUrl, snapshotAvailable, snapshotCompletedAt | desktopUrl can contain a token; redact it. |
SandboxActionResponse | ok, type, id, status, sandbox | Returned by lifecycle actions such as stop, resume, fork, and interrupt. |
SandboxInfoResponse | ok, type, sandbox | Returned by get() and update(). |
SandboxUsageResponse | ok, type, sandboxId, sandboxType, billingMultiplier, since, until, seconds, dollars, secondsPerDollar, running | Returned by usage(). seconds has the type multiplier applied; running means it is still growing. |
SandboxEvent | id, type, timestamp, taskId, conversationId, data, plus additional fields | Extensible event object returned inside EventsResponse.events. conversationId tells you which conversation the event belongs to. Branch on each event type. |
SandboxListResponse | ok, type, sandboxes, pageInfo | Returned by sandboxes(). |
CommandRequest | command, cwd, timeoutSeconds | Bounded command execution request. |
CommandResponse | ok, type, success, exitCode, signal, stdout, stderr, stdoutTruncated, stderrTruncated, timedOut, cwd, startedAt, finishedAt | Returned by command() and execCommand(). |
CompletionEvent | id, type, timestamp, taskId, data | Event subtype for task_notification and compaction_complete. |
CreateSandboxRequest | type, ttlSeconds, env, environment, noEnv, setupScript, from, org | type is small, default or large (see Machine size). ttlSeconds is the delay before auto-stop; null disables it. from names a snapshot to start from; org bills the sandbox to an organization wallet. |
CreateSandboxResponse | ok, type, status, ttlSeconds, sandbox | Returned immediately after creation starts. |
DesktopResponse | ok, type, success, desktopUrl, ip, mode, provisioning, message | If provisioning is true, poll desktop() again. |
ErrorEnvelope | ok, type, status, code, message, requestId, error | Non-2xx response body. Include requestId in support logs. |
ErrorEnvelopeError | code, message, status, details | Structured error details. |
ErrorEvent | id, type, timestamp, taskId, data | Event subtype for usage_limit and shield. |
EventsResponse | ok, type, id, events, pageInfo | events contains sandbox event objects. |
FileReadResponse | ok, type, success, path, encoding, size, content | Returned by readFile() and readText(). |
FileWriteRequest | path, content, encoding | Write a UTF-8 string or base64 payload. |
FileWriteResponse | ok, type, success, path, encoding, size | Returned by writeFile() and writeText(). |
LimitsFields | accessTier, blockedReason, currentLimits, standardLimits, trialLimits, upgradeEffects, canStart, checkoutRequired, startBlockedReason, contactMessage, activeSandboxes, activeStates, maxActiveSandboxes, maxCreationRequestsPerMinute, maxCreationRequestsPerDay, startLimits, starts, accountPlan, plan, planName, serviceAccount, unlimited, hasPaymentHistory, _package, subscriptionQuotaSeconds, subscriptionRemainingSeconds, packBalanceSeconds, packBalanceHours, packBalanceDollars, creditPurchasedSeconds, creditUsedSeconds, liveUsageSeconds, creditSecondsPerDollar, billingStatus, subscriptionStatus, subscriptionCancelAtPeriodEnd, hasSubscription, subscriptionTrialEndsAt, subscriptionCurrentPeriodEnd, creditBalanceSeconds, creditBalanceHours | Shared limit and billing-access fields. Use canStart and startBlockedReason before creating sandboxes. starts.*.remaining is remaining machine starts in each rolling window. serviceAccount/unlimited identify admin-created automation accounts. |
LimitsFieldsCurrentLimits | activeSandboxes, creationRatePerMinute, creationRequestsPerDay | Numeric quota limits. |
LimitsResponse | ok, type, plus all LimitsFields fields | Returned by limits(). |
MeResponse | ok, type, user | Authenticated account response. |
MeResponseAllOfUser | login, email | User identity fields. |
PageInfo | nextCursor, hasMore, limit | Optional pagination metadata on list responses. |
PromptEvent | id, type, timestamp, taskId, conversationId, data | Event subtype for prompt. |
PromptEventData | prompt, status, isReverted | Prompt event payload. |
PromptRequest | provider, model, reasoningEffort, new, conversationId, prompt | provider is codex, claude-code (alias claude), pi, opencode, prime-agent (alias prime), or kimi; omit it for the dashboard default. Omit model/reasoningEffort to use saved defaults (live catalog: GET /provider-models). Set new: true or conversationId to control the conversation. |
PromptResponse | ok, type, id, promptId, conversationId, promptRun, status, provider, model, reasoningEffort | Returned after work is queued. conversationId is the conversation the prompt runs in. |
PromptRun | id, promptId, sandboxId, status, done, createdAt, model, reasoningEffort, conversationId | First-class prompt run state. |
PromptRunResponse | ok, type, id, promptRun | Returned by promptRunStatus(). |
ResponseEvent | id, type, timestamp, taskId, conversationId, data | Event subtype for response. |
ResponseEventData | content, model, tools, isStreaming | Response event payload. |
RepoSelectionRequest | repositoryId, baseBranch | repositoryId is a databaseId from repos(). |
RepoSelectionResponse | ok, type, success, environmentId, selectedRepositories | Returned by selectRepo(). |
ReposResponse | ok, type, installations, environmentId, selectedRepositories, pageInfo | Repository inventory and current selections. |
Repository | id, databaseId, name, fullName, _private, permissions, pushedAt | Use databaseId when selecting a repository. |
RepositoryInstallation | type, accountLogin, accountAvatarUrl, repositories | Group of repositories available through one installation/account. |
SecretFile | path, contents | Treat contents as sensitive. |
SecretsResponse | ok, type, success, environmentId, envContents, secretFiles, pushed | Current secret setup. Treat envContents and secretFiles as sensitive. |
SecretsUpdateRequest | envContents, secretFiles | Full replacement request for secrets. |
SelectedRepository | id, databaseId, name, fullName, _private, permissions, pushedAt, baseBranch, setupRoutineId, setupScript, setupBlocking | Repository selected for future sandboxes. |
SshKeyRequest | key | Public SSH key in OpenSSH format. |
SshKeyResponse | ok, type, success, machineIp, sshUser | Returned after adding an SSH key. |
SuccessBase | ok, type | Base success-envelope fields. |
UnknownEvent | type, plus additional properties | Forward-compatible fallback for event types the SDK does not model yet. |
UpdateSandboxRequest | name, ttlSeconds | Send only fields you want to change. ttlSeconds: null disables auto-stop. |
Errors
Non-2xx responses reject with aResponseError. Read the status and parse the JSON body for the structured Boat error envelope. Redact API keys, Boat secrets, SSH keys, and desktop URLs.
import { ResponseError } from "@boatdev/sdk";
try {
await sandbox.get({ sandboxId: "bx_missing" });
} catch (error) {
if (error instanceof ResponseError) {
console.error(error.response.status);
console.error(await error.response.json());
}
}