# Quickstart

> Copy-paste flow: POST /sandbox with ports, POST /v1/sandboxes/{id}/tasks, stream SSE events, open s-{id}-{port}.preview.{domain}, and optional env injection for provider keys.

- Repository: tastyeffectco/sandboxes
- GitHub: https://github.com/tastyeffectco/sandboxes
- Human docs: https://grok-wiki.com/public/docs/tastyeffectco-sandboxes-f551c1a2e9a0
- Complete Markdown: https://grok-wiki.com/public/docs/tastyeffectco-sandboxes-f551c1a2e9a0/llms-full.txt

## Source Files

- `README.md`
- `AGENTS.md`
- `control-plane/internal/api/api.go`
- `control-plane/internal/api/v1_tasks.go`
- `control-plane/internal/traefik/traefik.go`
- `traefik/dynamic/wake.yml`

---

---
title: "Quickstart"
description: "Copy-paste flow: POST /sandbox with ports, POST /v1/sandboxes/{id}/tasks, stream SSE events, open s-{id}-{port}.preview.{domain}, and optional env injection for provider keys."
---

The default integration path is three HTTP calls against `sandboxd` on `SANDBOXED_API_BIND` (default `http://127.0.0.1:9090`): create an isolated sandbox with `POST /sandbox` and exposed ports, submit a headless coding task with `POST /v1/sandboxes/{id}/tasks`, then open the Traefik-registered preview host `s-{id}-{port}.preview.{PREVIEW_DOMAIN}` while streaming task progress from `GET /v1/sandboxes/{id}/tasks/{taskId}/events`.

<Note>
This flow requires a **Linux host** with **Docker Engine** and the **Compose plugin**. Run `./install.sh` from the repo root before the commands below.
</Note>

## Prerequisites

| Requirement | Detail |
|---|---|
| Host OS | Linux with Docker Engine + `docker compose` |
| Install | `./install.sh` — idempotent; copies `.env.example` → `.env`, builds `sandboxed-base:1.0.0`, starts the compose stack |
| API base URL | `http://127.0.0.1:9090` unless you changed `SANDBOXED_API_BIND` in `.env` |
| Auth (local) | `SANDBOXD_API_AUTH_DISABLED=true` by default — no `Authorization` header required |

## Verify the stack

```bash
curl -s http://127.0.0.1:9090/healthz   # ok
curl -s http://127.0.0.1:9090/readyz    # ready
```

If `readyz` is not `ready`, the control plane cannot reach Docker — see [Troubleshooting](/troubleshooting).

## End-to-end flow

```mermaid
sequenceDiagram
  participant Client
  participant sandboxd
  participant Docker
  participant runtimed
  participant Traefik

  Client->>sandboxd: POST /sandbox {"ports":[3000]}
  sandboxd->>Docker: create container s-{id}
  Docker-->>Traefik: Docker labels Host(s-{id}-3000.preview.{domain})
  Client->>sandboxd: POST /v1/sandboxes/{id}/tasks
  alt sandbox stopped
    sandboxd->>sandboxd: wake via POST /wake/{id}
  end
  sandboxd->>runtimed: StartTask (Unix socket)
  Client->>sandboxd: GET .../tasks/{taskId}/events (SSE)
  runtimed-->>Client: status, message, tool, build, done events
  Client->>Traefik: GET s-{id}-3000.preview.{domain}
  Traefik->>Docker: proxy to sandbox :3000
```

<Steps>
<Step title="Set API and create a sandbox">

Expose the port your dev server will listen on (commonly `3000`). Omit `id` to auto-generate a ULID.

```bash
API=http://127.0.0.1:9090

ID=$(curl -s -XPOST "$API/sandbox" \
  -H 'content-type: application/json' \
  -d '{"ports":[3000]}' | sed -E 's/.*"id":"([^"]+)".*/\1/')
echo "sandbox=$ID"
```

</Step>

<Step title="Submit a coding task">

`POST /v1/sandboxes/{id}/tasks` runs **OpenCode** headlessly inside the sandbox via `runtimed`. If the sandbox is **stopped**, `sandboxd` wakes it first (wake-on-task-submit).

```bash
TASK_JSON=$(curl -s -XPOST "$API/v1/sandboxes/$ID/tasks" \
  -H 'content-type: application/json' \
  -d '{
    "prompt": "create a Vite app that shows a todo list and run it on port 3000",
    "agent": "opencode"
  }')
echo "$TASK_JSON"

TASK_ID=$(echo "$TASK_JSON" | sed -E 's/.*"id":"([^"]+)".*/\1/')
```

</Step>

<Step title="Stream task events (SSE)">

Follow the `events_url` from the task response. Use `curl -N` so the connection stays open.

```bash
curl -N "$API/v1/sandboxes/$ID/tasks/$TASK_ID/events"
```

Resume after disconnect with `Last-Event-ID` or `?since=<index>`.

</Step>

<Step title="Open the live preview">

Once a process listens on the exposed port, Traefik routes the preview hostname to the sandbox container.

| Setting | Default | Preview URL |
|---|---|---|
| `PREVIEW_DOMAIN` | `localhost` | `http://s-{id}-3000.preview.localhost` |
| `HTTP_PORT` | `80` | Append `:{HTTP_PORT}` when not `80` (e.g. `:8088`) |

Modern browsers resolve `*.localhost` to `127.0.0.1` with no DNS setup. A stopped sandbox **wakes** on the first preview request (Traefik catch-all → `sandboxd`).

</Step>
</Steps>

## Create sandbox (`POST /sandbox`)

:::endpoint POST /sandbox
Create an isolated Linux sandbox container with optional preview ports and injected environment variables.
:::

<ParamField body="ports" type="integer[]">
TCP ports to expose via Traefik. Each port gets a router `s-{id}-{port}` with `Host(\`s-{id}-{port}.preview.{PREVIEW_DOMAIN}\`)` at priority **100** (above the wake catch-all at priority **1**).
</ParamField>

<ParamField body="id" type="string">
Optional ULID. Omit to auto-generate. Non-ULID values return `400` with `id must be a ULID`.
</ParamField>

<ParamField body="env" type="object">
Key/value map injected at container create via `docker run --env`. Visible to `runtimed` and agent processes (e.g. `ANTHROPIC_API_KEY`). Keys must be non-empty; values must not contain `=` or newlines.
</ParamField>

<RequestExample>

```bash
curl -s -XPOST http://127.0.0.1:9090/sandbox \
  -H 'content-type: application/json' \
  -d '{"ports":[3000]}'
```

</RequestExample>

<ResponseExample>

```json
{
  "id": "01HX…",
  "status": "running",
  "image": "sandboxed-base:1.0.0",
  "memory_high": "4G"
}
```

</ResponseExample>

Create is asynchronous: the row may show `creating` until the container and workspace are ready. Poll `GET /sandbox/{id}` if you need to wait before submitting a task.

## Submit task (`POST /v1/sandboxes/{id}/tasks`)

:::endpoint POST /v1/sandboxes/{id}/tasks
Start a headless coding agent task in the sandbox. Returns **202 Accepted** with `events_url`.
:::

<ParamField body="prompt" type="string" required>
Natural-language instruction for the agent.
</ParamField>

<ParamField body="agent" type="string">
Defaults to `opencode`. Only `opencode` is supported in this release; other values return `400 invalid_request`.
</ParamField>

<ResponseField name="id" type="string">
Task ULID.
</ResponseField>

<ResponseField name="status" type="string">
Initial value `running`.
</ResponseField>

<ResponseField name="events_url" type="string">
Relative path, e.g. `/v1/sandboxes/{id}/tasks/{taskId}/events`.
</ResponseField>

| Condition | HTTP | Code |
|---|---|---|
| Sandbox not found | 404 | `not_found` |
| Sandbox not `running` after wake attempt | 409 | `conflict` |
| Empty `prompt` | 400 | `invalid_request` |
| Task already in progress in runtimed | 409 | `task_in_progress` |
| runtimed unreachable | 502 | `sandbox_unavailable` |

Fetch the durable result later with `GET /v1/sandboxes/{id}/tasks/{taskId}` (works after the sandbox stops or is destroyed).

## Stream events (SSE)

:::endpoint GET /v1/sandboxes/{id}/tasks/{taskId}/events
Server-Sent Events stream proxied from in-sandbox `runtimed`. `Content-Type: text/event-stream`.
:::

Each event line uses the form:

```text
id: <n>
event: <type>
data: <json>

```

| Event type | Role |
|---|---|
| `status` | Phase updates from runtimed |
| `message` | Agent text (provider-derived, best-effort) |
| `tool` | Tool invocations (best-effort) |
| `build` | Build step progress |
| `done` | Terminal event; `data` carries the canonical `TaskResult` |

Resume options:

- Header `Last-Event-ID: <n>` — resumes after event `n`
- Query `?since=<index>` — start at a given index

```bash
curl -N "http://127.0.0.1:9090/v1/sandboxes/$ID/tasks/$TASK_ID/events"
```

Cancel an in-flight task: `POST /v1/sandboxes/{id}/tasks/{taskId}/cancel`.

## Preview URL

Hostname pattern (from Traefik Docker labels):

```text
s-{ulid}-{port}.preview.{PREVIEW_DOMAIN}
```

| Variable | Default | Effect |
|---|---|---|
| `PREVIEW_DOMAIN` | `localhost` | DNS suffix for preview hosts |
| `HTTP_PORT` | `80` | Host port Traefik binds; non-80 URLs need `:HTTP_PORT` |
| `PREVIEW_ENTRYPOINT` | `web` | Traefik entrypoint (`websecure` + `PREVIEW_TLS=true` in production) |

Local example (default `.env`):

```text
http://s-01HXABC-3000.preview.localhost
```

With `HTTP_PORT=8088`:

```text
http://s-01HXABC-3000.preview.localhost:8088
```

Test from the shell without a browser:

```bash
curl -s -H "Host: s-$ID-3000.preview.localhost" "http://127.0.0.1:${HTTP_PORT:-80}/"
```

<Warning>
A warming page (`Spinning up your app…`) appears when the sandbox is stopped and waking, or when nothing is listening on the requested port yet. Wait for the task to start the dev server, then reload.
</Warning>

## Inject provider keys at create

Inject API keys once at sandbox create so both the tasks API and any `exec` shell see them:

```bash
ID=$(curl -s -XPOST "$API/sandbox" \
  -H 'content-type: application/json' \
  -d '{
    "ports": [3000],
    "env": {"ANTHROPIC_API_KEY": "sk-ant-..."}
  }' | sed -E 's/.*"id":"([^"]+)".*/\1/')

curl -s -XPOST "$API/v1/sandboxes/$ID/tasks" \
  -H 'content-type: application/json' \
  -d '{"prompt":"build a Vite todo app and run it on port 3000","agent":"opencode"}'
```

OpenCode ships in the base image and can run on its default plan without a key; inject your own key to bill against your provider account. The design is provider-neutral — use whichever key names your chosen agent expects.

<Tip>
`POST /v1/sandboxes` is the multi-tenant create path (requires `project.id` and `project.user_id`) and always exposes port `3000`. For the shortest OSS quickstart, prefer `POST /sandbox` with explicit `ports`.
</Tip>

## Complete copy-paste script

```bash
API=http://127.0.0.1:9090
HTTP_PORT="${HTTP_PORT:-80}"
PREVIEW_DOMAIN="${PREVIEW_DOMAIN:-localhost}"
PORTSUFFIX=""
[ "$HTTP_PORT" != "80" ] && PORTSUFFIX=":$HTTP_PORT"

ID=$(curl -s -XPOST "$API/sandbox" -H 'content-type: application/json' \
  -d '{"ports":[3000]}' | sed -E 's/.*"id":"([^"]+)".*/\1/')
echo "sandbox=$ID"

TASK_JSON=$(curl -s -XPOST "$API/v1/sandboxes/$ID/tasks" -H 'content-type: application/json' -d '{
  "prompt":"create a Vite app that shows a todo list and run it on port 3000",
  "agent":"opencode"
}')
TASK_ID=$(echo "$TASK_JSON" | sed -E 's/.*"id":"([^"]+)".*/\1/')
echo "task=$TASK_ID"
echo "events: $API/v1/sandboxes/$ID/tasks/$TASK_ID/events"
echo "preview: http://s-$ID-3000.preview.$PREVIEW_DOMAIN$PORTSUFFIX"

curl -N "$API/v1/sandboxes/$ID/tasks/$TASK_ID/events"
```

## Related pages

<CardGroup>
<Card title="Installation" href="/installation">
Prerequisites, `./install.sh`, `.env` bootstrap, and health checks in depth.
</Card>
<Card title="Run coding agents" href="/run-coding-agents">
Task lifecycle, runtimed contract, wake-on-submit, and SSE semantics.
</Card>
<Card title="Preview URL reference" href="/preview-url-reference">
Hostname rules, port suffixes, and localhost vs production HTTPS.
</Card>
<Card title="Build a todo app with an agent" href="/example-agent-todo">
End-to-end recipe with verification steps.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
readyz failures, port 80 conflicts, warming-page stalls, and compose logs.
</Card>
</CardGroup>
