froglet.
SDK

TypeScript

Programmatic access to the Froglet API. Mirrors the CLI one-for-one. Node 18+ or anywhere fetch is global.

Installed from npm as @kortix/froglet-sdk. No Node-specific imports; runs in Node, Bun, Deno, or the browser.

Install

npm  install @kortix/froglet-sdk
pnpm add @kortix/froglet-sdk

Quickstart

import { FrogletClient } from '@kortix/froglet-sdk';

const froglet = new FrogletClient({
  apiKey: process.env.FROGLET_API_KEY,
  // baseUrl defaults to https://api.froglet.sh
});

// 1. boot a sandbox
const created = await froglet.sandboxes.create({
  templateSlug: 'ubuntu-24.04',
  size: 'xs',
  slug: 'demo',
});

// 2. grab a handle with the id pre-bound
const sb = froglet.sandbox(created.id);
await sb.waitForState('running');

// 3. run something inside it
const result = await sb.exec('uname -a');
console.log(result.stdout);

// 4. tear it down
await sb.destroy();

You don't have to use the handle — every method is also available flat on the resource (froglet.sandboxes.exec(id, 'uname -a')). The handle just removes the repetition when you're doing several things with the same sandbox.

Client options

new FrogletClient({
  apiKey: 'fl_…',                          // required
  baseUrl: 'https://api.froglet.sh',       // default
  fetch: globalThis.fetch,                 // override for testing
  userAgent: '@kortix/froglet-sdk/0.1.0',  // sent on every request
  timeoutMs: 60_000,                       // per-request timeout
});

For session-cookie auth (e.g. from a browser talking to your own backend), pass a custom fetch that forwards the cookie. The SDK doesn't care how Authorization gets onto the request as long as the response envelope is { data: T } | { error: { code, message } }.

Recipes

Build a template from a Docker image, then use it

const build = await froglet.templateBuilds.create({
  name: 'acme-app',
  slug: 'acme-app',
  sourceKind: 'image',
  sourceRef: 'ghcr.io/acme/app:1.2',
});

// poll for state=ready (or stream the SSE log via templateBuilds.logStreamUrl)
let row;
do {
  await new Promise((r) => setTimeout(r, 5_000));
  const builds = await froglet.templateBuilds.list();
  row = builds.find((b) => b.id === build.id);
} while (row && row.state !== 'ready' && row.state !== 'error');

if (row?.state === 'ready') {
  const sb = await froglet.sandboxes.create({ templateSlug: 'acme-app', size: 's' });
  await froglet.sandboxes.waitForState(sb.id, 'running');
}

Exec with custom cwd + stdin

const r = await froglet.sandboxes.exec(
  sb.id,
  'python -c "import sys; print(len(sys.stdin.read()))"',
  {
    cwd: '/tmp',
    stdin: 'hello\nworld\n',
    timeoutMs: 30_000,
  },
);
console.log(r.exit_code, r.stdout, r.stderr);

Upload + download a file

await froglet.sandboxes.uploadFile(
  sb.id,
  '/data/input.txt',
  new TextEncoder().encode('hello from node'),
);

const bytes = await froglet.sandboxes.downloadFile(sb.id, '/data/output.txt');
const text = new TextDecoder().decode(bytes);

Expose a port

const port = await froglet.ports.create(sb.id, {
  internalPort: 8080,
  label: 'web',                  // public route at https://web--<slug>.froglet.sh
  // public: false               // gated route (token required); defaults to true
  // ttlSec: 600                 // auto-revoke after 10 min
});
console.log(port.url);

One-time share link:

const { token, expires_at } = await froglet.sandboxes.mintPortShareToken(sb.id, port.id, 300);

SSH

const access = await froglet.sshAccess.create(sb.id, { name: 'laptop', ttlHours: 4 });
console.log(access.command);
//   ssh -p 2222 <jwt>@ssh.froglet.sh

Snapshot, mutate, restore

const backup = await froglet.sandboxes.backup(sb.id, 'before-experiment');

// ... break things ...

const job = await froglet.sandboxes.restore(sb.id, backup.id);
// state=in_progress; poll listRestoreJobs to track

Mount a persistent volume

const vol = await froglet.volumes.create({ name: 'shared-data', size_gb: 25 });

await froglet.volumes.attach(sb.id, {
  volume_id: vol.id,
  mount_path: '/data',
  // mode: 'ro'  // read-only
});

// later, on a different sandbox:
await froglet.volumes.attach(otherSb.id, { volume_id: vol.id, mount_path: '/data' });

Wait for a sandbox to reach a state

await froglet.sandboxes.waitForState(sb.id, 'running', { timeoutMs: 30_000 });

Stream a PTY into the browser

const ticket = await froglet.sandboxes.ptyTicket(sb.id);
const ws = new WebSocket(ticket.url); // wss://…?ticket=<jwt>
ws.onmessage = (ev) => term.write(ev.data);
term.onData((data) => ws.send(data));

Sandbox handle

client.sandbox(id) returns a SandboxHandle whose methods are pre-bound to the id:

const sb = froglet.sandbox(sandboxId);

// status + lifecycle
const { sandbox, events } = await sb.fetch();
await sb.start();
await sb.stop();
await sb.destroy();
await sb.waitForState('running');

// exec + files
const r = await sb.exec('uname -a');
await sb.uploadFile('/tmp/x', new TextEncoder().encode('hi'));
const bytes = await sb.downloadFile('/tmp/x');
await sb.listFiles('/etc');
const tkt = await sb.ptyTicket();

// snapshots + backups
await sb.snapshot('checkpoint');
const b = await sb.backup('nightly');
await sb.listBackups();
await sb.restore(b.id);
await sb.setBackupPolicy({
  enabled: true,
  intervalMinutes: 60,
  retainCount: 3,
  retainDays: 7,
  storage: 'r2',
});

// ports + ssh access
const port = await sb.exposePort({ internalPort: 8080, label: 'web' });
await sb.revokePort(port.id);
const t = await sb.mintPortShareToken(port.id, 300);
const access = await sb.createSshAccess({ name: 'laptop', ttlHours: 4 });
await sb.revokeSshAccess(access.id);

// volumes
await sb.attachVolume({ volume_id: vol.id, mount_path: '/data' });
await sb.attachedVolumes();
await sb.detachVolume(vol.id);

The handle is purely a convenience — it forwards to client.sandboxes, client.ports, client.sshAccess, and client.volumes. Use whichever style fits the call site.

API surface

NamespaceWhat it covers
meWho am I (workspace, role, scopes).
sandboxesLifecycle, exec, files, PTY, snapshots, backups, restore.
templatesCatalog + publish/unpublish/delete.
templateBuildsBuild templates from Docker images.
portsPublic routes per sandbox.
sshAccessSSH bastion tokens per sandbox.
volumesPersistent storage + attach/detach/search.
keysWorkspace API keys.
membersWorkspace member management.
quotasUsage + limits + raise-quota requests.
adminPlatform-admin operations (hosts, orders, cells). Workspace keys get 403.

Each namespace returns plain TypeScript objects whose shapes are exported alongside the resource:

import type { Sandbox, ExecResult, Backup } from '@kortix/froglet-sdk';

Errors

Every request returns the unwrapped data field on success and throws FrogletApiError on 4xx/5xx:

import { FrogletApiError } from '@kortix/froglet-sdk';

try {
  await froglet.sandboxes.create({ templateSlug: 'unknown', size: 'm' });
} catch (e) {
  if (e instanceof FrogletApiError) {
    console.log(e.status, e.code, e.message);
    // 404 not_found "template 'unknown' not found"
  }
  throw e;
}
StatusCodeMeaning
401unauthorizedMissing / wrong API key.
403forbidden_scopeToken doesn't carry the scope this route needs.
403platform_admin_requiredWorkspace key on an admin/* route.
404not_foundResource doesn't exist or the token can't see it.
429quota_exceededWorkspace at the limit. Inspect error.payload.
422backup_failedBackup pipeline rejected the request (e.g. host loss).

Browser usage

The SDK has no Node-specific deps — globalThis.fetch, Uint8Array, and a small structured-clone-safe envelope decoder are the only platform surfaces. Don't put your raw API key in a browser bundle. Either use a session cookie + your own fetch wrapper, or talk to a Vercel/Cloudflare Worker function that forwards requests with the key server-side.

Gotchas

  • sandboxes.create takes slug, not name — the field that becomes the short human alias.
  • sandboxes.exec and sandboxes.listFiles return snake_case fields (exit_code, modified_ms). The rest of the API is camelCase. Already reflected in the SDK types.
  • volumes.create, volumes.attach, volumes.detach use snake_case input (size_gb, volume_id, mount_path).
  • sandboxes.stop can exceed 60s because the host takes an auto-snapshot before suspending. For hot sandboxes bump the timeout: new FrogletClient({ ..., timeoutMs: 180_000 }).
  • volumes.search returns 503 on prod today — the API process is missing FROGLET_VOLUMED_URL. Operator-side fix.
  • templateBuilds.list states are 'pending' | 'building' | 'done' | 'ready' | 'error'. 'done' is the in-between state after the build finishes but before the template row is created — usually transient.

CLI vs SDK

Use caseReach for
Try things by handCLI (froglet sb new --template ubuntu-24.04 --size xs --name demo).
Script lifecycleEither. CLI + `--json
Build into an appSDK — the whole point.
Slug vs UUIDCLI accepts either (resolves via list). SDK always uses UUIDs returned by list/create.

Both ship one-for-one. The dashboard uses the same SDK client.