Skip to content

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/json
Authorization: Bearer iris_sk_...

Sandbox object

Returned by most gateway endpoints.

FieldTypeDescription
idstringUnique sandbox ID
namestringDisplay name
statestringCurrent lifecycle state
domainstringHostname for data plane calls; empty until RUNNING
created_atstringRFC 3339
updated_atstringRFC 3339
source_sandbox_idstringPopulated when created via ForkSandbox

States

ValueMeaning
SANDBOX_STATE_CREATINGVM is being provisioned
SANDBOX_STATE_RUNNINGReady; data plane accessible
SANDBOX_STATE_SUSPENDINGSnapshot in progress
SANDBOX_STATE_SUSPENDEDFrozen; no compute charges
SANDBOX_STATE_RESUMINGRestoring from snapshot
SANDBOX_STATE_DELETINGBeing destroyed
SANDBOX_STATE_ERRORUnrecoverable failure

CreateSandbox

POST /iris.public.v1.SandboxService/CreateSandbox
{ "name": "my-sandbox" }
FieldRequiredDescription
namenoAuto-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"
}
FieldRequiredDescription
source_sandbox_idyesMust be RUNNING and owned by your team
namenoDisplay 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
}
FieldRequiredDescription
cmdyesCommand and arguments as an array
dirnoWorking directory
envnoAdditional environment variables
stdinnoString piped to stdin
timeout_msnoKill 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/path

Body 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/path

Response: Raw bytes.

Delete

DELETE /v1/files?path=/absolute/path&recursive=true

recursive 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/services

Get 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": ""
}
FieldRequiredDescription
cmdyesCommand to run
argsnoArguments
envnoEnvironment variables
working_dirnoWorking directory
usernoRun as this Unix user
health_portnoTCP port to probe for readiness
needsnoServices that must be running first
containernoRun in an OCI container
imagenoOCI image when container: true

Start / stop

POST /v1/services/{name}/start
POST /v1/services/{name}/stop

Exec inside a service container

POST /v1/services/{name}/exec

Only 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/batch

All 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 }