# Runbox API — Code-Interpreter Sandboxes
> Stateful Python code-execution sandboxes for AI agents. Upload a user's data file,
> run pandas/matplotlib code with Jupyter semantics, get charts/files back as public
> URLs (never base64). Sessions persist variables between runs, one session per
> conversation. Base URL: https://runbox.api.insyde.one
## Authentication
Every route except `GET /health` and artifact downloads (`GET /a/…`) requires:
Authorization: Bearer {tenant}:{hash} (e.g. cnn:55abc47caf008d347a51)
`?key={token}` also works as a query-string fallback. Tokens are issued per tenant by
the operator. A token only ever sees its own tenant's sessions — anything else
returns a uniform 404.
## Intended agent flow
1. `POST /sessions` once per conversation → keep the `sessionId`.
2. `POST /sessions/{id}/files` for each user attachment → note the returned `path`.
3. `POST /sessions/{id}/run` per turn. Variables persist between runs (`df` defined
in run 1 is available in run 2).
4. Show the user any `artifacts[].url` (images render directly in `
`).
5. `DELETE /sessions/{id}` when the conversation ends (optional — sessions self-expire).
## Endpoints
### POST /sessions
Create a sandbox session.
Request body (all optional):
{ "conversationId": "conv-123", "idleTtlSec": 900, "maxLifetimeSec": 3600 }
Response 201:
{ "sessionId": "mdl_sb-abc123…", "expiresAt": "2026-06-04T21:06:04.274Z" }
- `idleTtlSec` (60–300, default 120): session dies after this long WITHOUT a run. It is a
sliding window — every run renews it — so short back-and-forth keeps the session warm.
DELETE the session when the conversation ends so the sandbox stops billing immediately.
- `maxLifetimeSec` (300–7200, default 3600): hard ceiling.
- 409 if the tenant already has too many live sessions (default 5).
### POST /sessions/{id}/files
Put a file into the sandbox filesystem. Two forms:
- multipart/form-data with a `file` field, or
- JSON `{ "url": "https://…/data.xlsx", "name": "data.xlsx" }` (server-side fetch).
Response: `{ "path": "/work/data.csv" }` — use this exact path in your code.
Max 25 MB (413 above that).
### POST /sessions/{id}/run
Execute Python in the session.
Request:
{ "code": "import pandas as pd\n…", "language": "python", "timeoutMs": 30000 }
- `timeoutMs` 1000–120000, default 30000. `language` must be `"python"`.
Response 200 (ALWAYS 200 when the code ran, even if it raised — read `ok`/`error`):
{
"ok": true,
"exitCode": 0,
"stdout": "…", // print() output, capped at 64KB
"stderr": "", // capped at 64KB
"truncated": false, // true if stdout/stderr hit the cap
"result": "42", // repr of the LAST EXPRESSION (Jupyter style), or null
"error": null, // or {"ename":"ZeroDivisionError","evalue":"…","traceback":["…"]}
"displays": [ // rich representations of the result value
{ "mimeType": "text/html", "data": "
" } // e.g. DataFrames
],
"artifacts": [ // files captured automatically from this run
{ "name": "figure-1-1.png", "mimeType": "image/png",
"url": "https://runbox.api.insyde.one/a/…/figure-1-1.png", "sizeBytes": 9981 }
],
"durationMs": 776
}
Execution semantics:
- The value of the last expression becomes `result` (like a Jupyter cell). Statements
only → `result: null`.
- Every open matplotlib figure is saved as a PNG artifact automatically — do NOT call
plt.savefig() or plt.show().
- Any file the code writes to `outputs/` (relative) or `/work/outputs/` also becomes
an artifact.
- `artifacts[].url` is public and unguessable — pass it straight to the user/chat UI.
Never inline file bytes into the model context.
- Artifact URLs stay valid for **7 days** (or until the session is DELETEd, which
purges them immediately). Persist anything you need longer on your side.
- On exception, `ok:false` and `error` carries a clean traceback → fix the code and
run again in the SAME session (state survives errors).
- A run that exceeds `timeoutMs` returns `error.ename = "TimeoutError"`; the session
survives. HTTP 504 only appears when a run is lost entirely.
### GET /sessions/{id}
{ "sessionId": "…", "alive": true, "createdAt": "…", "expiresAt": "…", "conversationId": "…" }
### GET /sessions/{id}/files — list files (optional `?path=`)
### GET /sessions/{id}/files/{path} — download one file (binary)
### DELETE /sessions/{id} — kill the sandbox and purge its artifacts
### GET /health — { "ok": true, "service": "runbox", "providers": ["mdl"] }
## Sandbox environment
- Python 3.12. Preinstalled packages (pip name → import name where they differ):
- Data analysis: pandas, numpy, scipy, statsmodels
- Plotting: matplotlib, seaborn, squarify (treemaps)
- Vega/D3-style: altair (Vega-Lite), vl-convert (offline SVG/PNG render, no browser)
- Geo/maps: geopandas, shapely, pyproj, pyogrio (read shp/geojson/gpkg), mapclassify (choropleth bins)
- Parliament: parliamentarch (hemicycle SVG), cairosvg (rasterize SVG→PNG)
- Spreadsheets: openpyxl (read/write xlsx), xlsxwriter (write xlsx), pyarrow (parquet/feather)
- PDF: pdfplumber (extract text/tables), pypdf (split/merge/metadata), reportlab (build laid-out PDFs)
- Documents: python-docx → `import docx`, Pillow → `import PIL`
- Parsing: beautifulsoup4 → `import bs4`, lxml, unidecode (strip accents), tabulate (text tables)
- Text & locale: charset-normalizer → `import charset_normalizer` (encoding detection),
babel (number/date/currency formatting)
- requests is installed but unusable (network blocked)
- Charts come PRE-STYLED with the insyde.one brand (navy #0f0051, green #00ff80,
blue #3cc5f5, DM Sans, pt_BR comma decimals). It is the matplotlib default — just
plot; do NOT call plt.style.use(...) or sns.set_theme(). `plt.style.use("insyde")`
also exists if you reset rcParams.
- For VECTOR output (broadcast/editable), save SVG: fig.savefig("outputs/chart.svg").
Auto-capture only makes PNG; emit .svg explicitly when you want vector. It still
comes back as an artifact URL.
- For the polished declarative (D3-style) look use Altair — it renders OFFLINE via
vl-convert (no browser). Apply the brand theme with one line then save:
import insyde_theme, altair as alt
chart = alt.Chart(df).mark_bar().encode(x="cat", y="val")
chart.save("outputs/chart.svg") # or .png
- Hemicycle/parliament: parliamentarch builds the seat SVG (rasterize with
cairosvg.svg2png if you need PNG), or just arrange dots in concentric arcs with
matplotlib (inherits the brand style, outputs PNG directly).
- Map boundaries are baked in at /opt/runbox/data/geo/ (read with geopandas.read_parquet,
EPSG:4326), so choropleths work offline:
- br_estados.parquet (27 rows: code_state, abbrev_state, name_state, name_region)
- br_municipios.parquet (~5570 rows: code_muni, name_muni, abbrev_state)
- mundo_paises.parquet (~240 rows: name, iso_a3, continent)
- mundo_continentes.parquet (~8 rows: continent)
Join your data on the key column, then gdf.plot(column=..., scheme="quantiles", legend=True).
- 1 CPU, 2 GB RAM. Working directory `/work`; uploads land at `/work/{name}`.
- **No network access** — `requests.get(...)` raises ConnectionError. Don't try to
pip install or download anything; everything needed is preinstalled.
- No secrets in the environment. Filesystem is destroyed with the session.
## Errors
All errors are `{ "error": "message" }`:
| Status | Meaning |
|--------|---------|
| 401 | missing/invalid token |
| 404 | session unknown, expired, or belongs to another tenant |
| 409 | concurrent-session quota exceeded — reuse or DELETE an existing session |
| 413 | file larger than 25 MB |
| 422 | bad input (missing code, language ≠ python, bad URL) |
| 502 | sandbox provider failure — retry once |
| 504 | run lost (rare) — retry; if it persists, create a new session |
## Tool definitions (copy-paste)
OpenAI tools format:
[
{
"type": "function",
"function": {
"name": "runbox_create_session",
"description": "Create a Python sandbox session for this conversation. Call once and reuse the sessionId for every run. State (variables, files) persists between runs.",
"parameters": { "type": "object", "properties": {
"conversationId": { "type": "string" } }, "required": [] }
}
},
{
"type": "function",
"function": {
"name": "runbox_upload_url",
"description": "Load a file from a URL into the sandbox filesystem. Returns the absolute path to use in code (e.g. /work/data.csv).",
"parameters": { "type": "object", "properties": {
"sessionId": { "type": "string" },
"url": { "type": "string" },
"name": { "type": "string" } }, "required": ["sessionId", "url"] }
}
},
{
"type": "function",
"function": {
"name": "runbox_run",
"description": "Execute Python in the session (Jupyter semantics: last expression is returned as result; matplotlib figures and files written to outputs/ come back as artifact URLs). On error, read error.traceback, fix the code and call again — state survives.",
"parameters": { "type": "object", "properties": {
"sessionId": { "type": "string" },
"code": { "type": "string" },
"timeoutMs": { "type": "integer" } }, "required": ["sessionId", "code"] }
}
}
]
Anthropic tools use the same parameter objects under `input_schema`.
Executor: each tool is one HTTP call with the Bearer token —
create → `POST /sessions`; upload → `POST /sessions/{id}/files` (JSON {url,name});
run → `POST /sessions/{id}/run` (JSON {code,timeoutMs}).
## Agent best practices
- One session per conversation; store the sessionId in conversation state. If a call
returns 404, the session expired — create a new one and re-upload files.
- Keep the model's view textual: stdout, result, error, and artifact NAMES; give the
UI the artifact URLs. Never fetch artifact bytes into the prompt.
- Prefer small, incremental runs (the state persists) over one giant script — errors
are cheaper to fix.
- `displays[0].data` (HTML table) is for the chat UI; summarize it in text yourself.
- Rates: a fresh session takes ~3s to boot; runs are typically 0.5–2s.