Skip to main content
Use a sandbox session when a program of yours, rather than a model, needs to run commands or code on the platform’s hardware: a cleanup script an agent wrote, a test suite against a file a user uploaded, or a build step inside an automation. A session is an isolated execution environment with a persistent /workspace directory. Which provider backs it depends on the MKA1 deployment you call, and the create response reports it as provider: runner-docker, runner-firecracker, runner-process, or in-memory. The Responses API runs its shell and code_interpreter tools on these same sessions, so you do not need this API to let a model execute code. Build an agent with memory stores shows the shell tool in use, and Sandbox sessions and the Responses API explains when to call this API yourself.

Before you start

You need: The CLI command reference documents no sandbox commands, so this guide shows the SDKs and curl.

How a session works

Kinds

Size and image

resource_class is optional, and medium is the only value the API accepts today. runtime_profile picks the image: standard (default) or eval-python, the profile the evals service uses for Python graders. Each session record reports the memory it reserved as sandbox_memory_mib.

Lifetime and states

ttl_seconds (default 600) is an idle timeout, not a total lifetime. Every command or code run renews the session’s lease, and the container is reclaimed only after ttl_seconds pass with no activity. When that happens the session becomes stopped, but the workspace survives: files persist for 7 days across container restarts, and the record’s workspace_expires_at says when they go. The next command re-provisions the container and rehydrates the workspace, which takes several seconds, so raise ttl_seconds for interactive workloads with long pauses between commands. Creating a session is idempotent on session_id. If a session with that id already exists, the request reuses it when it is running or idle, resumes it when it is stopped, and purges and recreates it when it is failed or terminated.

The session token

Create returns a session_token next to the session record. It is a credential for that one session, and it can be null on deployments that authorize on the API key and tenancy alone. When it is present, pass it back on every later call: as session_token in the body of command, code, and terminate requests, and as the session_token query parameter on workspace and browser-URL requests. Keep it on your server. Never send it to front-end code. X-On-Behalf-Of works the same way. If you set it on create, send the same header on every later call for that session: sessions are scoped to the caller that created them, and this guide’s later snippets omit the header only to keep them short.

Step 1 - Create a session

The response carries the record, the token, and the provider that backs the session. Trimmed to the fields this guide discusses:
The full record also carries the caller’s user_id, org_id, team_id, and api_key_id, plus the memory_mounts, sandbox_features, and session_key you set. Create request fields (the record echoes all of them except queue_if_full): 200 returns the session, which may still be provisioning; 202 means it is queued.

Step 2 - Wait for it to be ready

Read the record until status is running or idle.
Lifetime and states explains each status.

Step 3 - Run a command

command is the program and args its arguments. The command runs in /workspace unless you set cwd, with any extra env you pass, and is killed after timeout_seconds (default 60).
The result reports stdout, stderr, exit_code, the files_changed under the workspace, and resource_usage (reserved_memory_mib, peak_memory_mib, memory_pressure, oom_killed). A non-zero exit code is a normal 200 response, not an HTTP error, so check exit_code yourself. The Python SDK takes the path parameter as session_id_param and the body field as session_id; pass both.

Step 4 - Run code

run_code takes source text and a runtime, so you do not have to write the code to a file first, and returns the same result shape as a command.
runtime accepts these values: runtime defaults to python. cwd, env, and timeout_seconds work as they do for commands.

Step 5 - Upload and download files

Workspace paths are relative to /workspace: uploading to input.csv places the file at /workspace/input.csv, and a command reads it there. Single files travel as raw bytes with Content-Type: application/octet-stream; archives are covered below.
Uploads answer with {"status": "uploaded"}; downloads answer with the raw bytes. The Python SDK returns a download as a streamed httpx.Response, so call read() to get the bytes.

Archives

For many files at once, move a zip instead of one file at a time.

Step 6 - Inspect the workspace

The manifest lists every file in the workspace with its size and etag.

Step 7 - Terminate

Sessions are billed by elapsed time, so terminate one as soon as you are done with it rather than waiting for the idle timeout to reclaim the container.
Termination releases the backing resources and returns the record with status set to terminated. Pass expected_session_kind if you want the call to refuse a session of a different kind than the one you meant to stop.

Browser sessions

Create with session_kind: "browser" and the session runs headless Chrome instead of a command sandbox. Three operations are specific to it: Insert subpaths before the query string, as in /ports/9222/json/version?session_token=.... Standard sessions expose no public URL: browser-url answers 501 for them, so use the command, code, and workspace operations instead.
Wait for the session to reach running as in Step 2 before asking for the URL, and terminate it as in Step 7 when you are done. The C# tab stops at the URL; the proxy call takes the same session id, port, and token.

Sandbox sessions and the Responses API

When a response uses the shell or code_interpreter tool, the gateway creates a sandbox session on your behalf, runs the model’s commands in it, and returns the output to the model. It creates that session with queue_if_full: true and its own default idle timeout. The gateway names these sessions so that the same caller reuses the same sandbox across responses. The shell tool reuses the container_reference.container_id from its tool definition when there is one, and code_interpreter reuses its container string; without either, the gateway derives a key from the API key’s user and the end user. To find them, list sessions and match on session_key. The tools create these sessions as the end user in X-On-Behalf-Of when that header is present, and as the API key’s user otherwise, so a later request reuses them only when it carries the same header. A key that drives either tool needs both read:sandbox and write:sandbox: the flow polls session state and downloads files as well as running commands. Let a response create the session when the model should decide what to run. Call the sandbox API yourself when:
  • Your own code decides what runs, such as a fixed pipeline or a CI-style job.
  • You need to move files in and out without spending a model turn on it.
  • You want to control the session’s id, ttl_seconds, or runtime_profile.
  • You need a browser session.
The two can share a session. Create it here, upload its inputs, then hand the id to a response’s shell tool as environment: { type: "container_reference", container_id: "<session-id>" }; the response runs its commands in your workspace, and you download the results afterwards. The session must belong to the same caller: send the same X-On-Behalf-Of header you created it with, since sessions are scoped to the organization, team, and user that created them.

Usage

GET /api/v1/sandbox/usage aggregates sandbox activity over a time range: session starts, stops, and terminations, command and code runs, and workspace transfers. Buckets are calendar-aligned in UTC. start_time is snapped down and end_time snapped up to the nearest bucket_width boundary (1m, 1h, or 1d), so the first and last buckets can extend slightly past the range you asked for. Callers see their own usage, organization admins see their team’s, and cluster admins can pass all_orgs or org_ids.
Each result row carries num_operations, num_sessions, request_count, duration_ms, workspace_bytes_in, workspace_bytes_out, files_changed_count, oom_killed_count, peak_memory_mib, and reserved_memory_mib, plus whichever grouping fields you asked for (operation, session_kind, provider, user_id, api_key_id, org_id). The operation values are session_start, session_stop, session_terminate, turn, repo_task, command, code, workspace_manifest, workspace_download, workspace_archive_download, workspace_upload, and workspace_archive_upload. Usage counts activity; it does not price it. Spend in currency comes from the budgeting API’s cost endpoint (sdk.usage.costs, GET /api/v1/budgeting/usage/costs), which is admin-only.

For cluster admins

Cluster-admin bearer required.getPricing and setPricing answer 403 to any other caller. Spend for your own organization comes from sdk.usage.costs instead.
The budgeting service bills sandbox time as hours × (per_hour + reserved_memory_GiB × per_gib_hour) against a rate card keyed by SKU: standard, browser, and eval-python. A SKU with no configured rate accrues no spend. Shown for the TypeScript SDK and curl; the other SDKs expose the same two operations.
setPricing (PUT /api/v1/sandbox/pricing, cluster admins only) replaces the whole card: SKUs you omit become unpriced. The new rates apply from the next meter window without a restart.

Troubleshooting

API reference

For the full request and response schemas, open the Sandbox API groups (Sessions, Execution, Workspace, Sandbox Usage, Sandbox Pricing) and the Browser group in the API Reference.

See also