Sandbox API and SDK reference
Lizard SDK is the client for Sandboxes on Lizard: @lizard-build/sdk on npm for TypeScript and JavaScript, and lizard-sdk on PyPI for Python, imported as lizard. Both call the same HTTP API. Find each sandbox call below under its TypeScript and Python names, with examples in both languages. For a guided first run, use the quickstart; for what Sandboxes are, read the overview.
Install and authenticate
npm install @lizard-build/sdk
pip install lizard-sdk
export LIZARD_API_KEY="<your-api-key>"Create an API key in Settings → API keys or with lizard keys create <name>. The SDK reads LIZARD_API_KEY; pass apiKey (Python: api_key) to use another key. Each sandbox needs a project: pass projectId (Python: project_id) or resolve an ID, slug, or name with project.
Create options
| TypeScript | Python |
|---|---|
projectId / project | project_id / project |
timeoutMs | timeout_ms |
region | region |
volumeName / volumeId | volume_name / volume_id |
size | size |
snapshotId | snapshot_id |
envs | envs |
metadata | metadata |
lizardToken | lizard_token |
apiKey | api_key |
import { Sandbox } from '@lizard-build/sdk';
const sandbox = await Sandbox.create('base', {
projectId: 'proj_123',
size: 'small',
timeoutMs: 10 * 60 * 1000,
envs: { MODE: 'test' },
metadata: { task: 'report-42' },
});from lizard import Sandbox
sandbox = Sandbox.create(
"base",
project_id="proj_123",
size="small",
timeout_ms=10 * 60 * 1000,
envs={"MODE": "test"},
metadata={"task": "report-42"},
)- The first argument is a template from the catalog in runtime and templates. Without one, the SDK boots
base. sizeis'mini','small','medium', or'large': 1 vCPU / 2 GB, 2 vCPU / 4 GB, 4 vCPU / 8 GB (the default), or 8 vCPU / 16 GB; see sizes and prices.'mini'starts only from thebase,claude,codexandinterpretertemplates, without a volume. During the trial,'small'is the default, 1 Small and 2 Mini run at a time, and only US regions work;'medium','large', a second Small, a third Mini or another region throwsAuthenticationErrorwith HTTP 403.sizedoes not apply to a sandbox restored from a snapshot; it runs at its source’s size.timeoutMsdefaults to 300000, five minutes.0disables expiration. During the trial,timeoutMsis at most 3600000 (1 hour),0gets 1 hour, and a longer value throwsAuthenticationErrorwith HTTP 403 andcodeset toTRIAL_SANDBOX_LIFETIME. See resources and timeouts.- Select a region explicitly when needed; a catalog entry does not guarantee free capacity. Leave
regionunset when you attach a volume: the sandbox runs where the volume is. metadatais your own labels;getInfo()returns it,list()does not.lizardTokentakes one of yourliz_API keys and signs inlizardinside the sandbox. Your key only authorizes the request: the sandbox gets a new key of its own, limited to the sandbox’s project and deleted with the sandbox, solizardthere can act only on that project. Thebasetemplate has nolizard.
Methods
| TypeScript | Python | What it does |
|---|---|---|
Sandbox.create(template, options) | Sandbox.create(template, ...) | Create in a project from a catalog template. |
Sandbox.connect(sandboxId) | Sandbox.connect(sandbox_id) | Attach to an existing sandbox; no automatic resume. |
Sandbox.list({ projectId }) | Sandbox.list(project_id=...) | List accessible sandboxes; inspect their status. |
sandbox.getInfo() | sandbox.get_info() | Fetch status, runtime, and metadata. |
sandbox.process.exec(cmd, opts) | sandbox.process.exec_(cmd, ...) | Run a shell command or an argv array; return stdout, stderr, and the exit code. |
sandbox.process.list() | sandbox.process.list() | List processes started with exec. |
sandbox.process.kill(pid, signal) | sandbox.process.kill(pid, signal) | Signal a process and its children; SIGTERM by default. |
sandbox.fs.write(path, data) | sandbox.fs.write(path, data) | Write text or bytes; create parent directories. |
sandbox.fs.read(path) | sandbox.fs.read(path) | Read a UTF-8 text file. |
sandbox.fs.readBytes(path) | sandbox.fs.read_bytes(path) | Read bytes from a file. |
sandbox.fs.list(path) | sandbox.fs.list(path) | List directory entries. |
sandbox.fs.stat(path) | sandbox.fs.stat(path) | Return type, size, mode, owner, and modification time. |
sandbox.fs.makeDir(path) | sandbox.fs.make_dir(path) | Create a directory and missing parents. |
sandbox.fs.move(from, to) | sandbox.fs.move(src, dst) | Move a file or directory. |
sandbox.fs.remove(path) | sandbox.fs.remove(path) | Remove a file or directory. |
sandbox.exposePort(port) | sandbox.expose_port(port) | Publish a port; return url, hostname, and the access token. |
sandbox.getHost(port) | sandbox.get_host(port) | Publish a port and return its hostname; requests need sandbox.accessToken. |
sandbox.unexpose(port) | sandbox.unexpose(port) | Remove a published port. |
sandbox.setTimeout(ms) | sandbox.set_timeout(ms) | Set the remaining lifetime, at least 1000 ms. |
sandbox.pause() / sandbox.resume() | sandbox.pause() / sandbox.resume() | Queue a pause or resume and return at once. |
sandbox.pause({ fast: true }) | sandbox.pause(fast=True) | Pause keeping files only, in about a second at any memory size; processes end and the resume boots the sandbox fresh. |
sandbox.waitForStatus(status) | sandbox.wait_for_status(status) | Wait for paused or running; throw if the pause failed. |
sandbox.fork({ count }) | sandbox.fork(count=...) | Copy a running sandbox 1 to 100 times, memory and processes included; each copy counts toward your running-sandbox limit. |
sandbox.snapshot(name) | sandbox.snapshot(name) | Save memory, processes and every file as a snapshot. It restores only in this sandbox’s project and region. |
Sandbox.waitForSnapshot(snapshotId) | Sandbox.wait_for_snapshot(snapshot_id) | Wait until a snapshot is ready. |
Sandbox.restore(snapshotId, { projectId, volumeId, timeoutMs }) | Sandbox.restore(snapshot_id, project_id=..., volume_id=..., timeout_ms=...) | Start a new sandbox from a snapshot, in the snapshot’s project and region. Any other projectId throws AuthenticationError with HTTP 403 and code set to SNAPSHOT_SCOPE. |
Sandbox.snapshots(projectId) | Sandbox.snapshots(project_id) | List the snapshots taken in a project. |
Sandbox.mySnapshots() | Sandbox.my_snapshots() | List every snapshot you took, in all your workspaces. |
Sandbox.deleteSnapshot(snapshotId) | Sandbox.delete_snapshot(snapshot_id) | Delete a snapshot; sandboxes restored from it keep running. |
sandbox.kill() | sandbox.kill() | Stop the sandbox and detach its volume. |
Run commands
process.exec (Python: exec_) runs a command and waits for it to finish. Pass onStdout or onStderr (Python: on_stdout, on_stderr) to receive output while the command runs, one line per call without the trailing newline; the call still returns the full result at the end. Pass an argv array instead of a string to skip the shell, which is the safe way to pass untrusted text as an argument.
const result = await sandbox.process.exec('npm test', {
workdir: '/app',
envs: { NODE_ENV: 'test' },
timeoutMs: 120_000,
onStdout: (line) => console.log(line),
});
if (result.exitCode !== 0) throw new Error(result.stderr);
await sandbox.process.exec(['grep', '-rn', '--', userQuery, '/workspace']);result = sandbox.process.exec_(
"npm test",
workdir="/app",
envs={"NODE_ENV": "test"},
timeout_ms=120_000,
on_stdout=print,
)
if result.exit_code != 0:
raise RuntimeError(result.stderr)
sandbox.process.exec_(["grep", "-rn", "--", user_query, "/workspace"])Command results contain stdout, stderr, and the exit code. A nonzero exit code is a result you check; request failures throw. timeoutMs (Python: timeout_ms) sets how long a command may run, from 1 to 600 seconds. Without it, the server stops the command after 150 seconds, and the TypeScript SDK stops waiting after 60 seconds and throws a DOMException named TimeoutError. Pass timeoutMs for any command that may run longer than a minute. process.exec accepts envs, workdir, and user; commands run as root unless you pass user. Create-time envs reach every command.
Files
Use fs.write for text or bytes and fs.read for text. For binary files, write bytes and read them back with fs.readBytes (Python: read_bytes). Use absolute paths. A missing path throws NotFoundError.
await sandbox.fs.write('/app/data.csv', 'x,y\n1,2\n');
const text = await sandbox.fs.read('/app/data.csv');
const entries = await sandbox.fs.list('/app');
const png = await sandbox.fs.readBytes('/app/chart.png');sandbox.fs.write("/app/data.csv", "x,y\n1,2\n")
text = sandbox.fs.read("/app/data.csv")
entries = sandbox.fs.list("/app")
png = sandbox.fs.read_bytes("/app/chart.png")A pause or a snapshot keeps every file on the disk, wherever you wrote it. After a stop (kill or expiration), files outside a Persistent Volume at /workspace are gone, unless you took a snapshot first.
Ports
Start a server on 0.0.0.0 inside the sandbox, then publish its port. Every request needs the access token, as the X-Lizard-Access-Token header or once as ?lizard_token= in a browser. See preview URLs.
const { url, hostname, accessToken } = await sandbox.exposePort(3000);
console.log(url); // open in a browser; it carries the token
await fetch(`https://${hostname}/api`, { headers: { 'X-Lizard-Access-Token': accessToken } });
await sandbox.unexpose(3000);import httpx
exposed = sandbox.expose_port(3000)
print(exposed.url) # open in a browser; it carries the token
httpx.get(f"https://{exposed.hostname}/api", headers={"X-Lizard-Access-Token": exposed.access_token})
sandbox.unexpose(3000)Pause, snapshot, and fork
These calls keep memory, running processes and files. The quickstart explains what each one keeps, who can restore a snapshot, how restores are billed, and what fails.
await sandbox.pause();
await sandbox.waitForStatus('paused');
await sandbox.resume();
await sandbox.waitForStatus('running');
const saved = await sandbox.snapshot('ready-env');
const copy = await Sandbox.restore(saved.id);
for (const f of await sandbox.fork({ count: 2 })) {
if (f.sandbox) await f.sandbox.process.exec('echo hi');
else console.warn('fork failed:', f.error);
}sandbox.pause()
sandbox.wait_for_status("paused")
sandbox.resume()
sandbox.wait_for_status("running")
saved = sandbox.snapshot("ready-env")
copy = Sandbox.restore(saved["id"])
for f in sandbox.fork(count=2):
if f.sandbox:
f.sandbox.process.exec_("echo hi")
else:
print("fork failed:", f.error)fork returns one entry per copy: the new sandbox, or an error such as the account’s sandbox limit. Each copy bills like its source.
Run code with state
CodeSandbox boots the interpreter template and runs code with runCode (Python: run_code). In that template, Python runs in a persistent Jupyter kernel per context, so variables carry over between calls and results can include charts as image/png. JavaScript and bash run as a fresh process on each call. See code interpreter.
Volumes
Attach a Persistent Volume by name to keep /workspace between sandboxes:
import { Sandbox, Volume } from '@lizard-build/sdk';
await Volume.getOrCreate('proj_123', 'agent-workdir', { sizeGb: 10 });
const sandbox = await Sandbox.create('base', { projectId: 'proj_123', volumeName: 'agent-workdir' });from lizard import Sandbox, Volume
Volume.get_or_create("proj_123", "agent-workdir", size_gb=10)
sandbox = Sandbox.create("base", project_id="proj_123", volume_name="agent-workdir")Unsupported APIs
File watching is not available yet. sandbox.fork() returns 409 when a volume is attached. Sandbox.connect does not resume a paused or stopped sandbox; call resume() on a paused one. SSH protocol access is not available; lizard sandbox ssh opens an interactive shell with your CLI credentials.
Errors
API failures throw (Python: raise) these errors. Each carries the HTTP status and, when the API sends one, a code.
| Error | Meaning |
|---|---|
PaymentRequiredError | HTTP 402: the account must start Pro or pay an invoice before it can create anything. url is the page to open. |
AuthenticationError | HTTP 401 or 403: invalid key or no access. During the trial, a second Small or a third Mini running at once returns 403 with code set to TRIAL_SIZE_RUNNING, on create, resume or fork alike; a Medium sandbox returns TRIAL_SANDBOX_SIZE, a Large one SANDBOX_SIZE_NOT_ALLOWED, and a region outside the US TRIAL_SANDBOX_REGION. A lifetime over 1 hour, on create, fork or setTimeout, returns TRIAL_SANDBOX_LIFETIME. |
NotFoundError | HTTP 404: the sandbox, file, or snapshot does not exist or has expired. |
ConflictError | HTTP 409: conflicting state, such as forking a sandbox with a volume. |
LimitExceededError | HTTP 429: too many vCPU of sandboxes at once or too many starts. code says which limit, such as SANDBOX_LIMIT_REACHED or SANDBOX_RATE_LIMITED. For SANDBOX_LIMIT_REACHED, limit and running are in vCPU, unit says so (vcpu), and needed is the vCPU the refused sandbox needs. retryAfterS (Python: retry_after_s) says when to try again. |
TimeoutError | HTTP 408 or 504. The TypeScript SDK’s own 60-second wait for process.exec throws a DOMException named TimeoutError instead. |
LizardError | Other API failures, including unsupported operations. |
The numbers behind HTTP 429 are in account limits.
HTTP API
The SDK calls Lizard’s REST API at https://lizard.build. Send your API key as Authorization: Bearer liz_... or in the X-API-Key header.
| Call | Request |
|---|---|
| Create | POST /api/sandboxes with template, projectId, size, timeoutMs, envs, metadata, region, volumeName; returns 201 with sandboxId |
| Get status | GET /api/sandboxes/{id} |
| List | GET /api/sandboxes, or GET /api/projects/{projectId}/sandboxes for one project |
| Run a command | POST /api/sandboxes/{id}/exec with cmd, envs, workdir, user, timeoutMs; returns stdout, stderr, exitCode |
| Change lifetime | POST /api/sandboxes/{id}/timeout with timeoutMs |
| Pause or resume | POST /api/sandboxes/{id}/pause, POST /api/sandboxes/{id}/resume |
| Publish a port | POST /api/sandboxes/{id}/expose/{port}; DELETE on the same path removes it |
| Kill | DELETE /api/sandboxes/{id} |
| Check limits | GET /api/sandboxes/limits; returns tier, unit, limits, running and runningVcpu |
curl -X POST https://lizard.build/api/sandboxes \
-H "Authorization: Bearer $LIZARD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"template": "base", "projectId": "proj_123", "timeoutMs": 300000}'The raw API creates a sandbox with no expiration unless you pass timeoutMs.
Storage and billing
For billing, egress and files that survive a stop, see storage and billing. Sizes and hourly prices are in limits.
Updated