From 918e1b8fcc1fd32b5a9f1741d9388347d8dc6a1b Mon Sep 17 00:00:00 2001
From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com>
Date: Tue, 29 Sep 2026 13:16:47 +0000
Subject: [PATCH 01/14] Updated mintlify pages
- Created sandboxes/overview.mdx
- Updated docs.json
Mintlify-Source: dashboard-editor
---
docs.json | 6 ++
sandboxes/overview.mdx | 126 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 132 insertions(+)
create mode 100644 sandboxes/overview.mdx
diff --git a/docs.json b/docs.json
index af1ed113a..2e5fe268f 100644
--- a/docs.json
+++ b/docs.json
@@ -282,6 +282,12 @@
}
]
},
+ {
+ "group": "Sandboxes",
+ "pages": [
+ "sandboxes/overview"
+ ]
+ },
{
"group": "Accounts and billing",
"pages": [
diff --git a/sandboxes/overview.mdx b/sandboxes/overview.mdx
new file mode 100644
index 000000000..32ee0104f
--- /dev/null
+++ b/sandboxes/overview.mdx
@@ -0,0 +1,126 @@
+---
+title: "Sandboxes"
+sidebarTitle: "Overview"
+description: "Create short-lived, isolated CPU environments on demand to run untrusted or agent-generated code, then tear them down. Billed per second."
+---
+
+Sandboxes are short-lived, isolated CPU environments that you create on demand, run commands in, and tear down when you're done. They're built for running untrusted code: code generated by an AI agent, code submitted by your users, or one-off scripts you don't want touching your own infrastructure.
+
+You manage sandboxes through the [REST API v2](/api-reference-v2/overview). Each sandbox runs a container image you choose, starts in seconds, and is billed per second until it terminates.
+
+
+
+ Create a sandbox, run a command, and terminate it.
+
+
+
+ Endpoints and fields for the `/v2/sandboxes` resource.
+
+
+
+## When to use a sandbox
+
+Use a sandbox when you need a clean, disposable environment for a short task:
+
+- An AI agent needs to execute the code it writes.
+- Your application runs code or commands supplied by end users.
+- You want to test a script or package in isolation before running it elsewhere.
+
+For long-running workloads, GPU compute, or persistent development environments, use [Pods](/pods/overview) instead. For request-driven inference that scales with traffic, use [Serverless](/serverless/overview).
+
+## Isolation
+
+Every sandbox runs under [gVisor](https://gvisor.dev/), a user-space kernel that intercepts the container's system calls. Code inside a sandbox can't reach the host machine or other tenants' sandboxes, so you can run code you don't trust.
+
+## Lifecycle
+
+A sandbox moves through these states:
+
+| State | Meaning |
+| --- | --- |
+| `CREATING` | Compute is being allocated and the container image is being prepared. |
+| `RUNNING` | Compute is allocated. The container may still be starting (see [Known limitations](#known-limitations)). |
+| `TERMINATED` | The sandbox has stopped and its compute is released. Billing has stopped. |
+| `FAILED` | The sandbox couldn't be created. |
+
+`POST /v2/sandboxes` returns `201` with the new sandbox, usually still in the `CREATING` state. Poll `GET /v2/sandboxes/{id}` until `state` is `RUNNING` before you send commands.
+
+{/* TODO(DOCS-509): The ticket says create blocks until RUNNING or FAILED; the dev spec says it returns 201 while still CREATING. Confirm with Zack and adjust this section. */}
+
+
+
+There is no idempotency key for sandbox creation yet. If a create request fails at the network layer, list your sandboxes (optionally filtered by a label you set) before retrying, so you don't create and pay for a duplicate.
+
+
+
+### Termination is final
+
+A terminated sandbox can't be restarted. Its record stays readable through `GET /v2/sandboxes/{id}`, with `terminationReason` and `terminatedAt` set, so you can see why and when it stopped. To run more work, create a new sandbox.
+
+Terminating a sandbox that is already terminated succeeds, so it's safe to retry `DELETE /v2/sandboxes/{id}`.
+
+## Timeouts
+
+Every sandbox has two clocks. Whichever runs out first terminates the sandbox and stops billing:
+
+- **Idle timeout** (`idleTimeoutSeconds`): how long the sandbox can go without activity. The deadline is reported in `idleExpiresAt` and moves forward with each activity, recorded in `lastActivityAt`.
+- **Maximum lifetime** (`maxLifetimeSeconds`): a hard ceiling on total lifetime, counted from creation. The deadline is reported in `expiresAt`.
+
+{/* TODO(DOCS-509): Confirm production values with Zack (AppConfig sandboxPolicyConfig). */}
+
+| Setting | Default | Maximum |
+| --- | --- | --- |
+| Idle timeout | 10 minutes | 2 hours |
+| Maximum lifetime | 1 hour | 24 hours |
+
+If you omit either value, the default applies. If you request a value above the maximum, it's clamped to the maximum. Check `idleTimeoutSeconds` and `maxLifetimeSeconds` in the response to see the values actually applied.
+
+
+
+Terminate sandboxes explicitly when you're finished, including in your error-handling paths. Don't rely on a timeout to stop billing.
+
+
+
+## Network access
+
+By default, a sandbox has **open outbound internet access**. Code inside it can reach any host on the internet, for example to install packages or call external APIs.
+
+Restricting outbound traffic is opt-in. Set `egressMode` when you create the sandbox:
+
+- `ALLOW_ALL`: open outbound access. This is the default.
+- `ALLOW_LIST`: only the hosts listed in `egressAllowlist` are reachable.
+- `DENY_ALL`: no outbound access.
+
+{/* TODO(DOCS-509): egressMode/egressAllowlist ship in rphttp2#313 / AI-205 and are not in the dev spec yet. Verify field names and allowlist format before publishing, and add them to the create reference page. */}
+
+If you're running untrusted code that doesn't need the internet, consider setting `egressMode` to `DENY_ALL`.
+
+## Compute and pricing
+
+Sandboxes run on CPU compute. You can set `cpuFlavorId`, `vcpuCount`, and `memoryInGb` when you create a sandbox, or omit them to use the standard sandbox configuration. The `compute` object in the sandbox response shows the vCPUs, memory, and container disk backing it.
+
+Sandboxes are billed per second from creation until termination, at the rate shown in `compute.costPerHr` (USD per hour). Your account needs enough balance to start a sandbox; otherwise, create returns `402`.
+
+## Labels
+
+Attach `labels` (string key-value pairs) when you create a sandbox to track which job, user, or environment it belongs to. You can filter `GET /v2/sandboxes` by one or more labels, written as `key=value`.
+
+## Known limitations
+
+These limitations reflect current behavior and will change as the feature matures:
+
+- **The first command can return `409`.** `RUNNING` means compute is allocated, not that the container has finished starting. For about 10 to 30 seconds after a sandbox reaches `RUNNING`, `exec` and `logs` can return `409`. Retry with a short backoff.
+- **Command output arrives when the command finishes.** `exec` returns combined stdout and stderr in a single `output` field, plus an `error` field, only after the command completes. There is no separate exit code yet.
+- **Long-running commands are cut off.** Each `exec` call has a short execution cap. For longer work, start the process in the background and poll for its result:
+
+ ```json
+ {"command": ["sh", "-c", "setsid nohup python job.py > /tmp/job.log 2>&1 &"]}
+ ```
+
+ Then read `/tmp/job.log` with later `exec` calls.
+
+## Next steps
+
+- [Create your first sandbox](/sandboxes/quickstart).
+- Browse the [Sandboxes API reference](/api-reference-v2/sandboxes/create-a-sandbox).
+- Learn how to [create an API key](/get-started/api-keys).
From d3beddc4348c8689087d9ebdf9ebd8b61d633746 Mon Sep 17 00:00:00 2001
From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com>
Date: Tue, 29 Sep 2026 13:17:07 +0000
Subject: [PATCH 02/14] Updated mintlify pages
- Created sandboxes/quickstart.mdx
- Updated docs.json
Mintlify-Source: dashboard-editor
---
docs.json | 3 +-
sandboxes/quickstart.mdx | 231 +++++++++++++++++++++++++++++++++++++++
2 files changed, 233 insertions(+), 1 deletion(-)
create mode 100644 sandboxes/quickstart.mdx
diff --git a/docs.json b/docs.json
index 2e5fe268f..ec7cceff1 100644
--- a/docs.json
+++ b/docs.json
@@ -285,7 +285,8 @@
{
"group": "Sandboxes",
"pages": [
- "sandboxes/overview"
+ "sandboxes/overview",
+ "sandboxes/quickstart"
]
},
{
diff --git a/sandboxes/quickstart.mdx b/sandboxes/quickstart.mdx
new file mode 100644
index 000000000..ce495c00b
--- /dev/null
+++ b/sandboxes/quickstart.mdx
@@ -0,0 +1,231 @@
+---
+title: "Sandboxes quickstart"
+sidebarTitle: "Quickstart"
+description: "Create a Runpod sandbox with the REST API, run a shell command inside it, stream its logs, and terminate it when you're done, in a few minutes."
+---
+
+Learn how to create a sandbox, run commands in it, and clean it up using the Runpod REST API v2.
+
+## What you'll learn
+
+- How to create a sandbox from a container image.
+- How to wait until the sandbox is ready to accept commands.
+- How to run a command and read its output.
+- How to stream the sandbox's logs.
+- How to terminate the sandbox so billing stops.
+
+## Requirements
+
+- A Runpod account with a positive balance.
+- A [Runpod API key](/get-started/api-keys).
+- `curl` and [`jq`](https://jqlang.org/) for the command-line examples, or Python 3 with the `requests` package for the Python examples.
+
+Set your API key as an environment variable so the examples can use it:
+
+```bash
+export RUNPOD_API_KEY="YOUR_API_KEY"
+```
+
+## Step 1: Create a sandbox
+
+Create a sandbox from the `python:3.12-slim` image with a five-minute idle timeout:
+
+
+
+```bash cURL
+curl --request POST \
+ --url https://api.runpod.io/v2/sandboxes \
+ --header "Authorization: Bearer $RUNPOD_API_KEY" \
+ --header "Content-Type: application/json" \
+ --data '{
+ "name": "quickstart-sandbox",
+ "imageName": "python:3.12-slim",
+ "idleTimeoutSeconds": 300,
+ "labels": {"project": "quickstart"}
+ }'
+```
+
+```python Python
+import os
+import requests
+
+BASE_URL = "https://api.runpod.io/v2"
+HEADERS = {"Authorization": f"Bearer {os.environ['RUNPOD_API_KEY']}"}
+
+response = requests.post(
+ f"{BASE_URL}/sandboxes",
+ headers=HEADERS,
+ json={
+ "name": "quickstart-sandbox",
+ "imageName": "python:3.12-slim",
+ "idleTimeoutSeconds": 300,
+ "labels": {"project": "quickstart"},
+ },
+)
+response.raise_for_status()
+sandbox = response.json()
+sandbox_id = sandbox["id"]
+print(sandbox_id, sandbox["state"])
+```
+
+
+
+The response is the new sandbox. Note its `id`, which you use in every later request:
+
+```json
+{
+ "id": "7h3sbx91kq",
+ "name": "quickstart-sandbox",
+ "state": "CREATING",
+ "imageName": "python:3.12-slim",
+ "idleTimeoutSeconds": 300,
+ "maxLifetimeSeconds": 3600,
+ "labels": {"project": "quickstart"},
+ "...": "..."
+}
+```
+
+You must supply exactly one of `imageName` or `templateId`. Every other field is optional. See [Create a sandbox](/api-reference-v2/sandboxes/create-a-sandbox) for all fields.
+
+
+
+By default, the sandbox has open outbound internet access. To restrict it, see [Network access](/sandboxes/overview#network-access).
+
+
+
+## Step 2: Wait for the sandbox to be ready
+
+Poll the sandbox until its `state` is `RUNNING`:
+
+
+
+```bash cURL
+export SANDBOX_ID="7h3sbx91kq"
+
+curl --request GET \
+ --url https://api.runpod.io/v2/sandboxes/$SANDBOX_ID \
+ --header "Authorization: Bearer $RUNPOD_API_KEY" | jq -r .state
+```
+
+```python Python
+import time
+
+while True:
+ sandbox = requests.get(f"{BASE_URL}/sandboxes/{sandbox_id}", headers=HEADERS).json()
+ if sandbox["state"] == "RUNNING":
+ break
+ if sandbox["state"] in ("FAILED", "TERMINATED"):
+ raise RuntimeError(f"Sandbox stopped: {sandbox.get('terminationReason')}")
+ time.sleep(2)
+```
+
+
+
+If the state becomes `FAILED`, check `terminationReason` in the response.
+
+## Step 3: Run a command
+
+Send a command to the sandbox as an argv array. Commands aren't run through a shell, so wrap them in `["sh", "-c", "..."]` when you need shell syntax such as pipes or `&&`:
+
+
+
+```bash cURL
+curl --request POST \
+ --url https://api.runpod.io/v2/sandboxes/$SANDBOX_ID/exec \
+ --header "Authorization: Bearer $RUNPOD_API_KEY" \
+ --header "Content-Type: application/json" \
+ --data '{"command": ["sh", "-c", "python --version && uname -a"]}'
+```
+
+```python Python
+def run(command, retries=15):
+ # The container can take 10-30 seconds to start after RUNNING,
+ # so retry on 409 with a short delay.
+ for _ in range(retries):
+ response = requests.post(
+ f"{BASE_URL}/sandboxes/{sandbox_id}/exec",
+ headers=HEADERS,
+ json={"command": command},
+ )
+ if response.status_code != 409:
+ response.raise_for_status()
+ return response.json()
+ time.sleep(2)
+ raise TimeoutError("Sandbox did not accept commands in time")
+
+result = run(["sh", "-c", "python --version && uname -a"])
+print(result["output"])
+if result.get("error"):
+ print("Command failed:", result["error"])
+```
+
+
+
+The response contains the command's combined stdout and stderr:
+
+```json
+{
+ "output": "Python 3.12.7\nLinux 7h3sbx91kq 4.4.0 #1 SMP Sun Jan 10 15:06:54 PST 2016 x86_64 GNU/Linux\n",
+ "error": null
+}
+```
+
+`error` is `null` when the command succeeds. If the command exits with a non-zero status or runs past the execution cap, `error` describes the failure and `output` still contains anything the command wrote before it stopped.
+
+
+
+For 10 to 30 seconds after the sandbox reaches `RUNNING`, `exec` can return `409` while the container finishes starting. Retry after a short delay, as the Python example does.
+
+
+
+## Step 4: Stream logs
+
+Stream the sandbox's logs as Server-Sent Events. The stream includes the main process's stdout (`container`) and lifecycle events such as image pulls (`system`). Output from `exec` commands doesn't appear here.
+
+```bash
+curl --no-buffer --request GET \
+ --url "https://api.runpod.io/v2/sandboxes/$SANDBOX_ID/logs?tail=50" \
+ --header "Authorization: Bearer $RUNPOD_API_KEY" \
+ --header "Accept: text/event-stream"
+```
+
+Each event carries a JSON payload:
+
+```text
+id: 2026-09-29T14:02:11.482Z
+data: {"source":"system","line":"Pulling image python:3.12-slim","ts":"2026-09-29T14:02:11.482Z"}
+```
+
+Press `Ctrl+C` to stop streaming.
+
+## Step 5: Terminate the sandbox
+
+When you're done, terminate the sandbox to stop billing:
+
+
+
+```bash cURL
+curl --request DELETE \
+ --url https://api.runpod.io/v2/sandboxes/$SANDBOX_ID \
+ --header "Authorization: Bearer $RUNPOD_API_KEY"
+```
+
+```python Python
+requests.delete(f"{BASE_URL}/sandboxes/{sandbox_id}", headers=HEADERS).raise_for_status()
+```
+
+
+
+A successful request returns `204` with no body. Terminating a sandbox is final: you can still read it with `GET`, but you can't restart it.
+
+
+
+In your own code, terminate the sandbox in a `finally` block or equivalent, so it's cleaned up even when a command fails. If you forget, the sandbox terminates at its idle timeout or maximum lifetime, whichever comes first.
+
+
+
+## Next steps
+
+- Learn about [timeouts, network access, and pricing](/sandboxes/overview).
+- Run long commands in the background, as described in [Known limitations](/sandboxes/overview#known-limitations).
+- Browse the full [Sandboxes API reference](/api-reference-v2/sandboxes/create-a-sandbox).
From 103dc10ac9cdb2d61c44bdb47ce26192a9b8057c Mon Sep 17 00:00:00 2001
From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com>
Date: Tue, 29 Sep 2026 13:20:26 +0000
Subject: [PATCH 03/14] Updated mintlify pages
- Updated sandboxes/quickstart.mdx
- Created api-reference-v2/sandboxes/list-sandboxes.mdx
- Created api-reference-v2/sandboxes/create-a-sandbox.mdx
- Created api-reference-v2/sandboxes/get-a-sandbox.mdx
- Created api-reference-v2/sandboxes/terminate-a-sandbox.mdx
- Created api-reference-v2/sandboxes/run-a-command-in-a-sandbox.mdx
- Created api-reference-v2/sandboxes/stream-sandbox-logs.mdx
- Updated docs.json
Mintlify-Source: dashboard-editor
---
.../sandboxes/create-a-sandbox.mdx | 143 +++++++++++++++
api-reference-v2/sandboxes/get-a-sandbox.mdx | 167 ++++++++++++++++++
api-reference-v2/sandboxes/list-sandboxes.mdx | 89 ++++++++++
.../sandboxes/run-a-command-in-a-sandbox.mdx | 84 +++++++++
.../sandboxes/stream-sandbox-logs.mdx | 119 +++++++++++++
.../sandboxes/terminate-a-sandbox.mdx | 60 +++++++
docs.json | 11 ++
sandboxes/quickstart.mdx | 6 +-
8 files changed, 676 insertions(+), 3 deletions(-)
create mode 100644 api-reference-v2/sandboxes/create-a-sandbox.mdx
create mode 100644 api-reference-v2/sandboxes/get-a-sandbox.mdx
create mode 100644 api-reference-v2/sandboxes/list-sandboxes.mdx
create mode 100644 api-reference-v2/sandboxes/run-a-command-in-a-sandbox.mdx
create mode 100644 api-reference-v2/sandboxes/stream-sandbox-logs.mdx
create mode 100644 api-reference-v2/sandboxes/terminate-a-sandbox.mdx
diff --git a/api-reference-v2/sandboxes/create-a-sandbox.mdx b/api-reference-v2/sandboxes/create-a-sandbox.mdx
new file mode 100644
index 000000000..7bb431d88
--- /dev/null
+++ b/api-reference-v2/sandboxes/create-a-sandbox.mdx
@@ -0,0 +1,143 @@
+---
+title: "Create a sandbox"
+api: "POST https://api.runpod.io/v2/sandboxes"
+authMethod: "bearer"
+description: "Create an isolated, fast-starting Runpod CPU sandbox from a container image or template, with optional timeouts, compute size, env vars, and labels."
+---
+
+Launches a new sandbox: an isolated, fast-starting CPU environment for running untrusted code. Supply exactly one of `templateId` or `imageName`. Every other field is optional, and omitted fields use your account's default sandbox policy.
+
+The request returns `201` with the sandbox, usually still in the `CREATING` state. Poll [Get a sandbox](/api-reference-v2/sandboxes/get-a-sandbox) until `state` is `RUNNING`.
+
+The sandbox is billed per second until it terminates. Terminate it explicitly when you're finished, including on error paths, rather than relying on a timeout to stop billing.
+
+
+
+There is no idempotency key yet. If a create request fails at the network layer, [list your sandboxes](/api-reference-v2/sandboxes/list-sandboxes) (for example, by a label you set) before retrying, to avoid creating a duplicate.
+
+
+
+### Body
+
+
+ Container image to run, for example `python:3.12-slim`. Required unless `templateId` is given. Can't be combined with `templateId`.
+
+
+
+ [Template](/pods/templates/overview) to launch the sandbox from. Required unless `imageName` is given. Can't be combined with `imageName`.
+
+
+
+ Human-readable name. Names don't have to be unique. Generated when omitted.
+
+
+
+ CPU flavor to back the sandbox, for example `cpu3c`. Defaults to the standard sandbox flavor.
+
+
+
+ Number of vCPUs. Minimum `1`. Memory and container disk scale with the flavor.
+
+
+
+ Memory in GB. Minimum `1`.
+
+
+
+ Preferred data center, for example `US-KS-2`. Omit to let Runpod choose.
+
+
+
+ Environment variables to set in the sandbox, as string key-value pairs.
+
+
+
+ Seconds of inactivity before the sandbox is terminated. Omitted values use the default (10 minutes). Values above the maximum (2 hours) are clamped to the maximum.
+
+
+
+ Maximum total lifetime in seconds, counted from creation. Omitted values use the default (1 hour). Values above the maximum (24 hours) are clamped to the maximum.
+
+
+
+ String key-value labels for organizing sandboxes. You can filter [List sandboxes](/api-reference-v2/sandboxes/list-sandboxes) by label.
+
+
+{/* TODO(DOCS-509): Add egressMode (ALLOW_ALL default | ALLOW_LIST | DENY_ALL) and egressAllowlist once rphttp2#313 / AI-205 lands in the spec. Confirm timeout defaults and maximums with Zack. */}
+
+### Response
+
+Returns the created sandbox. See [Get a sandbox](/api-reference-v2/sandboxes/get-a-sandbox) for all response fields.
+
+### Errors
+
+| Status | Meaning |
+| --- | --- |
+| `400` | The request is malformed, for example both or neither of `templateId` and `imageName` are set, or it includes an unknown field. |
+| `401` | The API key is missing or invalid. |
+| `402` | Your account balance is too low to start the sandbox. |
+| `403` | The API key doesn't have access to sandboxes. |
+| `422` | The request is well-formed but can't be processed, for example an unknown template or flavor. |
+| `429` | Too many requests. Retry after a delay. |
+| `503` | No capacity is available for the requested configuration. Retry later, or remove placement constraints such as `dataCenterId`. |
+
+
+
+```bash cURL
+curl --request POST \
+ --url https://api.runpod.io/v2/sandboxes \
+ --header "Authorization: Bearer $RUNPOD_API_KEY" \
+ --header "Content-Type: application/json" \
+ --data '{
+ "name": "my-sandbox",
+ "imageName": "python:3.12-slim",
+ "idleTimeoutSeconds": 300,
+ "env": {"APP_ENV": "test"},
+ "labels": {"project": "checkout"}
+ }'
+```
+
+```python Python
+import os
+import requests
+
+response = requests.post(
+ "https://api.runpod.io/v2/sandboxes",
+ headers={"Authorization": f"Bearer {os.environ['RUNPOD_API_KEY']}"},
+ json={
+ "name": "my-sandbox",
+ "imageName": "python:3.12-slim",
+ "idleTimeoutSeconds": 300,
+ "env": {"APP_ENV": "test"},
+ "labels": {"project": "checkout"},
+ },
+)
+print(response.status_code, response.json())
+```
+
+
+
+
+
+```json 201
+{
+ "id": "7h3sbx91kq",
+ "name": "my-sandbox",
+ "state": "CREATING",
+ "templateId": null,
+ "imageName": "python:3.12-slim",
+ "cpuFlavorId": "cpu3c",
+ "idleTimeoutSeconds": 300,
+ "maxLifetimeSeconds": 3600,
+ "lastActivityAt": "2026-09-29T14:02:05Z",
+ "idleExpiresAt": "2026-09-29T14:07:05Z",
+ "expiresAt": "2026-09-29T15:02:05Z",
+ "labels": {"project": "checkout"},
+ "terminationReason": null,
+ "terminatedAt": null,
+ "createdAt": "2026-09-29T14:02:05Z",
+ "updatedAt": "2026-09-29T14:02:05Z"
+}
+```
+
+
diff --git a/api-reference-v2/sandboxes/get-a-sandbox.mdx b/api-reference-v2/sandboxes/get-a-sandbox.mdx
new file mode 100644
index 000000000..6568b1e65
--- /dev/null
+++ b/api-reference-v2/sandboxes/get-a-sandbox.mdx
@@ -0,0 +1,167 @@
+---
+title: "Get a sandbox"
+api: "GET https://api.runpod.io/v2/sandboxes/{id}"
+authMethod: "bearer"
+description: "Retrieve a Runpod sandbox by ID with its lifecycle state, compute, cost per hour, timeout deadlines, labels, and termination reason if it has stopped."
+---
+
+Returns a single sandbox by ID. Terminated sandboxes remain readable, with `state`, `terminationReason`, and `terminatedAt` set.
+
+`RUNNING` means the backing compute is allocated, not that the container has finished pulling and started. A sandbox can report `RUNNING` for a short while (typically 10 to 30 seconds) before it accepts commands.
+
+### Path parameters
+
+
+ The sandbox ID.
+
+
+### Response
+
+
+ Unique sandbox identifier.
+
+
+
+ Human-readable name. Not required to be unique.
+
+
+
+ Current lifecycle state: `CREATING`, `RUNNING`, `TERMINATED`, or `FAILED`.
+
+
+
+ Template the sandbox was created from, if any.
+
+
+
+ Container image the sandbox runs, if it was created from an image.
+
+
+
+ CPU flavor backing the sandbox.
+
+
+
+ The compute currently backing the sandbox. Absent while the sandbox is `CREATING` and after it's `TERMINATED`.
+
+
+
+ Number of vCPUs.
+
+
+
+ Memory in GB.
+
+
+
+ Container disk size in GB.
+
+
+
+ Current cost per hour in USD. Billing is per second at this rate.
+
+
+
+
+
+ Seconds of inactivity after which the sandbox is terminated, after any clamping to the account maximum.
+
+
+
+ Maximum total lifetime in seconds, after any clamping to the account maximum.
+
+
+
+ Time of the most recent recorded activity.
+
+
+
+ Time after which the sandbox is terminated for inactivity if no further activity occurs.
+
+
+
+ Time after which the sandbox is terminated for reaching its maximum lifetime.
+
+
+
+ User-supplied string key-value labels.
+
+
+
+ Why the sandbox was terminated. `null` while the sandbox is alive.
+
+
+
+ When the sandbox was terminated. `null` while the sandbox is alive.
+
+
+
+ When the sandbox was created.
+
+
+
+ When the sandbox was last updated.
+
+
+{/* TODO(DOCS-509): List possible terminationReason values once engineering documents them in the spec. */}
+
+### Errors
+
+| Status | Meaning |
+| --- | --- |
+| `401` | The API key is missing or invalid. |
+| `403` | The API key doesn't have access to sandboxes. |
+| `404` | No sandbox with this ID exists on your account. |
+| `429` | Too many requests. Retry after a delay. |
+
+
+
+```bash cURL
+curl --request GET \
+ --url https://api.runpod.io/v2/sandboxes/SANDBOX_ID \
+ --header "Authorization: Bearer $RUNPOD_API_KEY"
+```
+
+```python Python
+import os
+import requests
+
+response = requests.get(
+ "https://api.runpod.io/v2/sandboxes/SANDBOX_ID",
+ headers={"Authorization": f"Bearer {os.environ['RUNPOD_API_KEY']}"},
+)
+print(response.json())
+```
+
+
+
+
+
+```json 200
+{
+ "id": "7h3sbx91kq",
+ "name": "my-sandbox",
+ "state": "RUNNING",
+ "templateId": null,
+ "imageName": "python:3.12-slim",
+ "cpuFlavorId": "cpu3c",
+ "compute": {
+ "vcpuCount": 2,
+ "memoryInGb": 4,
+ "containerDiskInGb": 10,
+ "costPerHr": 0.026
+ },
+ "idleTimeoutSeconds": 300,
+ "maxLifetimeSeconds": 3600,
+ "lastActivityAt": "2026-09-29T14:02:40Z",
+ "idleExpiresAt": "2026-09-29T14:07:40Z",
+ "expiresAt": "2026-09-29T15:02:05Z",
+ "labels": {"project": "checkout"},
+ "terminationReason": null,
+ "terminatedAt": null,
+ "createdAt": "2026-09-29T14:02:05Z",
+ "updatedAt": "2026-09-29T14:02:40Z"
+}
+```
+
+
diff --git a/api-reference-v2/sandboxes/list-sandboxes.mdx b/api-reference-v2/sandboxes/list-sandboxes.mdx
new file mode 100644
index 000000000..fa9297077
--- /dev/null
+++ b/api-reference-v2/sandboxes/list-sandboxes.mdx
@@ -0,0 +1,89 @@
+---
+title: "List sandboxes"
+api: "GET https://api.runpod.io/v2/sandboxes"
+authMethod: "bearer"
+description: "List the live Runpod sandboxes owned by the authenticated user, filtered by lifecycle state or by labels. Terminated sandboxes are excluded by default."
+---
+
+Returns the authenticated user's live sandboxes. Terminated sandboxes are excluded unless you set `state=TERMINATED`. A terminated sandbox stays readable by ID either way. When you pass several filters, a sandbox must match all of them.
+
+### Query parameters
+
+
+ Only return sandboxes in this lifecycle state. One of `CREATING`, `RUNNING`, `TERMINATED`, or `FAILED`.
+
+
+
+ Only return sandboxes carrying every one of these labels, each written as `key=value`. Keys and values match exactly, with no wildcard or prefix matching. Repeat the parameter for multiple labels, for example `?labels=project=checkout&labels=env=staging`.
+
+
+### Response
+
+
+ The matching sandboxes. See [Get a sandbox](/api-reference-v2/sandboxes/get-a-sandbox) for the fields of each sandbox.
+
+
+### Errors
+
+| Status | Meaning |
+| --- | --- |
+| `401` | The API key is missing or invalid. |
+| `403` | The API key doesn't have access to sandboxes. |
+| `429` | Too many requests. Retry after a delay. |
+
+
+
+```bash cURL
+curl --request GET \
+ --url "https://api.runpod.io/v2/sandboxes?state=RUNNING&labels=project=quickstart" \
+ --header "Authorization: Bearer $RUNPOD_API_KEY"
+```
+
+```python Python
+import os
+import requests
+
+response = requests.get(
+ "https://api.runpod.io/v2/sandboxes",
+ headers={"Authorization": f"Bearer {os.environ['RUNPOD_API_KEY']}"},
+ params={"state": "RUNNING", "labels": ["project=quickstart"]},
+)
+print(response.json())
+```
+
+
+
+
+
+```json 200
+{
+ "sandboxes": [
+ {
+ "id": "7h3sbx91kq",
+ "name": "quickstart-sandbox",
+ "state": "RUNNING",
+ "templateId": null,
+ "imageName": "python:3.12-slim",
+ "cpuFlavorId": "cpu3c",
+ "compute": {
+ "vcpuCount": 2,
+ "memoryInGb": 4,
+ "containerDiskInGb": 10,
+ "costPerHr": 0.026
+ },
+ "idleTimeoutSeconds": 300,
+ "maxLifetimeSeconds": 3600,
+ "lastActivityAt": "2026-09-29T14:02:40Z",
+ "idleExpiresAt": "2026-09-29T14:07:40Z",
+ "expiresAt": "2026-09-29T15:02:05Z",
+ "labels": {"project": "quickstart"},
+ "terminationReason": null,
+ "terminatedAt": null,
+ "createdAt": "2026-09-29T14:02:05Z",
+ "updatedAt": "2026-09-29T14:02:40Z"
+ }
+ ]
+}
+```
+
+
diff --git a/api-reference-v2/sandboxes/run-a-command-in-a-sandbox.mdx b/api-reference-v2/sandboxes/run-a-command-in-a-sandbox.mdx
new file mode 100644
index 000000000..a088abba7
--- /dev/null
+++ b/api-reference-v2/sandboxes/run-a-command-in-a-sandbox.mdx
@@ -0,0 +1,84 @@
+---
+title: "Run a command in a sandbox"
+api: "POST https://api.runpod.io/v2/sandboxes/{id}/exec"
+authMethod: "bearer"
+description: "Run a single command inside a running Runpod sandbox and get its combined stdout and stderr, plus an error field if the command fails or times out."
+---
+
+Runs one command inside the sandbox and returns its combined stdout and stderr once it completes.
+
+Each command has a short execution cap. A command that runs past the cap is killed and reports an `error`. For longer work, start the process in the background with `setsid nohup ... &`, write its output to a file, and read the file with later calls. See [Known limitations](/sandboxes/overview#known-limitations).
+
+A sandbox accepts commands only once its container has started, which can lag the `RUNNING` state by 10 to 30 seconds. Until then, this endpoint returns `409`. Retry after a short delay.
+
+{/* TODO(DOCS-509): Update when streaming exec (AI-191) and separate stdout/stderr/exit code (AI-246) ship. Confirm the exact per-command cap with Zack. */}
+
+### Path parameters
+
+
+ The sandbox ID.
+
+
+### Body
+
+
+ The command to run, as an argv array with at least one element. It isn't interpreted by a shell. To use shell syntax such as pipes, redirects, or `&&`, wrap it: `["sh", "-c", "..."]`.
+
+
+### Response
+
+
+ Combined stdout and stderr of the command. If the command failed or was killed, this still contains whatever it wrote before it stopped.
+
+
+
+ The failure reported by the host: the command exited with a non-zero status, or ran past the per-command execution cap and was killed. `null` when the command succeeded.
+
+
+### Errors
+
+| Status | Meaning |
+| --- | --- |
+| `401` | The API key is missing or invalid. |
+| `403` | The API key doesn't have access to sandboxes. |
+| `404` | No sandbox with this ID exists on your account. |
+| `409` | The sandbox can't accept commands right now. Its container is still starting (retry shortly), or it has been terminated. |
+| `422` | The request is well-formed but can't be processed. |
+| `429` | Too many requests. Retry after a delay. |
+| `503` | Command execution is temporarily unavailable. The sandbox itself is unaffected. Retry later. |
+
+
+
+```bash cURL
+curl --request POST \
+ --url https://api.runpod.io/v2/sandboxes/SANDBOX_ID/exec \
+ --header "Authorization: Bearer $RUNPOD_API_KEY" \
+ --header "Content-Type: application/json" \
+ --data '{"command": ["sh", "-c", "echo hello && python --version"]}'
+```
+
+```python Python
+import os
+import requests
+
+response = requests.post(
+ "https://api.runpod.io/v2/sandboxes/SANDBOX_ID/exec",
+ headers={"Authorization": f"Bearer {os.environ['RUNPOD_API_KEY']}"},
+ json={"command": ["sh", "-c", "echo hello && python --version"]},
+)
+result = response.json()
+print(result["output"])
+```
+
+
+
+
+
+```json 200
+{
+ "output": "hello\nPython 3.12.7\n",
+ "error": null
+}
+```
+
+
diff --git a/api-reference-v2/sandboxes/stream-sandbox-logs.mdx b/api-reference-v2/sandboxes/stream-sandbox-logs.mdx
new file mode 100644
index 000000000..e36fd167a
--- /dev/null
+++ b/api-reference-v2/sandboxes/stream-sandbox-logs.mdx
@@ -0,0 +1,119 @@
+---
+title: "Stream sandbox logs"
+api: "GET https://api.runpod.io/v2/sandboxes/{id}/logs"
+authMethod: "bearer"
+description: "Stream a Runpod sandbox's container and system logs in real time as Server-Sent Events, with backfill and resumable reconnects via Last-Event-ID."
+---
+
+Streams the sandbox's logs as [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events). Two log sources are available:
+
+- `container`: stdout of the sandbox's main process.
+- `system`: lifecycle events, such as image pulls and container starts.
+
+Output from [exec](/api-reference-v2/sandboxes/run-a-command-in-a-sandbox) commands doesn't appear in this stream.
+
+Each event has an `id` line and a JSON `data` payload:
+
+```text
+id: 2026-09-29T14:02:11.482Z
+data: {"source":"container","line":"...","ts":"2026-09-29T14:02:11.482Z"}
+```
+
+The event `id` is the event's `ts` timestamp, so an `EventSource` client that reconnects resumes where it left off by sending `Last-Event-ID`.
+
+Logs are available only once the sandbox has a backing container, which can lag creation by a short while. Until then, and after the sandbox terminates, this endpoint returns `409`.
+
+### Path parameters
+
+
+ The sandbox ID.
+
+
+### Query parameters
+
+
+ Log source to stream: `container` or `system`. Omit to include both.
+
+
+
+ Number of historical lines to backfill before streaming live. Set `0` to stream live with no backfill. Maximum `5000`. Ignored when `since` or `Last-Event-ID` is provided.
+
+
+
+ RFC 3339 timestamp to resume from. When set, `tail` is ignored. Ignored when `Last-Event-ID` is provided.
+
+
+### Headers
+
+
+ Reconnect cursor: an event `id` previously emitted by this endpoint. Takes precedence over `since` and `tail`. `EventSource` sends it automatically on reconnect.
+
+
+### Response
+
+A `text/event-stream` response. Each event's `data` payload has these fields:
+
+
+ The log source: `container` or `system`.
+
+
+
+ The log line.
+
+
+
+ When the line was logged. Also used as the event `id`.
+
+
+### Errors
+
+| Status | Meaning |
+| --- | --- |
+| `401` | The API key is missing or invalid. |
+| `403` | The API key doesn't have access to sandboxes. |
+| `404` | No sandbox with this ID exists on your account. |
+| `409` | The sandbox has no streamable logs right now. Its container is still starting (retry shortly), or it has been terminated. |
+| `429` | Too many requests. Retry after a delay. |
+
+
+
+```bash cURL
+curl --no-buffer --request GET \
+ --url "https://api.runpod.io/v2/sandboxes/SANDBOX_ID/logs?source=container&tail=50" \
+ --header "Authorization: Bearer $RUNPOD_API_KEY" \
+ --header "Accept: text/event-stream"
+```
+
+```python Python
+import json
+import os
+import requests
+
+with requests.get(
+ "https://api.runpod.io/v2/sandboxes/SANDBOX_ID/logs",
+ headers={
+ "Authorization": f"Bearer {os.environ['RUNPOD_API_KEY']}",
+ "Accept": "text/event-stream",
+ },
+ params={"tail": 50},
+ stream=True,
+) as response:
+ for line in response.iter_lines(decode_unicode=True):
+ if line and line.startswith("data:"):
+ event = json.loads(line[len("data:"):])
+ print(event["source"], event["line"])
+```
+
+
+
+
+
+```text 200
+id: 2026-09-29T14:02:06.120Z
+data: {"source":"system","line":"Pulling image python:3.12-slim","ts":"2026-09-29T14:02:06.120Z"}
+
+id: 2026-09-29T14:02:11.482Z
+data: {"source":"system","line":"Container started","ts":"2026-09-29T14:02:11.482Z"}
+```
+
+
diff --git a/api-reference-v2/sandboxes/terminate-a-sandbox.mdx b/api-reference-v2/sandboxes/terminate-a-sandbox.mdx
new file mode 100644
index 000000000..79783d7ac
--- /dev/null
+++ b/api-reference-v2/sandboxes/terminate-a-sandbox.mdx
@@ -0,0 +1,60 @@
+---
+title: "Terminate a sandbox"
+api: "DELETE https://api.runpod.io/v2/sandboxes/{id}"
+authMethod: "bearer"
+description: "Terminate a Runpod sandbox to release its compute and stop billing. Termination is final, safe to retry, and the record stays readable by ID."
+---
+
+Terminates a sandbox and releases its compute. Billing stops at termination.
+
+Termination is final: a terminated sandbox can't be restarted. The sandbox record stays readable through [Get a sandbox](/api-reference-v2/sandboxes/get-a-sandbox) afterward, with `terminationReason` and `terminatedAt` set.
+
+Terminating a sandbox that is already terminated succeeds and has no effect, so it's safe to retry this request.
+
+### Path parameters
+
+
+ The sandbox ID.
+
+
+### Response
+
+Returns `204` with no body.
+
+### Errors
+
+| Status | Meaning |
+| --- | --- |
+| `401` | The API key is missing or invalid. |
+| `403` | The API key doesn't have access to sandboxes. |
+| `404` | No sandbox with this ID exists on your account. |
+| `429` | Too many requests. Retry after a delay. |
+
+
+
+```bash cURL
+curl --request DELETE \
+ --url https://api.runpod.io/v2/sandboxes/SANDBOX_ID \
+ --header "Authorization: Bearer $RUNPOD_API_KEY"
+```
+
+```python Python
+import os
+import requests
+
+response = requests.delete(
+ "https://api.runpod.io/v2/sandboxes/SANDBOX_ID",
+ headers={"Authorization": f"Bearer {os.environ['RUNPOD_API_KEY']}"},
+)
+print(response.status_code) # 204
+```
+
+
+
+
+
+```text 204
+No content
+```
+
+
diff --git a/docs.json b/docs.json
index ec7cceff1..d1c14b8a0 100644
--- a/docs.json
+++ b/docs.json
@@ -608,6 +608,17 @@
"api-reference-v2/clusters/list-a-clusters-pods"
]
},
+ {
+ "group": "Sandboxes",
+ "pages": [
+ "api-reference-v2/sandboxes/list-sandboxes",
+ "api-reference-v2/sandboxes/create-a-sandbox",
+ "api-reference-v2/sandboxes/get-a-sandbox",
+ "api-reference-v2/sandboxes/terminate-a-sandbox",
+ "api-reference-v2/sandboxes/run-a-command-in-a-sandbox",
+ "api-reference-v2/sandboxes/stream-sandbox-logs"
+ ]
+ },
{
"group": "Serverless",
"pages": [
diff --git a/sandboxes/quickstart.mdx b/sandboxes/quickstart.mdx
index ce495c00b..dcc1596e9 100644
--- a/sandboxes/quickstart.mdx
+++ b/sandboxes/quickstart.mdx
@@ -134,7 +134,7 @@ curl --request POST \
--url https://api.runpod.io/v2/sandboxes/$SANDBOX_ID/exec \
--header "Authorization: Bearer $RUNPOD_API_KEY" \
--header "Content-Type: application/json" \
- --data '{"command": ["sh", "-c", "python --version && uname -a"]}'
+ --data '{"command": ["sh", "-c", "echo hello && python --version"]}'
```
```python Python
@@ -153,7 +153,7 @@ def run(command, retries=15):
time.sleep(2)
raise TimeoutError("Sandbox did not accept commands in time")
-result = run(["sh", "-c", "python --version && uname -a"])
+result = run(["sh", "-c", "echo hello && python --version"])
print(result["output"])
if result.get("error"):
print("Command failed:", result["error"])
@@ -165,7 +165,7 @@ The response contains the command's combined stdout and stderr:
```json
{
- "output": "Python 3.12.7\nLinux 7h3sbx91kq 4.4.0 #1 SMP Sun Jan 10 15:06:54 PST 2016 x86_64 GNU/Linux\n",
+ "output": "hello\nPython 3.12.7\n",
"error": null
}
```
From a5dc9c5815b0f53a50cea3bf207e5bcd4c765893 Mon Sep 17 00:00:00 2001
From: lgunreddi
Date: Tue, 29 Sep 2026 09:32:47 -0400
Subject: [PATCH 04/14] Updated mintlify pages
- Updated sandboxes/quickstart.mdx
- Updated sandboxes/overview.mdx
Mintlify-Source: dashboard-editor
---
sandboxes/overview.mdx | 2 +-
sandboxes/quickstart.mdx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/sandboxes/overview.mdx b/sandboxes/overview.mdx
index 32ee0104f..a822e0fb7 100644
--- a/sandboxes/overview.mdx
+++ b/sandboxes/overview.mdx
@@ -123,4 +123,4 @@ These limitations reflect current behavior and will change as the feature mature
- [Create your first sandbox](/sandboxes/quickstart).
- Browse the [Sandboxes API reference](/api-reference-v2/sandboxes/create-a-sandbox).
-- Learn how to [create an API key](/get-started/api-keys).
+- Learn how to [create an API key](/get-started/credentials).
diff --git a/sandboxes/quickstart.mdx b/sandboxes/quickstart.mdx
index dcc1596e9..6639923bd 100644
--- a/sandboxes/quickstart.mdx
+++ b/sandboxes/quickstart.mdx
@@ -17,7 +17,7 @@ Learn how to create a sandbox, run commands in it, and clean it up using the Run
## Requirements
- A Runpod account with a positive balance.
-- A [Runpod API key](/get-started/api-keys).
+- A [Runpod API key](/get-started/credentials).
- `curl` and [`jq`](https://jqlang.org/) for the command-line examples, or Python 3 with the `requests` package for the Python examples.
Set your API key as an environment variable so the examples can use it:
From 944748ee07ae52b34e41c62996bc3efb466da7cf Mon Sep 17 00:00:00 2001
From: lgunreddi
Date: Mon, 19 Oct 2026 11:52:01 -0400
Subject: [PATCH 05/14] Update create-a-sandbox.mdx
---
.../sandboxes/create-a-sandbox.mdx | 280 +++++++++++-------
1 file changed, 168 insertions(+), 112 deletions(-)
diff --git a/api-reference-v2/sandboxes/create-a-sandbox.mdx b/api-reference-v2/sandboxes/create-a-sandbox.mdx
index 7bb431d88..03ab391b2 100644
--- a/api-reference-v2/sandboxes/create-a-sandbox.mdx
+++ b/api-reference-v2/sandboxes/create-a-sandbox.mdx
@@ -1,143 +1,199 @@
---
-title: "Create a sandbox"
-api: "POST https://api.runpod.io/v2/sandboxes"
-authMethod: "bearer"
-description: "Create an isolated, fast-starting Runpod CPU sandbox from a container image or template, with optional timeouts, compute size, env vars, and labels."
+title: "Sandboxes"
+sidebarTitle: "Overview"
+description: "Create short-lived, isolated CPU environments on demand to run untrusted or agent-generated code, then tear them down. Billed per second."
---
-Launches a new sandbox: an isolated, fast-starting CPU environment for running untrusted code. Supply exactly one of `templateId` or `imageName`. Every other field is optional, and omitted fields use your account's default sandbox policy.
+Sandboxes are short-lived, isolated CPU environments that you create on demand, run commands in, and tear down when you're done. They're built for running untrusted code: code generated by an AI agent, code submitted by your users, or one-off scripts you don't want touching your own infrastructure.
-The request returns `201` with the sandbox, usually still in the `CREATING` state. Poll [Get a sandbox](/api-reference-v2/sandboxes/get-a-sandbox) until `state` is `RUNNING`.
+You manage sandboxes through the [REST API v2](/api-reference-v2/overview). Each sandbox runs a container image you choose (or a default image), starts in seconds, and is billed per second until it terminates.
-The sandbox is billed per second until it terminates. Terminate it explicitly when you're finished, including on error paths, rather than relying on a timeout to stop billing.
+
+
+ Create a sandbox, run a command, and terminate it.
+
+
+
+ Endpoints and fields for the `/v2/sandboxes` resource.
+
+
+
+## When to use a sandbox
+
+Use a sandbox when you need a clean, disposable environment for a short task:
+
+- An AI agent needs to execute the code it writes.
+- Your application runs code or commands supplied by end users.
+- You want to test a script or package in isolation before running it elsewhere.
+
+For long-running workloads, GPU compute, or persistent development environments, use [Pods](/pods/overview) instead. For request-driven inference that scales with traffic, use [Serverless](/serverless/overview).
+
+## Isolation
+
+Every sandbox runs under [gVisor](https://gvisor.dev/), a user-space kernel that intercepts the container's system calls. Code inside a sandbox can't reach the host machine or other tenants' sandboxes, so you can run code you don't trust.
+
+## Images and templates
+
+You can start a sandbox in three ways:
+
+- **Default image:** send an empty request body (`{}`) and the sandbox runs Runpod's default sandbox image.
+- **Container image:** set `image` to any public image, for example `python:3.12-slim`. For a private image, also set `registry` to the ID of one of your saved container registry credentials (from `/v2/registries`).
+- **Template:** set `templateId` to a sandbox template. Runpod provides curated templates, such as Ubuntu and Harbor.
+
+`image` and `templateId` can't be used together.
-There is no idempotency key yet. If a create request fails at the network layer, [list your sandboxes](/api-reference-v2/sandboxes/list-sandboxes) (for example, by a label you set) before retrying, to avoid creating a duplicate.
+**Review question:** Which templates will be available to customers at Beta, and should the docs list their IDs? _Remove this box before merging._
-### Body
+If you create a sandbox from an image without a start command, the container runs a keepalive process so it stays up for you to run commands in. To run your own main process, set `cmd` (and optionally `entrypoint`) in exec form, for example `["python", "-m", "http.server", "8080"]`.
-
- Container image to run, for example `python:3.12-slim`. Required unless `templateId` is given. Can't be combined with `templateId`.
-
+## Lifecycle
-
- [Template](/pods/templates/overview) to launch the sandbox from. Required unless `imageName` is given. Can't be combined with `imageName`.
-
+A sandbox moves through these states:
-
- Human-readable name. Names don't have to be unique. Generated when omitted.
-
+| State | Meaning |
+| --- | --- |
+| `CREATING` | Compute is being allocated and the container image is being prepared. |
+| `RUNNING` | Compute is allocated. The container may still be starting (see [Known limitations](#known-limitations)). |
+| `TERMINATED` | The sandbox has stopped and its compute is released. Billing has stopped. |
+| `FAILED` | The sandbox couldn't be created. |
-
- CPU flavor to back the sandbox, for example `cpu3c`. Defaults to the standard sandbox flavor.
-
+`POST /v2/sandboxes` returns `201` with the new sandbox, usually still in the `CREATING` state. Poll `GET /v2/sandboxes/{id}` until `state` is `RUNNING` before you send commands.
-
- Number of vCPUs. Minimum `1`. Memory and container disk scale with the flavor.
-
+### Retry creates safely
-
- Memory in GB. Minimum `1`.
-
+Send an `Idempotency-Key` header with each create request. If the response is lost and you retry with the same key and the same body, the API returns the sandbox the first request created instead of creating (and billing for) a second one.
-
- Preferred data center, for example `US-KS-2`. Omit to let Runpod choose.
-
+If the first create failed, for example because no capacity was available, that key is spent: retrying with it returns `409`. Retry a failed create with a new key.
-
- Environment variables to set in the sandbox, as string key-value pairs.
-
+### Termination is final
-
- Seconds of inactivity before the sandbox is terminated. Omitted values use the default (10 minutes). Values above the maximum (2 hours) are clamped to the maximum.
-
+A terminated sandbox can't be restarted. Its record stays readable through `GET /v2/sandboxes/{id}`, with `terminationReason` and `terminatedAt` set, so you can see why and when it stopped. To run more work, create a new sandbox.
-
- Maximum total lifetime in seconds, counted from creation. Omitted values use the default (1 hour). Values above the maximum (24 hours) are clamped to the maximum.
-
+Terminating a sandbox that is already terminated succeeds, so it's safe to retry `DELETE /v2/sandboxes/{id}`.
-
- String key-value labels for organizing sandboxes. You can filter [List sandboxes](/api-reference-v2/sandboxes/list-sandboxes) by label.
-
+## Timeouts
-{/* TODO(DOCS-509): Add egressMode (ALLOW_ALL default | ALLOW_LIST | DENY_ALL) and egressAllowlist once rphttp2#313 / AI-205 lands in the spec. Confirm timeout defaults and maximums with Zack. */}
+Every sandbox has two clocks. Whichever runs out first terminates the sandbox and stops billing:
-### Response
+- **Idle timeout** (`idleTimeoutSeconds`): how long the sandbox can go without activity. Running a command and keeping a log stream open both count as activity. The deadline is reported in `idleExpiresAt`.
+- **Maximum lifetime** (`maxLifetimeSeconds`): a hard ceiling on total lifetime, counted from creation. The deadline is reported in `expiresAt`.
-Returns the created sandbox. See [Get a sandbox](/api-reference-v2/sandboxes/get-a-sandbox) for all response fields.
+
-### Errors
+**Review question:** Are these the production defaults and maximums (idle 10 min / max 2 h, lifetime 1 h / max 24 h)? _Remove this box before merging._
-| Status | Meaning |
-| --- | --- |
-| `400` | The request is malformed, for example both or neither of `templateId` and `imageName` are set, or it includes an unknown field. |
-| `401` | The API key is missing or invalid. |
-| `402` | Your account balance is too low to start the sandbox. |
-| `403` | The API key doesn't have access to sandboxes. |
-| `422` | The request is well-formed but can't be processed, for example an unknown template or flavor. |
-| `429` | Too many requests. Retry after a delay. |
-| `503` | No capacity is available for the requested configuration. Retry later, or remove placement constraints such as `dataCenterId`. |
-
-
-
-```bash cURL
-curl --request POST \
- --url https://api.runpod.io/v2/sandboxes \
- --header "Authorization: Bearer $RUNPOD_API_KEY" \
- --header "Content-Type: application/json" \
- --data '{
- "name": "my-sandbox",
- "imageName": "python:3.12-slim",
- "idleTimeoutSeconds": 300,
- "env": {"APP_ENV": "test"},
- "labels": {"project": "checkout"}
- }'
-```
+
-```python Python
-import os
-import requests
-
-response = requests.post(
- "https://api.runpod.io/v2/sandboxes",
- headers={"Authorization": f"Bearer {os.environ['RUNPOD_API_KEY']}"},
- json={
- "name": "my-sandbox",
- "imageName": "python:3.12-slim",
- "idleTimeoutSeconds": 300,
- "env": {"APP_ENV": "test"},
- "labels": {"project": "checkout"},
- },
-)
-print(response.status_code, response.json())
-```
+| Setting | Default | Minimum | Maximum |
+| --- | --- | --- | --- |
+| Idle timeout | 10 minutes | 90 seconds | 2 hours |
+| Maximum lifetime | 1 hour | | 24 hours |
+
+If you omit a value, the default applies. Values above the maximum are clamped to it, and idle timeouts under 90 seconds are raised to 90. Check `idleTimeoutSeconds` and `maxLifetimeSeconds` in the response to see the values actually applied.
-
-
-
-
-```json 201
-{
- "id": "7h3sbx91kq",
- "name": "my-sandbox",
- "state": "CREATING",
- "templateId": null,
- "imageName": "python:3.12-slim",
- "cpuFlavorId": "cpu3c",
- "idleTimeoutSeconds": 300,
- "maxLifetimeSeconds": 3600,
- "lastActivityAt": "2026-09-29T14:02:05Z",
- "idleExpiresAt": "2026-09-29T14:07:05Z",
- "expiresAt": "2026-09-29T15:02:05Z",
- "labels": {"project": "checkout"},
- "terminationReason": null,
- "terminatedAt": null,
- "createdAt": "2026-09-29T14:02:05Z",
- "updatedAt": "2026-09-29T14:02:05Z"
-}
+You can change both while a sandbox is running:
+
+- [Extend a sandbox's lifetime](/api-reference-v2/sandboxes/extend-a-sandbox) with `POST /v2/sandboxes/{id}/extend`.
+- [Change its idle timeout](/api-reference-v2/sandboxes/update-a-sandbox) with `PATCH /v2/sandboxes/{id}`.
+
+
+
+Terminate sandboxes explicitly when you're finished, including in your error-handling paths. Don't rely on a timeout to stop billing.
+
+
+
+## Network access
+
+By default, a sandbox has **open outbound internet access**. Code inside it can reach any host on the internet, for example to install packages or call external APIs.
+
+Restricting outbound traffic is opt-in and is not yet available to all accounts. When it is, you'll be able to set `egressMode` on create to `DENY_ALL` (no outbound access) or `ALLOW_LIST` (only the hostnames in `egressAllowlist`). Until restriction is enabled for your account, any mode other than `ALLOW_ALL` returns `400`.
+
+
+
+**Review question:** Will egress restriction (`DENY_ALL`, `ALLOW_LIST`) be enforced and available to customers by Beta, or should this stay "coming soon"? _Remove this box before merging._
+
+
+
+## Exposed ports
+
+To reach a service running inside a sandbox, list its ports in `ports` when you create the sandbox (up to 15):
+
+```json
+{"ports": [{"port": 8080, "protocol": "http"}]}
```
-
+- **`http`** ports are reached through the Runpod proxy at the `url` returned in the sandbox's `ports` list. Treat the URL as opaque.
+- **`tcp`** and **`udp`** ports are reached directly at the returned `ip` and `publicPort`.
+
+## Storage
+
+Each sandbox has an ephemeral container disk, 10 GB by default (set `disk` to change it). It's deleted when the sandbox terminates.
+
+To keep data, mount an existing volume with `mounts`:
+
+- `mounts.network`: a [network volume](/storage/network-volumes), mounted at `/workspace` by default. Place the sandbox in the volume's data center.
+- `mounts.global`: a global volume, mounted at `/workspace-global` by default.
+
+
+
+**Review question:** Will the default user be able to write to network volumes in EU-RO-1 (MooseFS, AI-262) by Beta? If not, should network volumes be left out of the docs? _Remove this box before merging._
+
+
+
+## Connect over SSH
+
+Set `startSsh: true` on create to inject the SSH public keys registered on your account. Connect using the details in the sandbox's `ssh` field. For a direct connection, also expose `22/tcp` in `ports`. Only images that start `sshd` from the `PUBLIC_KEY` environment variable support this; all Runpod official images do.
+
+## Environment variables and secrets
+
+Pass environment variables in `env`. You can reference a [Runpod secret](/pods/templates/secrets) with `{{ RUNPOD_SECRET_name }}`; a create that references a secret you can't access is rejected.
+
+
+
+`env` is returned as written in API responses, so anyone who can read the sandbox can see plain values. Use secret references for credentials instead of plain values.
+
+
+
+## Compute and pricing
+
+Sandboxes run on CPU compute. Set the CPU flavor and vCPU count in `cpu`, or omit it to use the standard sandbox configuration. Memory and container disk limits scale with the flavor. The `cpu` and `compute` fields in the sandbox response show what's backing it.
+
+Sandboxes are billed per second from creation until termination, at the rate shown in `compute.costPerHr` (USD per hour). Your account needs enough balance to start a sandbox; otherwise, create returns `402`.
+
+
+
+**Review question:** Should the docs publish the per-unit rates ($0.035 per vCPU-hour, $0.0175 per GB-hour) at Beta? Will there be a launch credit? Also: which data centers offer sandboxes at Beta? _Remove this box before merging._
+
+
+
+
+
+**Review question:** Will sandbox concurrency be a separate cap or part of the shared per-plan vCPU pool (64 / 256 / 512)? What numbers and plan names should the docs show? _Remove this box before merging._
+
+
+
+## Labels
+
+Attach `labels` (string key-value pairs) when you create a sandbox to track which job, user, or environment it belongs to. You can filter `GET /v2/sandboxes` by one or more labels, written as `key=value`.
+
+## Known limitations
+
+These limitations reflect current behavior and will change as the feature matures:
+
+- **The first command can return `409`.** `RUNNING` means compute is allocated, not that the container has finished starting. Until it has, commands and log streams return `409` with code `sandbox_starting`. Retry after a short delay.
+- **Commands are capped at about 4 seconds.** A command still running at the cap is stopped and returns `exitCode: null`. For longer work, run the command with `background: true`, write its output to a file, and read the file with a later command.
+- **A sandbox can show `RUNNING` after its container exits.** If a command returns `409` with code `container_exited`, terminate the sandbox and create a new one.
+- **`/tmp` uses memory.** Writing large files to `/tmp` counts against the sandbox's memory and can stop it. Write large files elsewhere, such as your home directory or a mounted volume.
+- **Some terminal UIs don't render.** Tool compatibility follows the gVisor version Runpod runs, and some full-screen terminal apps may not display correctly.
+
+{/* TODO: Rewrite the command limitations when streaming/async exec ships. sudo is not yet available in prod; don't document it until validated. */}
+
+## Next steps
+
+- [Create your first sandbox](/sandboxes/quickstart).
+- Browse the [Sandboxes API reference](/api-reference-v2/sandboxes/create-a-sandbox).
+- Learn how to [create an API key](/get-started/credentials).
From 1ff1fd11f4d460bcc8e98909686d6a4397604555 Mon Sep 17 00:00:00 2001
From: lgunreddi
Date: Mon, 19 Oct 2026 12:13:06 -0400
Subject: [PATCH 06/14] Update docs.json
---
docs.json | 2 ++
1 file changed, 2 insertions(+)
diff --git a/docs.json b/docs.json
index d1c14b8a0..90ea068ef 100644
--- a/docs.json
+++ b/docs.json
@@ -615,6 +615,8 @@
"api-reference-v2/sandboxes/create-a-sandbox",
"api-reference-v2/sandboxes/get-a-sandbox",
"api-reference-v2/sandboxes/terminate-a-sandbox",
+ "api-reference-v2/sandboxes/update-a-sandbox",
+ "api-reference-v2/sandboxes/extend-a-sandbox",
"api-reference-v2/sandboxes/run-a-command-in-a-sandbox",
"api-reference-v2/sandboxes/stream-sandbox-logs"
]
From 631d6b83dbea83e221fff7039371cd2eac8544af Mon Sep 17 00:00:00 2001
From: lgunreddi
Date: Mon, 19 Oct 2026 12:15:54 -0400
Subject: [PATCH 07/14] Update quickstart.mdx
---
sandboxes/quickstart.mdx | 59 ++++++++++++++++++++++++----------------
1 file changed, 36 insertions(+), 23 deletions(-)
diff --git a/sandboxes/quickstart.mdx b/sandboxes/quickstart.mdx
index 6639923bd..e065365e1 100644
--- a/sandboxes/quickstart.mdx
+++ b/sandboxes/quickstart.mdx
@@ -28,7 +28,7 @@ export RUNPOD_API_KEY="YOUR_API_KEY"
## Step 1: Create a sandbox
-Create a sandbox from the `python:3.12-slim` image with a five-minute idle timeout:
+Create a sandbox from the `python:3.12-slim` image with a five-minute idle timeout. The `Idempotency-Key` header makes the request safe to retry if the response is lost:
@@ -37,9 +37,10 @@ curl --request POST \
--url https://api.runpod.io/v2/sandboxes \
--header "Authorization: Bearer $RUNPOD_API_KEY" \
--header "Content-Type: application/json" \
+ --header "Idempotency-Key: quickstart-$(date +%s)" \
--data '{
"name": "quickstart-sandbox",
- "imageName": "python:3.12-slim",
+ "image": "python:3.12-slim",
"idleTimeoutSeconds": 300,
"labels": {"project": "quickstart"}
}'
@@ -47,6 +48,7 @@ curl --request POST \
```python Python
import os
+import uuid
import requests
BASE_URL = "https://api.runpod.io/v2"
@@ -54,10 +56,10 @@ HEADERS = {"Authorization": f"Bearer {os.environ['RUNPOD_API_KEY']}"}
response = requests.post(
f"{BASE_URL}/sandboxes",
- headers=HEADERS,
+ headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())},
json={
"name": "quickstart-sandbox",
- "imageName": "python:3.12-slim",
+ "image": "python:3.12-slim",
"idleTimeoutSeconds": 300,
"labels": {"project": "quickstart"},
},
@@ -77,19 +79,20 @@ The response is the new sandbox. Note its `id`, which you use in every later req
"id": "7h3sbx91kq",
"name": "quickstart-sandbox",
"state": "CREATING",
- "imageName": "python:3.12-slim",
+ "image": "python:3.12-slim",
"idleTimeoutSeconds": 300,
"maxLifetimeSeconds": 3600,
+ "egressPolicy": {"mode": "ALLOW_ALL", "allow": []},
"labels": {"project": "quickstart"},
"...": "..."
}
```
-You must supply exactly one of `imageName` or `templateId`. Every other field is optional. See [Create a sandbox](/api-reference-v2/sandboxes/create-a-sandbox) for all fields.
+Every field is optional; an empty body (`{}`) creates a sandbox from the default image. See [Create a sandbox](/api-reference-v2/sandboxes/create-a-sandbox) for all fields.
-By default, the sandbox has open outbound internet access. To restrict it, see [Network access](/sandboxes/overview#network-access).
+By default, the sandbox has open outbound internet access. See [Network access](/sandboxes/overview#network-access).
@@ -139,48 +142,58 @@ curl --request POST \
```python Python
def run(command, retries=15):
- # The container can take 10-30 seconds to start after RUNNING,
- # so retry on 409 with a short delay.
+ # The container can take a few seconds to start after RUNNING.
+ # Retry only while the API says it's still starting.
for _ in range(retries):
response = requests.post(
f"{BASE_URL}/sandboxes/{sandbox_id}/exec",
headers=HEADERS,
json={"command": command},
)
- if response.status_code != 409:
- response.raise_for_status()
- return response.json()
- time.sleep(2)
+ if response.status_code == 409 and response.json().get("code") == "sandbox_starting":
+ time.sleep(2)
+ continue
+ response.raise_for_status()
+ return response.json()
raise TimeoutError("Sandbox did not accept commands in time")
result = run(["sh", "-c", "echo hello && python --version"])
-print(result["output"])
-if result.get("error"):
- print("Command failed:", result["error"])
+print(result["stdout"], result["exitCode"])
```
-The response contains the command's combined stdout and stderr:
+The response contains the command's output and exit status:
```json
{
"output": "hello\nPython 3.12.7\n",
- "error": null
+ "stdout": "hello\nPython 3.12.7\n",
+ "stderr": "",
+ "exitCode": 0,
+ "error": null,
+ "durationMs": 412,
+ "truncated": false
}
```
-`error` is `null` when the command succeeds. If the command exits with a non-zero status or runs past the execution cap, `error` describes the failure and `output` still contains anything the command wrote before it stopped.
+`exitCode` is `0` when the command succeeds. `output` always contains stdout and stderr combined.
-For 10 to 30 seconds after the sandbox reaches `RUNNING`, `exec` can return `409` while the container finishes starting. Retry after a short delay, as the Python example does.
+Commands are capped at about 4 seconds; a command still running at the cap is stopped and returns `exitCode: null`. For longer work, add `"background": true` to the request and redirect the command's output to a file, then read that file with a later command.
+
+
+
+
+
+**Review question:** What does the `409` response body look like? These examples assume the conflict code is in a field named `code`. Also, are `python-requests` and `curl` user agents allowed through Cloudflare in prod? _Remove this box before merging._
## Step 4: Stream logs
-Stream the sandbox's logs as Server-Sent Events. The stream includes the main process's stdout (`container`) and lifecycle events such as image pulls (`system`). Output from `exec` commands doesn't appear here.
+Stream the sandbox's logs as Server-Sent Events. The stream includes the main process's stdout (`container`) and lifecycle events such as image pulls (`system`). Output from commands you run with exec doesn't appear here. An open log stream counts as activity, so it keeps the sandbox from timing out as idle.
```bash
curl --no-buffer --request GET \
@@ -226,6 +239,6 @@ In your own code, terminate the sandbox in a `finally` block or equivalent, so i
## Next steps
-- Learn about [timeouts, network access, and pricing](/sandboxes/overview).
-- Run long commands in the background, as described in [Known limitations](/sandboxes/overview#known-limitations).
+- Learn about [timeouts, ports, storage, and network access](/sandboxes/overview).
+- [Extend a running sandbox's lifetime](/api-reference-v2/sandboxes/extend-a-sandbox).
- Browse the full [Sandboxes API reference](/api-reference-v2/sandboxes/create-a-sandbox).
From d0297d4a8cd257d8eac475bfb1757c7dea04298d Mon Sep 17 00:00:00 2001
From: lgunreddi
Date: Mon, 19 Oct 2026 12:16:38 -0400
Subject: [PATCH 08/14] Update create-a-sandbox.mdx
---
.../sandboxes/create-a-sandbox.mdx | 320 +++++++++---------
1 file changed, 168 insertions(+), 152 deletions(-)
diff --git a/api-reference-v2/sandboxes/create-a-sandbox.mdx b/api-reference-v2/sandboxes/create-a-sandbox.mdx
index 03ab391b2..5ecfac317 100644
--- a/api-reference-v2/sandboxes/create-a-sandbox.mdx
+++ b/api-reference-v2/sandboxes/create-a-sandbox.mdx
@@ -1,199 +1,215 @@
---
-title: "Sandboxes"
-sidebarTitle: "Overview"
-description: "Create short-lived, isolated CPU environments on demand to run untrusted or agent-generated code, then tear them down. Billed per second."
+title: "Create a sandbox"
+api: "POST https://api.runpod.io/v2/sandboxes"
+authMethod: "bearer"
+description: "Create an isolated, fast-starting Runpod CPU sandbox from an image, template, or the default image, with timeouts, ports, volumes, and labels."
---
-Sandboxes are short-lived, isolated CPU environments that you create on demand, run commands in, and tear down when you're done. They're built for running untrusted code: code generated by an AI agent, code submitted by your users, or one-off scripts you don't want touching your own infrastructure.
+Launches a new sandbox: an isolated, fast-starting CPU environment for running untrusted code. Supply at most one of `templateId` or `image`; with neither, the sandbox runs the default sandbox image. Every field is optional, so an empty body (`{}`) is a valid request. Omitted fields use your account's default sandbox policy.
-You manage sandboxes through the [REST API v2](/api-reference-v2/overview). Each sandbox runs a container image you choose (or a default image), starts in seconds, and is billed per second until it terminates.
+The request returns `201` with the sandbox, usually still in the `CREATING` state. Poll [Get a sandbox](/api-reference-v2/sandboxes/get-a-sandbox) until `state` is `RUNNING`.
-
-
- Create a sandbox, run a command, and terminate it.
-
+The sandbox is billed per second until it terminates. Terminate it explicitly when you're finished, including on error paths, rather than relying on a timeout to stop billing.
-
- Endpoints and fields for the `/v2/sandboxes` resource.
-
-
+### Headers
-## When to use a sandbox
+
+ Makes the create safe to retry after a lost response. Repeating the request with the same key and the same body returns the sandbox the first request created (`201`) instead of creating another. The same key with a different body is a `400`. A key whose first create failed is spent: retrying with it returns `409`, so retry a failed create with a new key. Keys are unique per account and compared case-insensitively.
+
-Use a sandbox when you need a clean, disposable environment for a short task:
+### Body
-- An AI agent needs to execute the code it writes.
-- Your application runs code or commands supplied by end users.
-- You want to test a script or package in isolation before running it elsewhere.
+
+ Container image to run, for example `python:3.12-slim`. Can't be combined with `templateId`.
+
-For long-running workloads, GPU compute, or persistent development environments, use [Pods](/pods/overview) instead. For request-driven inference that scales with traffic, use [Serverless](/serverless/overview).
+
+ Sandbox template to launch from. Can't be combined with `image`.
+
-## Isolation
+
+ ID of one of your saved container registry credentials (from `/v2/registries`), for pulling a private image. An ID that isn't yours is rejected.
+
-Every sandbox runs under [gVisor](https://gvisor.dev/), a user-space kernel that intercepts the container's system calls. Code inside a sandbox can't reach the host machine or other tenants' sandboxes, so you can run code you don't trust.
+
+ Human-readable name. Names don't have to be unique. Generated when omitted.
+
-## Images and templates
+
+ CPU flavor and vCPU count, as on a CPU Pod. Memory and container disk limits scale with the flavor. Defaults to the standard sandbox flavor and size.
-You can start a sandbox in three ways:
+
+
+ CPU flavor ID, for example `cpu3c`.
+
-- **Default image:** send an empty request body (`{}`) and the sandbox runs Runpod's default sandbox image.
-- **Container image:** set `image` to any public image, for example `python:3.12-slim`. For a private image, also set `registry` to the ID of one of your saved container registry credentials (from `/v2/registries`).
-- **Template:** set `templateId` to a sandbox template. Runpod provides curated templates, such as Ubuntu and Harbor.
-
-`image` and `templateId` can't be used together.
+
+ Number of vCPUs.
+
+
+
-**Review question:** Which templates will be available to customers at Beta, and should the docs list their IDs? _Remove this box before merging._
+**Review question:** Besides `id` and `vcpuCount`, which fields does `cpu` accept on create? Is `cpu.memory` read-only? _Remove this box before merging._
-If you create a sandbox from an image without a start command, the container runs a keepalive process so it stays up for you to run commands in. To run your own main process, set `cmd` (and optionally `entrypoint`) in exec form, for example `["python", "-m", "http.server", "8080"]`.
-
-## Lifecycle
-
-A sandbox moves through these states:
-
-| State | Meaning |
-| --- | --- |
-| `CREATING` | Compute is being allocated and the container image is being prepared. |
-| `RUNNING` | Compute is allocated. The container may still be starting (see [Known limitations](#known-limitations)). |
-| `TERMINATED` | The sandbox has stopped and its compute is released. Billing has stopped. |
-| `FAILED` | The sandbox couldn't be created. |
-
-`POST /v2/sandboxes` returns `201` with the new sandbox, usually still in the `CREATING` state. Poll `GET /v2/sandboxes/{id}` until `state` is `RUNNING` before you send commands.
-
-### Retry creates safely
+
+ Container disk in GB. Ephemeral: deleted when the sandbox terminates. Capped per vCPU, the same limit CPU Pods have.
+
-Send an `Idempotency-Key` header with each create request. If the response is lost and you retry with the same key and the same body, the API returns the sandbox the first request created instead of creating (and billing for) a second one.
+
+ Preferred data centers, each one that offers sandboxes. Omit to let Runpod choose. With a network volume mounted, omit it or include the volume's data center.
+
-If the first create failed, for example because no capacity was available, that key is spent: retrying with it returns `409`. Retry a failed create with a new key.
+
+ Environment variables as string key-value pairs. Use `{{ RUNPOD_SECRET_name }}` to reference a Runpod secret. Values are returned as written in API responses, so don't put plain credentials here.
+
-### Termination is final
+
+ Container CMD in exec form, for example `["python", "-m", "http.server", "8080"]`. If the image defines an ENTRYPOINT, this is the argument list passed to it. Omit `cmd` and `entrypoint` to keep the image's own; an image with no command runs a keepalive process.
+
-A terminated sandbox can't be restarted. Its record stays readable through `GET /v2/sandboxes/{id}`, with `terminationReason` and `terminatedAt` set, so you can see why and when it stopped. To run more work, create a new sandbox.
+
+ Container ENTRYPOINT in exec form, overriding the image's own.
+
-Terminating a sandbox that is already terminated succeeds, so it's safe to retry `DELETE /v2/sandboxes/{id}`.
+
+ Ports to expose, at most 15. Each entry has a `port` and a `protocol` (`http`, `tcp`, or `udp`). HTTP ports are reached through a proxy URL; TCP and UDP ports through a public IP and port. See [Exposed ports](/sandboxes/overview#exposed-ports).
+
-## Timeouts
-
-Every sandbox has two clocks. Whichever runs out first terminates the sandbox and stops billing:
-
-- **Idle timeout** (`idleTimeoutSeconds`): how long the sandbox can go without activity. Running a command and keeping a log stream open both count as activity. The deadline is reported in `idleExpiresAt`.
-- **Maximum lifetime** (`maxLifetimeSeconds`): a hard ceiling on total lifetime, counted from creation. The deadline is reported in `expiresAt`.
-
-
-
-**Review question:** Are these the production defaults and maximums (idle 10 min / max 2 h, lifetime 1 h / max 24 h)? _Remove this box before merging._
-
-
+
+ Volumes to mount.
-| Setting | Default | Minimum | Maximum |
-| --- | --- | --- | --- |
-| Idle timeout | 10 minutes | 90 seconds | 2 hours |
-| Maximum lifetime | 1 hour | | 24 hours |
+
+
+ Network volumes: `volumeId` (required) and `path` (defaults to `/workspace`).
+
-If you omit a value, the default applies. Values above the maximum are clamped to it, and idle timeouts under 90 seconds are raised to 90. Check `idleTimeoutSeconds` and `maxLifetimeSeconds` in the response to see the values actually applied.
+
+ Global volumes: `volumeId` (required) and `path` (defaults to `/workspace-global`).
+
+
+
-You can change both while a sandbox is running:
+
+ Inject the SSH public keys registered on your account (`PUT /v2/account/ssh-keys`) so you can connect over SSH. Does nothing if you have no registered keys. Only images that start `sshd` from `PUBLIC_KEY` support it; all Runpod official images do. For a direct connection, also expose `22/tcp` in `ports`.
+
-- [Extend a sandbox's lifetime](/api-reference-v2/sandboxes/extend-a-sandbox) with `POST /v2/sandboxes/{id}/extend`.
-- [Change its idle timeout](/api-reference-v2/sandboxes/update-a-sandbox) with `PATCH /v2/sandboxes/{id}`.
+
+ Seconds of inactivity before the sandbox is terminated. Omitted uses the default. Values above the maximum are clamped to it, and values under 90 are raised to 90. Read the response for the value applied.
+
-
+
+ Maximum total lifetime in seconds, counted from creation. Omitted or out-of-range values are clamped to the account policy's default and maximum.
+
-Terminate sandboxes explicitly when you're finished, including in your error-handling paths. Don't rely on a timeout to stop billing.
+
+ String key-value labels for organizing sandboxes. You can filter [List sandboxes](/api-reference-v2/sandboxes/list-sandboxes) by label.
+
-
+
+ Outbound network policy: `ALLOW_ALL`, `ALLOW_LIST`, or `DENY_ALL`. Egress is open by default. Restriction isn't enabled for all accounts yet; until it is, any value other than `ALLOW_ALL` returns `400`.
+
-## Network access
+
+ Hostnames the sandbox may reach when `egressMode` is `ALLOW_LIST`. Requires restricted egress to be enabled for your account.
+
-By default, a sandbox has **open outbound internet access**. Code inside it can reach any host on the internet, for example to install packages or call external APIs.
+{/* TODO: Document allowlist rules (exact lowercase hostnames, max 20, no IPs or wildcards) once host enforcement ships (AI-259). */}
-Restricting outbound traffic is opt-in and is not yet available to all accounts. When it is, you'll be able to set `egressMode` on create to `DENY_ALL` (no outbound access) or `ALLOW_LIST` (only the hostnames in `egressAllowlist`). Until restriction is enabled for your account, any mode other than `ALLOW_ALL` returns `400`.
+
-
+`imageName`, `cpuFlavorId`, `vcpuCount`, `memoryInGb`, and `dataCenterId` are deprecated aliases for `image`, `cpu.id`, `cpu.vcpuCount`, and `dataCenterIds`. Use the new names.
-**Review question:** Will egress restriction (`DENY_ALL`, `ALLOW_LIST`) be enforced and available to customers by Beta, or should this stay "coming soon"? _Remove this box before merging._
+
-
+### Response
-## Exposed ports
+Returns the created sandbox. See [Get a sandbox](/api-reference-v2/sandboxes/get-a-sandbox) for all response fields.
-To reach a service running inside a sandbox, list its ports in `ports` when you create the sandbox (up to 15):
+### Errors
-```json
-{"ports": [{"port": 8080, "protocol": "http"}]}
+| Status | Meaning |
+| --- | --- |
+| `400` | The request is malformed, for example both `templateId` and `image` are set, an unknown field is included, or the account has reached its concurrent sandbox limit. |
+| `401` | The API key is missing or invalid. |
+| `402` | Your account balance is too low to start the sandbox. |
+| `403` | The API key doesn't have access to sandboxes. |
+| `409` | The `Idempotency-Key` was spent by a create that failed. Retry with a new key. |
+| `422` | The request is well-formed but can't be processed, for example an unknown template or flavor. |
+| `429` | Too many requests. Retry after a delay. |
+| `503` | No capacity is available for the requested configuration. Retry later, or remove placement constraints such as `dataCenterIds`. |
+
+
+
+```bash cURL
+curl --request POST \
+ --url https://api.runpod.io/v2/sandboxes \
+ --header "Authorization: Bearer $RUNPOD_API_KEY" \
+ --header "Content-Type: application/json" \
+ --header "Idempotency-Key: build-4821" \
+ --data '{
+ "name": "my-sandbox",
+ "image": "python:3.12-slim",
+ "idleTimeoutSeconds": 300,
+ "ports": [{"port": 8080, "protocol": "http"}],
+ "env": {"APP_ENV": "test"},
+ "labels": {"project": "checkout"}
+ }'
```
-- **`http`** ports are reached through the Runpod proxy at the `url` returned in the sandbox's `ports` list. Treat the URL as opaque.
-- **`tcp`** and **`udp`** ports are reached directly at the returned `ip` and `publicPort`.
-
-## Storage
-
-Each sandbox has an ephemeral container disk, 10 GB by default (set `disk` to change it). It's deleted when the sandbox terminates.
-
-To keep data, mount an existing volume with `mounts`:
-
-- `mounts.network`: a [network volume](/storage/network-volumes), mounted at `/workspace` by default. Place the sandbox in the volume's data center.
-- `mounts.global`: a global volume, mounted at `/workspace-global` by default.
-
-
-
-**Review question:** Will the default user be able to write to network volumes in EU-RO-1 (MooseFS, AI-262) by Beta? If not, should network volumes be left out of the docs? _Remove this box before merging._
-
-
-
-## Connect over SSH
-
-Set `startSsh: true` on create to inject the SSH public keys registered on your account. Connect using the details in the sandbox's `ssh` field. For a direct connection, also expose `22/tcp` in `ports`. Only images that start `sshd` from the `PUBLIC_KEY` environment variable support this; all Runpod official images do.
-
-## Environment variables and secrets
-
-Pass environment variables in `env`. You can reference a [Runpod secret](/pods/templates/secrets) with `{{ RUNPOD_SECRET_name }}`; a create that references a secret you can't access is rejected.
-
-
-
-`env` is returned as written in API responses, so anyone who can read the sandbox can see plain values. Use secret references for credentials instead of plain values.
-
-
-
-## Compute and pricing
-
-Sandboxes run on CPU compute. Set the CPU flavor and vCPU count in `cpu`, or omit it to use the standard sandbox configuration. Memory and container disk limits scale with the flavor. The `cpu` and `compute` fields in the sandbox response show what's backing it.
-
-Sandboxes are billed per second from creation until termination, at the rate shown in `compute.costPerHr` (USD per hour). Your account needs enough balance to start a sandbox; otherwise, create returns `402`.
-
-
-
-**Review question:** Should the docs publish the per-unit rates ($0.035 per vCPU-hour, $0.0175 per GB-hour) at Beta? Will there be a launch credit? Also: which data centers offer sandboxes at Beta? _Remove this box before merging._
-
-
-
-
-
-**Review question:** Will sandbox concurrency be a separate cap or part of the shared per-plan vCPU pool (64 / 256 / 512)? What numbers and plan names should the docs show? _Remove this box before merging._
-
-
-
-## Labels
-
-Attach `labels` (string key-value pairs) when you create a sandbox to track which job, user, or environment it belongs to. You can filter `GET /v2/sandboxes` by one or more labels, written as `key=value`.
-
-## Known limitations
-
-These limitations reflect current behavior and will change as the feature matures:
-
-- **The first command can return `409`.** `RUNNING` means compute is allocated, not that the container has finished starting. Until it has, commands and log streams return `409` with code `sandbox_starting`. Retry after a short delay.
-- **Commands are capped at about 4 seconds.** A command still running at the cap is stopped and returns `exitCode: null`. For longer work, run the command with `background: true`, write its output to a file, and read the file with a later command.
-- **A sandbox can show `RUNNING` after its container exits.** If a command returns `409` with code `container_exited`, terminate the sandbox and create a new one.
-- **`/tmp` uses memory.** Writing large files to `/tmp` counts against the sandbox's memory and can stop it. Write large files elsewhere, such as your home directory or a mounted volume.
-- **Some terminal UIs don't render.** Tool compatibility follows the gVisor version Runpod runs, and some full-screen terminal apps may not display correctly.
-
-{/* TODO: Rewrite the command limitations when streaming/async exec ships. sudo is not yet available in prod; don't document it until validated. */}
+```python Python
+import os
+import requests
+
+response = requests.post(
+ "https://api.runpod.io/v2/sandboxes",
+ headers={
+ "Authorization": f"Bearer {os.environ['RUNPOD_API_KEY']}",
+ "Idempotency-Key": "build-4821",
+ },
+ json={
+ "name": "my-sandbox",
+ "image": "python:3.12-slim",
+ "idleTimeoutSeconds": 300,
+ "ports": [{"port": 8080, "protocol": "http"}],
+ "env": {"APP_ENV": "test"},
+ "labels": {"project": "checkout"},
+ },
+)
+print(response.status_code, response.json())
+```
-## Next steps
+
+
+
+
+```json 201
+{
+ "id": "7h3sbx91kq",
+ "name": "my-sandbox",
+ "state": "CREATING",
+ "templateId": null,
+ "image": "python:3.12-slim",
+ "registry": null,
+ "dataCenterId": null,
+ "env": {"APP_ENV": "test"},
+ "ports": [{"port": 8080, "protocol": "http"}],
+ "egressPolicy": {"mode": "ALLOW_ALL", "allow": []},
+ "idleTimeoutSeconds": 300,
+ "maxLifetimeSeconds": 3600,
+ "lastActivityAt": "2026-10-19T14:02:05Z",
+ "idleExpiresAt": "2026-10-19T14:07:05Z",
+ "expiresAt": "2026-10-19T15:02:05Z",
+ "labels": {"project": "checkout"},
+ "startedAt": null,
+ "terminationReason": null,
+ "terminatedAt": null,
+ "createdAt": "2026-10-19T14:02:05Z",
+ "updatedAt": "2026-10-19T14:02:05Z"
+}
+```
-- [Create your first sandbox](/sandboxes/quickstart).
-- Browse the [Sandboxes API reference](/api-reference-v2/sandboxes/create-a-sandbox).
-- Learn how to [create an API key](/get-started/credentials).
+
From 4fb4df899f3ccf18910312c3827a04858994e35e Mon Sep 17 00:00:00 2001
From: lgunreddi
Date: Mon, 19 Oct 2026 12:17:14 -0400
Subject: [PATCH 09/14] Update overview.mdx
---
sandboxes/overview.mdx | 129 ++++++++++++++++++++++++++++++++---------
1 file changed, 101 insertions(+), 28 deletions(-)
diff --git a/sandboxes/overview.mdx b/sandboxes/overview.mdx
index a822e0fb7..03ab391b2 100644
--- a/sandboxes/overview.mdx
+++ b/sandboxes/overview.mdx
@@ -6,7 +6,7 @@ description: "Create short-lived, isolated CPU environments on demand to run unt
Sandboxes are short-lived, isolated CPU environments that you create on demand, run commands in, and tear down when you're done. They're built for running untrusted code: code generated by an AI agent, code submitted by your users, or one-off scripts you don't want touching your own infrastructure.
-You manage sandboxes through the [REST API v2](/api-reference-v2/overview). Each sandbox runs a container image you choose, starts in seconds, and is billed per second until it terminates.
+You manage sandboxes through the [REST API v2](/api-reference-v2/overview). Each sandbox runs a container image you choose (or a default image), starts in seconds, and is billed per second until it terminates.
@@ -32,6 +32,24 @@ For long-running workloads, GPU compute, or persistent development environments,
Every sandbox runs under [gVisor](https://gvisor.dev/), a user-space kernel that intercepts the container's system calls. Code inside a sandbox can't reach the host machine or other tenants' sandboxes, so you can run code you don't trust.
+## Images and templates
+
+You can start a sandbox in three ways:
+
+- **Default image:** send an empty request body (`{}`) and the sandbox runs Runpod's default sandbox image.
+- **Container image:** set `image` to any public image, for example `python:3.12-slim`. For a private image, also set `registry` to the ID of one of your saved container registry credentials (from `/v2/registries`).
+- **Template:** set `templateId` to a sandbox template. Runpod provides curated templates, such as Ubuntu and Harbor.
+
+`image` and `templateId` can't be used together.
+
+
+
+**Review question:** Which templates will be available to customers at Beta, and should the docs list their IDs? _Remove this box before merging._
+
+
+
+If you create a sandbox from an image without a start command, the container runs a keepalive process so it stays up for you to run commands in. To run your own main process, set `cmd` (and optionally `entrypoint`) in exec form, for example `["python", "-m", "http.server", "8080"]`.
+
## Lifecycle
A sandbox moves through these states:
@@ -45,13 +63,11 @@ A sandbox moves through these states:
`POST /v2/sandboxes` returns `201` with the new sandbox, usually still in the `CREATING` state. Poll `GET /v2/sandboxes/{id}` until `state` is `RUNNING` before you send commands.
-{/* TODO(DOCS-509): The ticket says create blocks until RUNNING or FAILED; the dev spec says it returns 201 while still CREATING. Confirm with Zack and adjust this section. */}
+### Retry creates safely
-
+Send an `Idempotency-Key` header with each create request. If the response is lost and you retry with the same key and the same body, the API returns the sandbox the first request created instead of creating (and billing for) a second one.
-There is no idempotency key for sandbox creation yet. If a create request fails at the network layer, list your sandboxes (optionally filtered by a label you set) before retrying, so you don't create and pay for a duplicate.
-
-
+If the first create failed, for example because no capacity was available, that key is spent: retrying with it returns `409`. Retry a failed create with a new key.
### Termination is final
@@ -63,17 +79,26 @@ Terminating a sandbox that is already terminated succeeds, so it's safe to retry
Every sandbox has two clocks. Whichever runs out first terminates the sandbox and stops billing:
-- **Idle timeout** (`idleTimeoutSeconds`): how long the sandbox can go without activity. The deadline is reported in `idleExpiresAt` and moves forward with each activity, recorded in `lastActivityAt`.
+- **Idle timeout** (`idleTimeoutSeconds`): how long the sandbox can go without activity. Running a command and keeping a log stream open both count as activity. The deadline is reported in `idleExpiresAt`.
- **Maximum lifetime** (`maxLifetimeSeconds`): a hard ceiling on total lifetime, counted from creation. The deadline is reported in `expiresAt`.
-{/* TODO(DOCS-509): Confirm production values with Zack (AppConfig sandboxPolicyConfig). */}
+
-| Setting | Default | Maximum |
-| --- | --- | --- |
-| Idle timeout | 10 minutes | 2 hours |
-| Maximum lifetime | 1 hour | 24 hours |
+**Review question:** Are these the production defaults and maximums (idle 10 min / max 2 h, lifetime 1 h / max 24 h)? _Remove this box before merging._
-If you omit either value, the default applies. If you request a value above the maximum, it's clamped to the maximum. Check `idleTimeoutSeconds` and `maxLifetimeSeconds` in the response to see the values actually applied.
+
+
+| Setting | Default | Minimum | Maximum |
+| --- | --- | --- | --- |
+| Idle timeout | 10 minutes | 90 seconds | 2 hours |
+| Maximum lifetime | 1 hour | | 24 hours |
+
+If you omit a value, the default applies. Values above the maximum are clamped to it, and idle timeouts under 90 seconds are raised to 90. Check `idleTimeoutSeconds` and `maxLifetimeSeconds` in the response to see the values actually applied.
+
+You can change both while a sandbox is running:
+
+- [Extend a sandbox's lifetime](/api-reference-v2/sandboxes/extend-a-sandbox) with `POST /v2/sandboxes/{id}/extend`.
+- [Change its idle timeout](/api-reference-v2/sandboxes/update-a-sandbox) with `PATCH /v2/sandboxes/{id}`.
@@ -85,22 +110,72 @@ Terminate sandboxes explicitly when you're finished, including in your error-han
By default, a sandbox has **open outbound internet access**. Code inside it can reach any host on the internet, for example to install packages or call external APIs.
-Restricting outbound traffic is opt-in. Set `egressMode` when you create the sandbox:
+Restricting outbound traffic is opt-in and is not yet available to all accounts. When it is, you'll be able to set `egressMode` on create to `DENY_ALL` (no outbound access) or `ALLOW_LIST` (only the hostnames in `egressAllowlist`). Until restriction is enabled for your account, any mode other than `ALLOW_ALL` returns `400`.
+
+
+
+**Review question:** Will egress restriction (`DENY_ALL`, `ALLOW_LIST`) be enforced and available to customers by Beta, or should this stay "coming soon"? _Remove this box before merging._
+
+
+
+## Exposed ports
+
+To reach a service running inside a sandbox, list its ports in `ports` when you create the sandbox (up to 15):
+
+```json
+{"ports": [{"port": 8080, "protocol": "http"}]}
+```
-- `ALLOW_ALL`: open outbound access. This is the default.
-- `ALLOW_LIST`: only the hosts listed in `egressAllowlist` are reachable.
-- `DENY_ALL`: no outbound access.
+- **`http`** ports are reached through the Runpod proxy at the `url` returned in the sandbox's `ports` list. Treat the URL as opaque.
+- **`tcp`** and **`udp`** ports are reached directly at the returned `ip` and `publicPort`.
-{/* TODO(DOCS-509): egressMode/egressAllowlist ship in rphttp2#313 / AI-205 and are not in the dev spec yet. Verify field names and allowlist format before publishing, and add them to the create reference page. */}
+## Storage
-If you're running untrusted code that doesn't need the internet, consider setting `egressMode` to `DENY_ALL`.
+Each sandbox has an ephemeral container disk, 10 GB by default (set `disk` to change it). It's deleted when the sandbox terminates.
+
+To keep data, mount an existing volume with `mounts`:
+
+- `mounts.network`: a [network volume](/storage/network-volumes), mounted at `/workspace` by default. Place the sandbox in the volume's data center.
+- `mounts.global`: a global volume, mounted at `/workspace-global` by default.
+
+
+
+**Review question:** Will the default user be able to write to network volumes in EU-RO-1 (MooseFS, AI-262) by Beta? If not, should network volumes be left out of the docs? _Remove this box before merging._
+
+
+
+## Connect over SSH
+
+Set `startSsh: true` on create to inject the SSH public keys registered on your account. Connect using the details in the sandbox's `ssh` field. For a direct connection, also expose `22/tcp` in `ports`. Only images that start `sshd` from the `PUBLIC_KEY` environment variable support this; all Runpod official images do.
+
+## Environment variables and secrets
+
+Pass environment variables in `env`. You can reference a [Runpod secret](/pods/templates/secrets) with `{{ RUNPOD_SECRET_name }}`; a create that references a secret you can't access is rejected.
+
+
+
+`env` is returned as written in API responses, so anyone who can read the sandbox can see plain values. Use secret references for credentials instead of plain values.
+
+
## Compute and pricing
-Sandboxes run on CPU compute. You can set `cpuFlavorId`, `vcpuCount`, and `memoryInGb` when you create a sandbox, or omit them to use the standard sandbox configuration. The `compute` object in the sandbox response shows the vCPUs, memory, and container disk backing it.
+Sandboxes run on CPU compute. Set the CPU flavor and vCPU count in `cpu`, or omit it to use the standard sandbox configuration. Memory and container disk limits scale with the flavor. The `cpu` and `compute` fields in the sandbox response show what's backing it.
Sandboxes are billed per second from creation until termination, at the rate shown in `compute.costPerHr` (USD per hour). Your account needs enough balance to start a sandbox; otherwise, create returns `402`.
+
+
+**Review question:** Should the docs publish the per-unit rates ($0.035 per vCPU-hour, $0.0175 per GB-hour) at Beta? Will there be a launch credit? Also: which data centers offer sandboxes at Beta? _Remove this box before merging._
+
+
+
+
+
+**Review question:** Will sandbox concurrency be a separate cap or part of the shared per-plan vCPU pool (64 / 256 / 512)? What numbers and plan names should the docs show? _Remove this box before merging._
+
+
+
## Labels
Attach `labels` (string key-value pairs) when you create a sandbox to track which job, user, or environment it belongs to. You can filter `GET /v2/sandboxes` by one or more labels, written as `key=value`.
@@ -109,15 +184,13 @@ Attach `labels` (string key-value pairs) when you create a sandbox to track whic
These limitations reflect current behavior and will change as the feature matures:
-- **The first command can return `409`.** `RUNNING` means compute is allocated, not that the container has finished starting. For about 10 to 30 seconds after a sandbox reaches `RUNNING`, `exec` and `logs` can return `409`. Retry with a short backoff.
-- **Command output arrives when the command finishes.** `exec` returns combined stdout and stderr in a single `output` field, plus an `error` field, only after the command completes. There is no separate exit code yet.
-- **Long-running commands are cut off.** Each `exec` call has a short execution cap. For longer work, start the process in the background and poll for its result:
-
- ```json
- {"command": ["sh", "-c", "setsid nohup python job.py > /tmp/job.log 2>&1 &"]}
- ```
+- **The first command can return `409`.** `RUNNING` means compute is allocated, not that the container has finished starting. Until it has, commands and log streams return `409` with code `sandbox_starting`. Retry after a short delay.
+- **Commands are capped at about 4 seconds.** A command still running at the cap is stopped and returns `exitCode: null`. For longer work, run the command with `background: true`, write its output to a file, and read the file with a later command.
+- **A sandbox can show `RUNNING` after its container exits.** If a command returns `409` with code `container_exited`, terminate the sandbox and create a new one.
+- **`/tmp` uses memory.** Writing large files to `/tmp` counts against the sandbox's memory and can stop it. Write large files elsewhere, such as your home directory or a mounted volume.
+- **Some terminal UIs don't render.** Tool compatibility follows the gVisor version Runpod runs, and some full-screen terminal apps may not display correctly.
- Then read `/tmp/job.log` with later `exec` calls.
+{/* TODO: Rewrite the command limitations when streaming/async exec ships. sudo is not yet available in prod; don't document it until validated. */}
## Next steps
From ed0907fe53aa6e1ee9817df901ffec59b314a479 Mon Sep 17 00:00:00 2001
From: lgunreddi
Date: Mon, 19 Oct 2026 12:18:01 -0400
Subject: [PATCH 10/14] Update list-sandboxes.mdx
---
api-reference-v2/sandboxes/list-sandboxes.mdx | 21 ++++++++++++-------
1 file changed, 13 insertions(+), 8 deletions(-)
diff --git a/api-reference-v2/sandboxes/list-sandboxes.mdx b/api-reference-v2/sandboxes/list-sandboxes.mdx
index fa9297077..c3fe1b7ab 100644
--- a/api-reference-v2/sandboxes/list-sandboxes.mdx
+++ b/api-reference-v2/sandboxes/list-sandboxes.mdx
@@ -7,6 +7,8 @@ description: "List the live Runpod sandboxes owned by the authenticated user, fi
Returns the authenticated user's live sandboxes. Terminated sandboxes are excluded unless you set `state=TERMINATED`. A terminated sandbox stays readable by ID either way. When you pass several filters, a sandbox must match all of them.
+{/* TODO: Add pagination parameters when API-680 / API-682 ship. */}
+
### Query parameters
@@ -63,24 +65,27 @@ print(response.json())
"name": "quickstart-sandbox",
"state": "RUNNING",
"templateId": null,
- "imageName": "python:3.12-slim",
- "cpuFlavorId": "cpu3c",
+ "image": "python:3.12-slim",
+ "dataCenterId": "EU-RO-1",
"compute": {
"vcpuCount": 2,
"memoryInGb": 4,
"containerDiskInGb": 10,
- "costPerHr": 0.026
+ "costPerHr": 0.08
},
+ "ports": [],
+ "egressPolicy": {"mode": "ALLOW_ALL", "allow": []},
"idleTimeoutSeconds": 300,
"maxLifetimeSeconds": 3600,
- "lastActivityAt": "2026-09-29T14:02:40Z",
- "idleExpiresAt": "2026-09-29T14:07:40Z",
- "expiresAt": "2026-09-29T15:02:05Z",
+ "lastActivityAt": "2026-10-19T14:02:40Z",
+ "idleExpiresAt": "2026-10-19T14:07:40Z",
+ "expiresAt": "2026-10-19T15:02:05Z",
"labels": {"project": "quickstart"},
+ "startedAt": "2026-10-19T14:02:09Z",
"terminationReason": null,
"terminatedAt": null,
- "createdAt": "2026-09-29T14:02:05Z",
- "updatedAt": "2026-09-29T14:02:40Z"
+ "createdAt": "2026-10-19T14:02:05Z",
+ "updatedAt": "2026-10-19T14:02:40Z"
}
]
}
From b5eebd1f7cb2d72579c53e67735312e3d93ae9bd Mon Sep 17 00:00:00 2001
From: lgunreddi
Date: Mon, 19 Oct 2026 12:18:20 -0400
Subject: [PATCH 11/14] Update run-a-command-in-a-sandbox.mdx
---
.../sandboxes/run-a-command-in-a-sandbox.mdx | 67 ++++++++++++++++---
1 file changed, 57 insertions(+), 10 deletions(-)
diff --git a/api-reference-v2/sandboxes/run-a-command-in-a-sandbox.mdx b/api-reference-v2/sandboxes/run-a-command-in-a-sandbox.mdx
index a088abba7..0fcdf523a 100644
--- a/api-reference-v2/sandboxes/run-a-command-in-a-sandbox.mdx
+++ b/api-reference-v2/sandboxes/run-a-command-in-a-sandbox.mdx
@@ -2,16 +2,22 @@
title: "Run a command in a sandbox"
api: "POST https://api.runpod.io/v2/sandboxes/{id}/exec"
authMethod: "bearer"
-description: "Run a single command inside a running Runpod sandbox and get its combined stdout and stderr, plus an error field if the command fails or times out."
+description: "Run a command inside a running Runpod sandbox and get its stdout, stderr, and exit code, or start it in the background for longer work."
---
-Runs one command inside the sandbox and returns its combined stdout and stderr once it completes.
+Runs one command inside the sandbox and returns its output once it completes.
-Each command has a short execution cap. A command that runs past the cap is killed and reports an `error`. For longer work, start the process in the background with `setsid nohup ... &`, write its output to a file, and read the file with later calls. See [Known limitations](/sandboxes/overview#known-limitations).
+Commands are synchronous and capped at about 4 seconds. A command still running at the cap is stopped and returns `exitCode: null`. For longer work, set `background: true`: the command starts detached and the request returns as soon as it has started. Redirect its output to a file and read the file with a later command.
-A sandbox accepts commands only once its container has started, which can lag the `RUNNING` state by 10 to 30 seconds. Until then, this endpoint returns `409`. Retry after a short delay.
+A sandbox accepts commands only once its container has started, which can lag the `RUNNING` state by a few seconds. Until then, this endpoint returns `409` with code `sandbox_starting`. Retry after a short delay.
-{/* TODO(DOCS-509): Update when streaming exec (AI-191) and separate stdout/stderr/exit code (AI-246) ship. Confirm the exact per-command cap with Zack. */}
+Each accepted command counts as activity and resets the sandbox's idle timer.
+
+
+
+**Review question:** Will exec keep this shape (synchronous, 4-second cap, `background`) for Beta, or will the job-based Commands API (start, poll, stream, cancel) replace it before Beta? If it's replacing it, when will it be in dev? _Remove this box before merging._
+
+
### Path parameters
@@ -25,14 +31,42 @@ A sandbox accepts commands only once its container has started, which can lag th
The command to run, as an argv array with at least one element. It isn't interpreted by a shell. To use shell syntax such as pipes, redirects, or `&&`, wrap it: `["sh", "-c", "..."]`.
+
+ How long the command may run before it's stopped. The effective limit is currently 4 seconds; longer values are clamped.
+
+
+
+ Start the command detached and return as soon as it has started. `output`, `stdout`, and `stderr` are empty and `exitCode` is `0` once it has started.
+
+
### Response
- Combined stdout and stderr of the command. If the command failed or was killed, this still contains whatever it wrote before it stopped.
+ Combined stdout and stderr of the command.
+
+
+
+ The command's standard output alone. `null` when the host doesn't report the streams separately.
+
+
+
+ The command's standard error alone. `null` when the host doesn't report the streams separately.
+
+
+
+ The command's exit status, `0` on success. `null` when the command didn't exit on its own, for example when it was stopped at the time limit.
- The failure reported by the host: the command exited with a non-zero status, or ran past the per-command execution cap and was killed. `null` when the command succeeded.
+ The failure reported by the host, if the command exited non-zero or was stopped. `null` when the command succeeded.
+
+
+
+ Wall-clock time the call took, in milliseconds.
+
+
+
+ `true` when the command wrote more than 4 MiB to a stream and the excess was dropped, so the output is incomplete.
### Errors
@@ -42,7 +76,7 @@ A sandbox accepts commands only once its container has started, which can lag th
| `401` | The API key is missing or invalid. |
| `403` | The API key doesn't have access to sandboxes. |
| `404` | No sandbox with this ID exists on your account. |
-| `409` | The sandbox can't accept commands right now. Its container is still starting (retry shortly), or it has been terminated. |
+| `409` | The sandbox can't run commands right now. The response code says whether to retry: `sandbox_starting` (retry shortly), `container_exited` (create a new sandbox), or `sandbox_terminated` (the sandbox is `TERMINATED` or `FAILED`). |
| `422` | The request is well-formed but can't be processed. |
| `429` | Too many requests. Retry after a delay. |
| `503` | Command execution is temporarily unavailable. The sandbox itself is unaffected. Retry later. |
@@ -67,7 +101,15 @@ response = requests.post(
json={"command": ["sh", "-c", "echo hello && python --version"]},
)
result = response.json()
-print(result["output"])
+print(result["stdout"], result["exitCode"])
+```
+
+```bash Background
+curl --request POST \
+ --url https://api.runpod.io/v2/sandboxes/SANDBOX_ID/exec \
+ --header "Authorization: Bearer $RUNPOD_API_KEY" \
+ --header "Content-Type: application/json" \
+ --data '{"command": ["sh", "-c", "pip install -r requirements.txt > /root/install.log 2>&1"], "background": true}'
```
@@ -77,7 +119,12 @@ print(result["output"])
```json 200
{
"output": "hello\nPython 3.12.7\n",
- "error": null
+ "stdout": "hello\nPython 3.12.7\n",
+ "stderr": "",
+ "exitCode": 0,
+ "error": null,
+ "durationMs": 412,
+ "truncated": false
}
```
From 9c15a837a72ae58cb9b395347aefbc154a5ab053 Mon Sep 17 00:00:00 2001
From: lgunreddi
Date: Mon, 19 Oct 2026 12:18:43 -0400
Subject: [PATCH 12/14] Update stream-sandbox-logs.mdx
---
.../sandboxes/stream-sandbox-logs.mdx | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/api-reference-v2/sandboxes/stream-sandbox-logs.mdx b/api-reference-v2/sandboxes/stream-sandbox-logs.mdx
index e36fd167a..954ed8266 100644
--- a/api-reference-v2/sandboxes/stream-sandbox-logs.mdx
+++ b/api-reference-v2/sandboxes/stream-sandbox-logs.mdx
@@ -10,18 +10,18 @@ Streams the sandbox's logs as [Server-Sent Events](https://developer.mozilla.org
- `container`: stdout of the sandbox's main process.
- `system`: lifecycle events, such as image pulls and container starts.
-Output from [exec](/api-reference-v2/sandboxes/run-a-command-in-a-sandbox) commands doesn't appear in this stream.
+Output from [exec](/api-reference-v2/sandboxes/run-a-command-in-a-sandbox) commands doesn't appear in this stream. An open log stream counts as activity, so it keeps the sandbox from timing out as idle.
Each event has an `id` line and a JSON `data` payload:
```text
-id: 2026-09-29T14:02:11.482Z
-data: {"source":"container","line":"...","ts":"2026-09-29T14:02:11.482Z"}
+id: 2026-10-19T14:02:11.482Z
+data: {"source":"container","line":"...","ts":"2026-10-19T14:02:11.482Z"}
```
The event `id` is the event's `ts` timestamp, so an `EventSource` client that reconnects resumes where it left off by sending `Last-Event-ID`.
-Logs are available only once the sandbox has a backing container, which can lag creation by a short while. Until then, and after the sandbox terminates, this endpoint returns `409`.
+Logs are available only once the sandbox has a backing container, which can lag creation by a short while. Until then, and after the sandbox stops, this endpoint returns `409`.
### Path parameters
@@ -72,7 +72,7 @@ A `text/event-stream` response. Each event's `data` payload has these fields:
| `401` | The API key is missing or invalid. |
| `403` | The API key doesn't have access to sandboxes. |
| `404` | No sandbox with this ID exists on your account. |
-| `409` | The sandbox has no streamable logs right now. Its container is still starting (retry shortly), or it has been terminated. |
+| `409` | The sandbox has no streamable logs right now. The response code says whether to retry: `sandbox_starting` (retry shortly), `container_exited`, or `sandbox_terminated`. |
| `429` | Too many requests. Retry after a delay. |
@@ -109,11 +109,11 @@ with requests.get(
```text 200
-id: 2026-09-29T14:02:06.120Z
-data: {"source":"system","line":"Pulling image python:3.12-slim","ts":"2026-09-29T14:02:06.120Z"}
+id: 2026-10-19T14:02:06.120Z
+data: {"source":"system","line":"Pulling image python:3.12-slim","ts":"2026-10-19T14:02:06.120Z"}
-id: 2026-09-29T14:02:11.482Z
-data: {"source":"system","line":"Container started","ts":"2026-09-29T14:02:11.482Z"}
+id: 2026-10-19T14:02:11.482Z
+data: {"source":"system","line":"Container started","ts":"2026-10-19T14:02:11.482Z"}
```
From 4ecef1428b121442afc732284bb668cef46ec2fa Mon Sep 17 00:00:00 2001
From: lgunreddi
Date: Mon, 19 Oct 2026 12:20:35 -0400
Subject: [PATCH 13/14] Create extend-a-sandbox.mdx
---
.../sandboxes/extend-a-sandbox.mdx | 82 +++++++++++++++++++
1 file changed, 82 insertions(+)
create mode 100644 api-reference-v2/sandboxes/extend-a-sandbox.mdx
diff --git a/api-reference-v2/sandboxes/extend-a-sandbox.mdx b/api-reference-v2/sandboxes/extend-a-sandbox.mdx
new file mode 100644
index 000000000..500bbbb4c
--- /dev/null
+++ b/api-reference-v2/sandboxes/extend-a-sandbox.mdx
@@ -0,0 +1,82 @@
+---
+title: "Extend a sandbox's lifetime"
+api: "POST https://api.runpod.io/v2/sandboxes/{id}/extend"
+authMethod: "bearer"
+description: "Extend a running Runpod sandbox's maximum lifetime by setting a new total lifetime measured from creation, within your account's policy limit."
+---
+
+Sets a new maximum lifetime for a running sandbox. `maxLifetimeSeconds` is the new **total** lifetime measured from the sandbox's `createdAt`, the same meaning as on create, so `expiresAt` becomes `createdAt + maxLifetimeSeconds`.
+
+A request beyond your account's maximum lifetime is rejected, and the deadline is left unchanged.
+
+
+
+**Review question:** Does a value above the account's maximum return `400` or `422`? Can `extend` also shorten a sandbox's lifetime? _Remove this box before merging._
+
+
+
+### Path parameters
+
+
+ The sandbox ID.
+
+
+### Body
+
+
+ New total lifetime in seconds, measured from `createdAt`.
+
+
+### Response
+
+Returns the updated sandbox, with the new `maxLifetimeSeconds` and `expiresAt`. See [Get a sandbox](/api-reference-v2/sandboxes/get-a-sandbox) for all fields.
+
+### Errors
+
+| Status | Meaning |
+| --- | --- |
+| `400` | The request is malformed. |
+| `401` | The API key is missing or invalid. |
+| `403` | The API key doesn't have access to sandboxes. |
+| `404` | No sandbox with this ID exists on your account. |
+| `422` | The request can't be applied, for example the value exceeds your account's maximum lifetime or the sandbox is no longer running. |
+| `429` | Too many requests. Retry after a delay. |
+
+
+
+```bash cURL
+curl --request POST \
+ --url https://api.runpod.io/v2/sandboxes/SANDBOX_ID/extend \
+ --header "Authorization: Bearer $RUNPOD_API_KEY" \
+ --header "Content-Type: application/json" \
+ --data '{"maxLifetimeSeconds": 7200}'
+```
+
+```python Python
+import os
+import requests
+
+response = requests.post(
+ "https://api.runpod.io/v2/sandboxes/SANDBOX_ID/extend",
+ headers={"Authorization": f"Bearer {os.environ['RUNPOD_API_KEY']}"},
+ json={"maxLifetimeSeconds": 7200},
+)
+print(response.json()["expiresAt"])
+```
+
+
+
+
+
+```json 200
+{
+ "id": "7h3sbx91kq",
+ "state": "RUNNING",
+ "maxLifetimeSeconds": 7200,
+ "expiresAt": "2026-10-19T16:02:05Z",
+ "createdAt": "2026-10-19T14:02:05Z",
+ "...": "..."
+}
+```
+
+
From 30467c5bd02e94fe6cf012fdd92cb0bc59c0af80 Mon Sep 17 00:00:00 2001
From: lgunreddi
Date: Mon, 19 Oct 2026 12:21:06 -0400
Subject: [PATCH 14/14] Create update-a-sandbox.mdx
---
.../sandboxes/update-a-sandbox.mdx | 84 +++++++++++++++++++
1 file changed, 84 insertions(+)
create mode 100644 api-reference-v2/sandboxes/update-a-sandbox.mdx
diff --git a/api-reference-v2/sandboxes/update-a-sandbox.mdx b/api-reference-v2/sandboxes/update-a-sandbox.mdx
new file mode 100644
index 000000000..ff129fc9c
--- /dev/null
+++ b/api-reference-v2/sandboxes/update-a-sandbox.mdx
@@ -0,0 +1,84 @@
+---
+title: "Update a sandbox's timeouts"
+api: "PATCH https://api.runpod.io/v2/sandboxes/{id}"
+authMethod: "bearer"
+description: "Change a running Runpod sandbox's idle timeout. The new idle deadline is recalculated from the sandbox's last recorded activity, not from now."
+---
+
+Changes the idle timeout of a running sandbox. The new `idleExpiresAt` is calculated from the sandbox's `lastActivityAt`, not from the time of the request.
+
+Values above your account's maximum are clamped to it, and values under 90 seconds are raised to 90. Read the response for the value applied.
+
+To change the maximum lifetime, use [Extend a sandbox's lifetime](/api-reference-v2/sandboxes/extend-a-sandbox).
+
+
+
+**Review question:** Is `idleTimeoutSeconds` the only field `PATCH` accepts, or can it also change other settings? _Remove this box before merging._
+
+
+
+### Path parameters
+
+
+ The sandbox ID.
+
+
+### Body
+
+
+ New idle timeout in seconds.
+
+
+### Response
+
+Returns the updated sandbox. See [Get a sandbox](/api-reference-v2/sandboxes/get-a-sandbox) for all fields.
+
+### Errors
+
+| Status | Meaning |
+| --- | --- |
+| `400` | The request is malformed. |
+| `401` | The API key is missing or invalid. |
+| `403` | The API key doesn't have access to sandboxes. |
+| `404` | No sandbox with this ID exists on your account. |
+| `422` | The request can't be applied, for example the sandbox is no longer running. |
+| `429` | Too many requests. Retry after a delay. |
+
+
+
+```bash cURL
+curl --request PATCH \
+ --url https://api.runpod.io/v2/sandboxes/SANDBOX_ID \
+ --header "Authorization: Bearer $RUNPOD_API_KEY" \
+ --header "Content-Type: application/json" \
+ --data '{"idleTimeoutSeconds": 1800}'
+```
+
+```python Python
+import os
+import requests
+
+response = requests.patch(
+ "https://api.runpod.io/v2/sandboxes/SANDBOX_ID",
+ headers={"Authorization": f"Bearer {os.environ['RUNPOD_API_KEY']}"},
+ json={"idleTimeoutSeconds": 1800},
+)
+print(response.json()["idleExpiresAt"])
+```
+
+
+
+
+
+```json 200
+{
+ "id": "7h3sbx91kq",
+ "state": "RUNNING",
+ "idleTimeoutSeconds": 1800,
+ "lastActivityAt": "2026-10-19T14:02:40Z",
+ "idleExpiresAt": "2026-10-19T14:32:40Z",
+ "...": "..."
+}
+```
+
+