API Reference
Iris has two distinct API surfaces that talk to different things.
The gateway (api.iris.dev) handles sandbox lifecycle — create, fork, list, suspend. It uses your API key and speaks ConnectRPC (HTTP POST + JSON).
The data plane is a REST API running inside each sandbox VM. It handles exec, files, checkpoints, services, and the KV store. It does not accept your API key.
Why two tokens
Your API key belongs to your team and never leaves your backend. The data plane runs inside a microVM that has no connection to your auth infrastructure — it cannot call out to validate API keys. Instead, it receives a short-lived RS256 JWT (data_plane_token) from the gateway, signed with a private key the VM trusts.
The JWT is scoped to a single sandbox ID, so a leaked token can only affect that one sandbox. It expires (configurable, default 1 hour) so stale tokens stop working automatically.
Your backend │ ├─ API key ──▶ api.iris.dev (gateway) │ │ │ └─ issues data_plane_token (JWT, scoped to sandbox_id) │ └─ data_plane_token ──▶ https://{sandbox.domain} (VM)How your client should handle token refresh
CreateSandbox and ForkSandbox both return a data_plane_token alongside the sandbox. Stash it with the sandbox — it covers most use cases. When it expires (the companion data_plane_token_expires_at_unix tells you when), call IssueDataPlaneToken to get a new one.
The cleanest pattern is to integrate this into your HTTP transport layer rather than managing it at each call site:
class SandboxTransport { private token: string private expiresAt: number
async request(path: string, init?: RequestInit) { if (Date.now() / 1000 > this.expiresAt - 30) { await this.refreshToken() // refresh 30s before expiry }
const res = await fetch(`https://${this.domain}${path}`, { ...init, headers: { ...init?.headers, Authorization: `Bearer ${this.token}` }, })
// If the token expired mid-flight (clock skew etc.), refresh and retry once if (res.status === 401) { await this.refreshToken() return fetch(`https://${this.domain}${path}`, { ...init, headers: { ...init?.headers, Authorization: `Bearer ${this.token}` }, }) }
return res }
private async refreshToken() { const res = await gatewayClient.issueDataPlaneToken({ sandbox_id: this.sandboxId }) this.token = res.jwt this.expiresAt = res.expires_at_unix }}The TypeScript SDK does exactly this — token refresh is wired into the transport layer so every method on sandbox.exec, sandbox.files, etc. gets a valid token automatically.
Gateway API
Base URL: https://api.iris.dev
Auth: Authorization: Bearer iris_sk_your_key
All requests are POST with a JSON body. The method name forms the path.
POST /iris.public.v1.SandboxService/{MethodName}Content-Type: application/jsonAuthorization: Bearer iris_sk_...Sandbox object
Returned by most gateway endpoints.
| Field | Type | Description |
|---|---|---|
id | string | Unique sandbox ID |
name | string | Display name |
state | string | Current lifecycle state |
domain | string | Hostname for data plane calls; empty until RUNNING |
created_at | string | RFC 3339 |
updated_at | string | RFC 3339 |
source_sandbox_id | string | Populated when created via ForkSandbox |
States
| Value | Meaning |
|---|---|
SANDBOX_STATE_CREATING | VM is being provisioned |
SANDBOX_STATE_RUNNING | Ready; data plane accessible |
SANDBOX_STATE_SUSPENDING | Snapshot in progress |
SANDBOX_STATE_SUSPENDED | Frozen; no compute charges |
SANDBOX_STATE_RESUMING | Restoring from snapshot |
SANDBOX_STATE_DELETING | Being destroyed |
SANDBOX_STATE_ERROR | Unrecoverable failure |
CreateSandbox
POST /iris.public.v1.SandboxService/CreateSandbox{ "name": "my-sandbox" }| Field | Required | Description |
|---|---|---|
name | no | Auto-generated if omitted |
Response
{ "sandbox": { ... }, "data_plane_token": "eyJ...", "data_plane_token_expires_at_unix": 1234567890}GetSandbox
POST /iris.public.v1.SandboxService/GetSandbox{ "id": "sb_abc123" }Response: { "sandbox": { ... } }
ListSandboxes
POST /iris.public.v1.SandboxService/ListSandboxes{}Response: { "sandboxes": [ ... ] }
DeleteSandbox
POST /iris.public.v1.SandboxService/DeleteSandbox{ "id": "sb_abc123" }Permanently destroys the VM. Checkpoints are not deleted.
Response: {}
SuspendSandbox
POST /iris.public.v1.SandboxService/SuspendSandbox{ "id": "sb_abc123" }Snapshots the sandbox and stops the VM. Compute billing pauses. Poll GetSandbox until state is SANDBOX_STATE_SUSPENDED.
Response: { "sandbox": { ... } }
ResumeSandbox
POST /iris.public.v1.SandboxService/ResumeSandbox{ "id": "sb_abc123" }Response: { "sandbox": { ... } } — poll until RUNNING before making data plane calls.
ForkSandbox
POST /iris.public.v1.SandboxService/ForkSandbox{ "source_sandbox_id": "sb_abc123", "name": "experiment-1"}| Field | Required | Description |
|---|---|---|
source_sandbox_id | yes | Must be RUNNING and owned by your team |
name | no | Display name for the fork |
Creates a copy-on-write clone instantly. The source keeps running unchanged.
Response
{ "sandbox": { "source_sandbox_id": "sb_abc123", ... }, "data_plane_token": "eyJ...", "data_plane_token_expires_at_unix": 1234567890}IssueDataPlaneToken
POST /iris.public.v1.SandboxService/IssueDataPlaneToken{ "sandbox_id": "sb_abc123" }Issues a fresh JWT for data plane access. Call this when the token from CreateSandbox or ForkSandbox expires.
Response
{ "jwt": "eyJ...", "expires_at_unix": 1234571490}Data Plane API
Base URL: https://{sandbox.domain}
Auth: Authorization: Bearer <data_plane_token>
Exec
Run a command
POST /v1/exec{ "cmd": ["/bin/sh", "-c", "echo hello"], "dir": "/home/user", "env": { "FOO": "bar" }, "stdin": "", "timeout_ms": 30000}| Field | Required | Description |
|---|---|---|
cmd | yes | Command and arguments as an array |
dir | no | Working directory |
env | no | Additional environment variables |
stdin | no | String piped to stdin |
timeout_ms | no | Kill after this many milliseconds |
Response
{ "stdout": "hello\n", "stderr": "", "exit_code": 0, "ok": true, "duration_ms": 12, "timed_out": false}Files
Write a file
POST /v1/files?path=/absolute/pathBody is raw binary. Set x-file-mode to an octal string (e.g. 755) to control permissions.
Response: EntryInfo (see stat).
Max size: 1 MB.
Read a file
GET /v1/files?path=/absolute/pathResponse: Raw bytes.
Delete
DELETE /v1/files?path=/absolute/path&recursive=truerecursive is required to delete non-empty directories.
Check existence
GET /v1/files/exists?path=/absolute/path{ "exists": true, "entry_type": "file" }entry_type is "file", "directory", or "symlink".
List directory
GET /v1/files/list?path=/absolute/path&depth=1{ "path": "/app", "entries": [ ... ], "truncated": false}truncated: true means there are more entries than returned. Increase depth or list subdirectories individually.
Create directory
POST /v1/files/mkdir{ "path": "/app/logs", "parents": true }parents: true creates intermediate directories (like mkdir -p).
Move or rename
POST /v1/files/move{ "source": "/tmp/output", "destination": "/app/output" }Get file metadata
GET /v1/files/stat?path=/absolute/path{ "name": "script.sh", "path": "/app/script.sh", "size": 42, "modified_at": "2024-05-11T10:00:00Z", "is_dir": false, "permissions": "755", "owner": "root", "group": "root", "symlink_target": ""}Checkpoints
Create a checkpoint
POST /v1/checkpoint{ "name": "after-install" }Non-blocking — the sandbox keeps running. Snapshots the full filesystem.
Response
{ "checkpoint_id": "ckpt_abc123", "ok": true, "size_bytes": 1234567, "copy_duration_ms": 420}List checkpoints
GET /v1/checkpoint/list{ "checkpoints": [ { "id": "ckpt_abc123", "name": "after-install", "created_at": "2024-05-11T10:00:00Z", "size_bytes": 1234567, "size_human": "1.2 MB" } ]}Wait for a checkpoint
POST /v1/checkpoint/wait{ "checkpoint_id": "ckpt_abc123", "poll_interval_ms": 100, "timeout_secs": 30}Needed when you want to fork immediately after creating a checkpoint — the snapshot write is async and the fork will fail if it can’t find the checkpoint in storage.
Response
{ "visible": true, "ok": true, "waited_ms": 320, "attempts": 4}Returns 408 if timeout_secs is exceeded.
Delete a checkpoint
DELETE /v1/checkpoint/{id}{ "ok": true, "freed_bytes": 1234567 }Restore from a checkpoint
POST /v1/restore{ "checkpoint_id": "ckpt_abc123" }Rewinds the filesystem in-place. The same sandbox ID and token remain valid — no new sandbox is created.
Response
{ "ok": true, "restore_duration_ms": 380, "started_services": ["redis"], "stopped_services": [], "failed_services": []}Services
Long-running background processes managed by the sandbox supervisor.
List services
GET /v1/servicesGet a service
GET /v1/services/{name}ServiceResponse
{ "name": "redis", "cmd": "redis-server", "status": "running", "pid": 142, "container": false}status is "running", "stopped", "failed", or "unknown".
Create or update a service
PUT /v1/services/{name}{ "cmd": "redis-server", "args": ["--port", "6379"], "env": { "REDIS_LOGLEVEL": "warning" }, "working_dir": "/data", "user": "nobody", "health_port": 6379, "needs": ["postgres"], "container": false, "image": ""}| Field | Required | Description |
|---|---|---|
cmd | yes | Command to run |
args | no | Arguments |
env | no | Environment variables |
working_dir | no | Working directory |
user | no | Run as this Unix user |
health_port | no | TCP port to probe for readiness |
needs | no | Services that must be running first |
container | no | Run in an OCI container |
image | no | OCI image when container: true |
Start / stop
POST /v1/services/{name}/startPOST /v1/services/{name}/stopExec inside a service container
POST /v1/services/{name}/execOnly valid for container: true services.
{ "cmd": ["redis-cli", "ping"], "timeout_secs": 10 }Response
{ "stdout": "PONG\n", "stderr": "", "exit_code": 0, "timed_out": false }Delete a service
DELETE /v1/services/{name}Store
Per-sandbox key-value store. Values are arbitrary JSON and persist for the lifetime of the sandbox.
Set a value
PUT /v1/store/{key}{ "value": { "step": 3, "score": 0.91 }, "ttl_seconds": 3600 }ttl_seconds is optional. Omit for no expiry.
Get a value
GET /v1/store/{key}{ "value": { "step": 3, "score": 0.91 }, "expires_at": "2024-05-11T11:00:00Z" }Returns 404 when the key does not exist or has expired.
Delete a key
DELETE /v1/store/{key}{ "deleted": true, "ok": true }List keys
GET /v1/store?prefix=run_&limit=100{ "keys": ["run_1", "run_2"], "truncated": false }Batch operations
POST /v1/store/batchAll operations in a batch are atomic.
{ "operations": [ { "op": "set", "key": "result", "value": { "score": 0.95 }, "ttl_seconds": 7200 }, { "op": "delete", "key": "temp_state" } ]}{ "applied": 2, "ok": true }