Skip to main content

Agent Sandboxes API

The Agent Sandboxes API lets you provision isolated gVisor sandboxes for agent workloads in Quave ONE. Sandboxes are created from versioned sandbox templates whose immutable revisions pin the image, resources, persistence mode, and lifetime limits, and Quave ONE owns the whole sandbox lifecycle: allocation, idle termination, hard-lifetime expiry, pause and resume, and termination.

Availability. Agent Sandboxes are available on both Direct and Connect plans. On Direct, sandbox usage is billed at 3× the regional zCloud rate. On Connect, sandboxes run on your own nodes and are included in the Connect license. See Billing and Agent Sandboxes pricing.

New to sandboxes? Start with the Agent Sandboxes guide and its Quickstart; this page is the field-by-field reference. Make sure to read the Get Started document to understand how the API works.

Note: These endpoints accept user tokens and OAuth tokens carrying the scopes documented per operation. MCP keys reach them only through the MCP sandbox tools. Environment tokens are not accepted, because a sandbox records the caller who requested it and an environment token identifies an environment rather than a person.

To limit an MCP client to some apps, regions and templates of one account, give its MCP key a restriction. A request outside that restriction answers 403.

Sandboxes are account-scoped. Every request must include accountId, and the caller needs access to that account: read operations require account membership, and create or terminate operations require an admin or technical member.

Sandbox template object​

A sandbox template is the named, reusable definition an agent asks for. Its current revision holds the runtime configuration.

FieldTypeDescription
templateIdStringTemplate ID.
accountIdStringOwning account ID.
appIdStringOptional app the template belongs to.
nameStringHuman-readable template name.
slugStringURL-safe identifier derived from the name.
descriptionStringOptional description.
enabledBooleanWhether new sandboxes may be requested from this template.
deprecatedAtStringISO 8601 timestamp set when the template was deprecated. Absent while active.
currentRevisionNumberIntegerRevision number of the latest revision.
createdAtStringISO 8601 creation timestamp.
updatedAtStringISO 8601 last update timestamp.

The current revision is returned by the get-template operation:

FieldTypeDescription
revisionIdStringRevision ID.
templateIdStringParent template ID.
revisionIntegerMonotonic revision number. Revisions are immutable.
imageDigestStringImmutable image digest the sandbox runs.
imageRefStringHuman-readable image reference.
commandArray of stringsOptional entrypoint command.
workingDirStringOptional working directory inside the sandbox.
portsArrayOptional declared ports.
resourcesObjectPinned sizing: zClouds (1 zCloud = 0.5 vCPU + 512 MB, allocated as both the request and the limit), plus optional ephemeralDiskMb and pidLimit.
persistenceModeStringDefault persistence mode for sandboxes of this revision: EPHEMERAL, DIRECTORY, SNAPSHOT, or BOTH. See Persistence modes. A sandbox may override it at creation.
persistentWorkspaceObjectVolume modes only. sizeMb is the size of the shared volume created for a sandbox when none is given (nominal on object storage); mountPath is where the volume data directory is mounted in DIRECTORY and BOTH, default /workspace/persist.
snapshotExcludeArray of stringsSNAPSHOT and BOTH only. Gitignore-like patterns, relative to the workspace, left out of the snapshot (for example node_modules, *.log, build/tmp).
snapshotMaxMbIntegerSNAPSHOT and BOTH only. Cap on the uncompressed size of the snapshot in MB. Default: 2048.
isolationProfileStringRuntime isolation profile. GVISOR in this version.
idleTimeoutSecondsIntegerIdle time after which Quave ONE terminates the sandbox.
hardLifetimeSecondsIntegerMaximum lifetime, after which Quave ONE terminates the sandbox.
suspendedRetentionSecondsIntegerHow long a suspended workspace is retained.
warmPoolObjectOptional warm-pool capacity with desired, min, and max. Mutually exclusive with the volume persistence modes.
createdAtStringISO 8601 creation timestamp.

Sandbox object​

FieldTypeDescription
sandboxIdStringSandbox ID.
accountIdStringOwning account ID.
appIdStringApp the sandbox belongs to.
templateIdStringTemplate the sandbox was created from.
templateRevisionIdStringExact template revision the sandbox runs.
templateRevisionNumberIntegerRevision number of that revision.
regionStringRegion the sandbox runs in.
workloadProfileStringWorkload profile the sandbox was provisioned for.
isolationProfileStringRuntime isolation profile, denormalized from the revision.
persistenceModeStringPersistence mode resolved at creation: the persistenceMode you sent, else the revision's default.
volumeNameStringShared volume the sandbox mounts or snapshots to. Present for the volume modes only.
persistenceObjectPersistence status reported by the runtime: mountPath, lastSnapshotAt, lastSnapshotBytes, and lastSnapshotError (set when the last snapshot or restore failed).
ownerKindStringTyped owner reference kind required by the workload profile.
ownerIdStringID of the owner the sandbox is attached to.
externalSessionIdStringOptional external session identifier.
callerKeyStringCaller provenance, in the form user:USER_ID.
executionWebhookObjectPresent when the sandbox has an execution completion webhook. Only host is returned, never the full URL or the secret.
statusStringCurrent lifecycle status. See Sandbox statuses below.
statusReasonStringOptional short reason for the current status.
desiredStatusStringStatus Quave ONE is driving the sandbox toward, when different from status.
generationIntegerIncrements on every status change. Useful to detect concurrent transitions.
isWarmAllocationBooleanWhether the sandbox came from a warm pool.
resourcesObjectCopy of the revision's resources (zClouds, ephemeralDiskMb, pidLimit) taken at creation. It never changes for the sandbox's life. Billing uses zClouds.
claimedAtStringISO 8601 timestamp when the sandbox was claimed.
readyAtStringISO 8601 timestamp when the sandbox became ready.
lastActiveAtStringISO 8601 timestamp of the last activity (an API call on the sandbox). Drives the idle timeout.
suspendedAtStringISO 8601 timestamp of the last suspension.
resumedAtStringISO 8601 timestamp of the last resume.
terminatedAtStringISO 8601 termination timestamp.
expiresAtStringISO 8601 hard-lifetime deadline, derived from the revision.
terminationReasonStringWhy the sandbox was terminated.
errorCodeStringStable product error code when the sandbox failed.
errorRetryableBooleanWhether errorCode describes a condition worth retrying.
correlationIdStringCorrelation ID for tracing the sandbox across systems.
createdAtStringISO 8601 creation timestamp.
updatedAtStringISO 8601 last update timestamp.

Note: idempotencyKey is write-only. You send it when creating a sandbox, and it is never returned in a sandbox object.

Persistence modes​

Every sandbox keeps its /workspace on local, ephemeral disk. A persistence mode decides what else is kept, on a shared volume of your account backed by object storage:

ModeWhat persistsCost
EPHEMERALNothing. The workspace is gone when the sandbox is terminated. Pause is not available.No volume.
DIRECTORYFiles written under the mount path (default /workspace/persist) land in the shared volume as they are closed, so they are available to the next sandbox that mounts the same volume.The objects of the shared volume, billed as shared storage.
SNAPSHOTThe whole workspace is archived to the shared volume when the sandbox is paused and restored before it serves again after a resume.One archive object per pause on the shared volume.
BOTHBoth of the above on the same volume: a live directory plus a workspace snapshot at pause.Directory objects plus one archive per pause.

The shared volume is an ordinary object storage volume of the account:

  • When you do not name one, Quave ONE creates a volume named sbvol- followed by the sandbox id on an Object Storage point the account can use in the sandbox's region: its own bucket, or the Quave ONE platform point where the region has one. Without such a point in that region the create request answers 409; check with GET /api/public/v1/storage-servers and connect a bucket with POST /api/public/v1/storage-server/object-storage/create if needed.
  • When you pass volumeName, the sandbox reuses that existing shared volume. It must belong to the account, live in the sandbox's region, and be active (404 when it does not exist, 409 when it is pending purge).
  • Terminating a sandbox never removes its volume. Remove it from the Shared Storage page or with the Shared Volumes API when it is no longer needed; removal keeps the data restorable for a purge delay.

Idle timeout and hard lifetime terminate a sandbox; they do not pause it. In DIRECTORY mode nothing is lost, because files under the mount path are already on the volume. In SNAPSHOT and BOTH modes, changes made since the last pause are lost when the sandbox times out. For a workspace that must survive any timeout, such as one volume per conversation reused by every new sandbox through volumeName, use DIRECTORY.

Sandboxes run as uid and gid 1001. A volume created for a sandbox (sbvol-…) reports that owner, so tar -x, git and package managers work on it. The volume keeps contents, not modes or times: every file reports 0664 (directories 2775), and chmod or touch -d succeed without being stored. Keep files that need a mode, such as SSH keys or executable scripts, in /workspace (carried across sandboxes by SNAPSHOT and BOTH, which archive modes); see File ownership and modes. A volume you create on the Shared Storage page or with the Shared Volumes API reports the owner of its storage point: connect your bucket with uid and gid 1001 (or update the point before creating the volume) when sandboxes will use it. The SANDBOX_AGENT_* variables are internal to the runtime agent and are not visible to your commands; use the mount path.

Memory is a hard limit: a process that goes over the template's memory is killed, and the command result carries exitCode 137 with reason killed. The sandbox keeps running.

Volume modes are not available on templates with warm-pool capacity: such a create request answers 400 with persistence modes with a volume are not available on warm pools.

Create or connect a sandbox​

Send a POST request to /api/public/v1/sandbox/create. Requires the quave:write:deploy scope.

FieldTypeRequiredDescription
accountIdStringYesAccount that owns the sandbox.
appIdStringYesApp the sandbox belongs to.
templateIdStringYes, unless templateRevisionId is sentSandbox template to provision from. It must be enabled, not deprecated, and have at least one revision.
workloadProfileStringYesCODE_INTERPRETER_TURN, QUAVE_INTERACTIVE_AGENT, or QUAVE_AUTOMATION_RUN. See Workload profiles.
ownerKindStringYesOwner reference kind. Must be the kind required by the workload profile: EXTERNAL_SESSION, AGENT_CONVERSATION, or AGENT_RUN.
ownerIdStringYesID of the owner the sandbox is attached to.
externalSessionIdStringNoOptional external session identifier.
regionStringYesRegion to run the sandbox in.
idempotencyKeyStringYesCaller-chosen key that makes this call safe to retry.
persistenceModeStringNoOverrides the template's default persistence mode: EPHEMERAL, DIRECTORY, SNAPSHOT, or BOTH. See Persistence modes.
templateRevisionIdStringNoPin an exact immutable revision of the template. Default: the latest revision. 404 when it does not belong to the template.
projectIdStringNoYour own grouping key inside the app (for example a repository or a customer project), used with maxProjectConcurrency.
maxProjectConcurrencyIntegerNoYour own cap on active sandboxes with the same projectId (or the app when projectId is omitted). Over it, create answers 429 with project_concurrency_exceeded.
supervisedBooleanNoRun the workload under the runtime supervisor, which applies the process limit separately to the supervisor and the workload.
volumeNameStringNoExisting active shared volume of the account to attach, for the volume modes (create one with POST /api/public/v1/shared-volume/create). When omitted, a volume is created for the sandbox on an Object Storage point the account can use in the region.
executionWebhookObjectNo{ "url": "https://...", "secret": "whsec_..." }. Quave ONE calls this URL when any command in the sandbox finishes. See Execution completion webhooks. A reconnect with the same idempotencyKey keeps the first configuration.
privateBooleanNoMake the sandbox private to you. See Private sandboxes and personal AI subscriptions. Requires the EPHEMERAL persistence mode (400 otherwise).

The region is validated at create time: an unknown region answers 400, a region this account cannot use answers 403, and a region without the Agent Sandbox add-on answers 409 — nothing is created in any of those cases. Accounts without the sandboxes entitlement answer 403 on every sandbox endpoint.

This operation is idempotent. The same account, app, caller, and idempotencyKey always resolve to the same sandbox:

  • The first call creates the sandbox and answers 201 with wasCreated: true.
  • Any repeated call connects to the existing sandbox and answers 200 with wasCreated: false.

Retrying a create request after a network timeout is therefore safe and never provisions a second sandbox. The database uniqueness constraint is established before allocation; if that prerequisite fails, the request fails without allocating another sandbox.

curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"accountId": "ACCOUNT_ID",
"appId": "APP_ID",
"templateId": "TEMPLATE_ID",
"workloadProfile": "QUAVE_INTERACTIVE_AGENT",
"ownerKind": "AGENT_CONVERSATION",
"ownerId": "CONVERSATION_ID",
"region": "us-5",
"idempotencyKey": "conversation-42-turn-7"
}' \
https://api.quave.cloud/api/public/v1/sandbox/create

Example response:

{
"success": true,
"wasCreated": true,
"sandbox": {
"sandboxId": "SANDBOX_ID",
"accountId": "ACCOUNT_ID",
"appId": "APP_ID",
"templateId": "TEMPLATE_ID",
"templateRevisionId": "TEMPLATE_REVISION_ID",
"templateRevisionNumber": 3,
"region": "us-5",
"workloadProfile": "QUAVE_INTERACTIVE_AGENT",
"isolationProfile": "GVISOR",
"persistenceMode": "EPHEMERAL",
"ownerKind": "AGENT_CONVERSATION",
"ownerId": "CONVERSATION_ID",
"callerKey": "user:USER_ID",
"status": "REQUESTED",
"generation": 0,
"expiresAt": "2026-08-07T13:00:00.000Z",
"createdAt": "2026-08-07T12:00:00.000Z"
}
}

List sandboxes​

Send a GET request to /api/public/v1/sandboxes. Requires the quave:read scope.

Query parameterTypeDescription
accountIdStringAccount ID. Required.
appIdStringOptional app filter.
statusStringOptional status filter. Pass one status or comma-separated statuses, for example READY,BUSY. Unknown statuses are ignored.
limitIntegerSandboxes per page. Default: 50. Maximum: 100.
pageIntegerOne-based page number. Default: 1.

The response includes accountId, page, limit, totalCount, and sandboxes, sorted by creation time, newest first.

curl -X GET \
-H 'Authorization: YOUR_TOKEN' \
'https://api.quave.cloud/api/public/v1/sandboxes?accountId=ACCOUNT_ID&status=READY,BUSY&page=1&limit=20'

Get a sandbox​

Send a GET request to /api/public/v1/sandbox. Requires the quave:read scope.

Query parameterTypeDescription
accountIdStringAccount ID. Required.
sandboxIdStringSandbox ID. Required.
waitSecondsInteger0–25. While the sandbox is still provisioning (REQUESTED, ALLOCATING, STARTING or RESUMING), wait up to this long and answer as soon as it is READY, BUSY or FAILED. Default: 0.
curl -X GET \
-H 'Authorization: YOUR_TOKEN' \
'https://api.quave.cloud/api/public/v1/sandbox?accountId=ACCOUNT_ID&sandboxId=SANDBOX_ID&waitSeconds=25'

Reading a sandbox that is still provisioning refreshes its status from the runtime first, so a poll never shows a stale state. To wait for a new sandbox, repeat the call with waitSeconds=25 until the status is no longer a provisioning one. Each call answers as soon as the sandbox settles, so the loop needs no sleep of its own.

Terminate a sandbox​

Send a POST request to /api/public/v1/sandbox/terminate. Requires the quave:write:deploy scope.

FieldTypeRequiredDescription
accountIdStringYesAccount ID.
sandboxIdStringYesSandbox ID.

Termination is idempotent. Terminating a sandbox that is already terminated succeeds and returns the sandbox unchanged, so retries are safe. Termination never removes the sandbox's shared volume; see Persistence modes. A sandbox that has not reached a running state yet is first marked as failed and then terminated, so no sandbox is ever left holding compute.

If the sandbox status changes concurrently while the request is being processed, Quave ONE answers 409 with Sandbox is changing state. Retry the call.

curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"accountId": "ACCOUNT_ID",
"sandboxId": "SANDBOX_ID"
}' \
https://api.quave.cloud/api/public/v1/sandbox/terminate

Example response:

{
"success": true,
"sandbox": {
"sandboxId": "SANDBOX_ID",
"status": "TERMINATED",
"terminationReason": "USER_REQUESTED",
"terminatedAt": "2026-08-07T12:30:00.000Z"
}
}

Pause a sandbox​

Send a POST request to /api/public/v1/sandbox/pause. Requires the quave:write:deploy scope.

Pausing releases the sandbox's compute while keeping its shared volume. In the SNAPSHOT and BOTH modes the workspace is archived to the volume first, and the request answers when the archive is written. A large workspace can take minutes. Only a READY or BUSY sandbox with a volume persistence mode can be paused; work in flight on a BUSY sandbox is lost.

FieldTypeRequiredDescription
accountIdStringYesAccount ID.
sandboxIdStringYesSandbox ID.
snapshotExcludeArray of stringsNoOverrides the revision's snapshot exclude patterns for this pause only.
snapshotMaxMbIntegerNoOverrides the snapshot size cap for this pause only.
forceBooleanNoSnapshot even when the restore at start failed. Without it such a pause is refused so the last good snapshot is not overwritten by a near-empty workspace.

On success the sandbox becomes SUSPENDED (paused) and, when the mode snapshots, persistence.lastSnapshotAt and persistence.lastSnapshotBytes describe the archive that was written. Pausing an already paused sandbox succeeds and returns it unchanged.

Failures:

  • 400 for an EPHEMERAL sandbox: there is nothing to keep; terminate it instead.
  • 409 when the sandbox is not READY or BUSY.
  • 502 when the snapshot failed, 503 when the sandbox has no reachable pod yet, and 504 when the snapshot did not finish in time. In all three cases the sandbox is left running, its status goes back to what it was, and the response carries data.retryable: true, plus data.retryAfterSeconds on 503. Retry the pause after that delay. If a 504 repeats, exclude large paths with snapshotExclude.
curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"accountId": "ACCOUNT_ID",
"sandboxId": "SANDBOX_ID",
"snapshotExclude": ["node_modules", "*.log"]
}' \
https://api.quave.cloud/api/public/v1/sandbox/pause

Example response:

{
"success": true,
"sandbox": {
"sandboxId": "SANDBOX_ID",
"status": "SUSPENDED",
"persistenceMode": "SNAPSHOT",
"volumeName": "sbvol-sandbox_id",
"persistence": {
"lastSnapshotAt": "2026-09-09T12:30:00.000Z",
"lastSnapshotBytes": 5242880
},
"suspendedAt": "2026-09-09T12:30:00.000Z"
}
}

Resume a sandbox​

Send a POST request to /api/public/v1/sandbox/resume. Requires the quave:write:deploy scope.

FieldTypeRequiredDescription
accountIdStringYesAccount ID.
sandboxIdStringYesSandbox ID.

A SUSPENDED sandbox moves to RESUMING, is deployed again with the same sandbox id on its shared volume, and, in the SNAPSHOT and BOTH modes, restores its latest snapshot before it serves. Call Get a sandbox with waitSeconds until the status is READY. Resuming a sandbox that is already RESUMING, READY, or BUSY returns it unchanged; any other status answers 409. Concurrent resume requests start a single resume.

You rarely need this call: executing a command or connecting with the same idempotency key resumes a paused sandbox automatically.

curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"accountId": "ACCOUNT_ID",
"sandboxId": "SANDBOX_ID"
}' \
https://api.quave.cloud/api/public/v1/sandbox/resume

List sandbox templates​

Send a GET request to /api/public/v1/sandbox-templates. Requires the quave:read scope.

Query parameterTypeDescription
accountIdStringAccount ID. Required.
appIdStringOptional app filter.
includeDeprecatedBooleanInclude deprecated templates. Default: false.
curl -X GET \
-H 'Authorization: YOUR_TOKEN' \
'https://api.quave.cloud/api/public/v1/sandbox-templates?accountId=ACCOUNT_ID'

Get a sandbox template​

Send a GET request to /api/public/v1/sandbox-template. Requires the quave:read scope.

Query parameterTypeDescription
accountIdStringAccount ID. Required.
templateIdStringTemplate ID. Required.

The response contains template and currentRevision. currentRevision is null when the template has no revision yet, and a template without a revision cannot serve sandboxes.

curl -X GET \
-H 'Authorization: YOUR_TOKEN' \
'https://api.quave.cloud/api/public/v1/sandbox-template?accountId=ACCOUNT_ID&templateId=TEMPLATE_ID'

Save a sandbox template​

Send a POST request to /api/public/v1/sandbox-template/save. Requires the quave:write:config scope.

FieldTypeRequiredDescription
accountIdStringYesAccount that owns the template.
templateIdStringNoTemplate to update. Omit to create a new template.
appIdStringNoOptional app the template belongs to (create only).
nameStringYesHuman-readable template name.
descriptionStringNoOptional description.
enabledBooleanNoWhether new sandboxes may be requested from this template. Default: true.

Creating answers 201; updating answers 200. Runtime configuration (image, command, resources, lifetimes) is never edited here — it lives in immutable revisions.

curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"accountId": "ACCOUNT_ID",
"name": "python-repl"
}' \
https://api.quave.cloud/api/public/v1/sandbox-template/save

Create a sandbox template revision​

Send a POST request to /api/public/v1/sandbox-template/revision. Requires the quave:write:config scope.

Revisions are immutable and append-only: new sandboxes pin the latest revision, and already-allocated sandboxes keep the revision they started from.

networkControl optionally restricts egress. Its triggers are independent:

  • CIDR/IP only: {"enabled":true,"addressAllowlist":["203.0.113.8"]}. Enabling address restrictions requires at least one non-blank address.
  • Domain only: {"enabled":false,"domainAllowlist":["github.com"]}. The non-empty domain list activates the egress proxy even though address restrictions are disabled.
  • Combined: set enabled:true and provide both lists. Domains go through the proxy; address exceptions are enforced by network policy. This is not a domain/IP intersection.

Blank-only domain lists are rejected when creating a revision. IP/domain syntax, reserved infrastructure ranges, and runtime enforcement are additionally validated by the infrastructure service at sandbox allocation. Infrastructure ranges remain denied. Omit the whole block for default egress; an empty domain list does not activate domain restrictions. Existing immutable revisions are not rewritten by these checks.

FieldTypeRequiredDescription
accountIdStringYesAccount ID.
templateIdStringYesTemplate to append the revision to.
imageDigestStringYesDigest-pinned image (repo/image@sha256:<64 hex chars>).
imageRefStringNoHuman-readable image reference (informational).
commandArray of stringsNoEntrypoint argv — never a shell string. Without it the image entrypoint must be long-running, or the sandbox exits immediately and never becomes READY.
workingDirStringNoWorking directory inside the sandbox.
zCloudsIntegerNoSandbox size in zClouds. Default: 1.
persistenceModeStringNoDefault persistence mode: EPHEMERAL (default), DIRECTORY, SNAPSHOT, or BOTH.
volumeSizeMbIntegerNoSize in MB of the shared volume created for a sandbox when none is given (nominal on object storage). Volume modes only.
mountPathStringNoWhere the volume data directory is mounted in DIRECTORY and BOTH. Default: /workspace/persist.
snapshotExcludeArray of stringsNoSNAPSHOT and BOTH only. Gitignore-like patterns left out of the snapshot.
snapshotMaxMbIntegerNoSNAPSHOT and BOTH only. Cap on the uncompressed snapshot size in MB. Default: 2048.
idleTimeoutSecondsIntegerNoIdle time after which the sandbox is terminated.
hardLifetimeSecondsIntegerNoMaximum lifetime, after which the sandbox is terminated.
curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"accountId": "ACCOUNT_ID",
"templateId": "TEMPLATE_ID",
"imageDigest": "docker.io/library/debian:bookworm-slim@sha256:...",
"imageRef": "debian:bookworm-slim",
"command": ["sleep", "infinity"],
"zClouds": 1
}' \
https://api.quave.cloud/api/public/v1/sandbox-template/revision

Execute a command in a sandbox​

Send a POST request to /api/public/v1/sandbox/execute. Requires the quave:write:dangerous scope.

Runs a command to completion inside the sandbox through the region gateway and returns its exit code and captured output. The sandbox must be READY or BUSY and carry the sandbox runtime agent; a sandbox deployed without the agent answers 409. Output is returned to you and never stored by Quave ONE.

A paused (SUSPENDED) sandbox is resumed by the request itself. When the resume finishes within the request, the command runs and you get its result. Otherwise, and while the sandbox is RESUMING, SUSPENDING or still starting (REQUESTED, ALLOCATING, STARTING), the answer is 409 with data.retryable: true, data.retryAfterSeconds, and a Retry-After header; retry after that delay. Concurrent requests on one paused sandbox start a single resume. Right after a resume the sandbox can be READY while the runtime agent is still restoring a SNAPSHOT workspace; until it answers, commands fail with AGENT_UNREACHABLE (data.retryable: true, data.retryAfterSeconds). Retry it the same way.

FieldTypeRequiredDescription
accountIdStringYesAccount ID.
sandboxIdStringYesSandbox ID.
commandArray of stringsYesCommand argv — never a shell string. For shell features pass ["sh", "-c", "..."].
cwdStringNoWorking directory for the command.
envVarsObjectNoExtra environment variables for the command.
timeoutSecondsIntegerNoCommand deadline in seconds. Default: 120. Max: 900.
curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"accountId": "ACCOUNT_ID",
"sandboxId": "SANDBOX_ID",
"command": ["sh", "-c", "python3 -V && uname -a"]
}' \
https://api.quave.cloud/api/public/v1/sandbox/execute

Example response:

{
"success": true,
"result": {
"exitCode": 0,
"stdout": "UHl0aG9uIDMuMTIuMTUK"
}
}
FieldTypeDescription
result.exitCodeIntegerExit code of the process. 137 when it was killed for exceeding memory, usually 143 when it hit the deadline.
result.stdoutStringCaptured standard output, base64-encoded. Omitted when empty.
result.stderrStringCaptured standard error, base64-encoded. Omitted when empty.
result.stdoutTruncated, result.stderrTruncatedBooleantrue when the output was too large and was cut.
result.reasonStringPresent when the process did not exit on its own: deadline_exceeded, canceled, killed (out of memory or another unrequested kill), or agent_shutdown (the sandbox was paused or terminated during the command).

Executing a command counts as sandbox activity for the idle timeout. For work longer than a few minutes, start the command instead. The audit trail records that a command ran (and its exit code) but never the command line, environment variables, or output.

Start a command​

Send a POST request to /api/public/v1/sandbox/command/start. Requires the quave:write:dangerous scope and permission to manage the sandbox.

Starts a command and answers as soon as the process runs. The command keeps running after the response, with no 15-minute ceiling: it ends when it exits, at its optional deadline, or with the sandbox. A paused sandbox is resumed first, with the same 409 + retryAfterSeconds contract as execute. While a started command runs, the sandbox is never terminated for idleness; the maximum lifetime still applies.

FieldTypeRequiredDescription
accountIdStringYesAccount ID.
sandboxIdStringYesSandbox ID.
commandArray of stringsYesCommand argv — never a shell string. For shell features pass ["sh", "-c", "..."].
cwdStringNoWorking directory for the command.
envVarsObjectNoExtra environment variables for the command. Never stored.
aiSubscriptionIdStringNoRun with one of your personal AI subscriptions. Only in a private sandbox you created.
timeoutSecondsIntegerNoOptional deadline in seconds, up to 604800 (7 days). Without it, only the sandbox lifetime bounds the command.
idempotencyKeyStringNoUp to 200 characters. Reuse it when you retry: the same key returns the same execution instead of starting the command twice.
curl -X POST \
-H 'Authorization: YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"accountId": "ACCOUNT_ID",
"sandboxId": "SANDBOX_ID",
"command": ["sh", "-c", "cd /workspace/persist/repo && npm test"],
"idempotencyKey": "test-run-42"
}' \
https://api.quave.cloud/api/public/v1/sandbox/command/start

Example response (201):

{
"success": true,
"execution": {
"executionId": "EXECUTION_ID",
"processId": 17,
"startedAt": "2026-10-11T12:00:00Z"
}
}

Get a command​

Send a GET request to /api/public/v1/sandbox/command. Requires the quave:read scope.

Returns the state of a started command and its output since from. Call it again with the returned nextSeq to continue: every event is delivered once, with no gaps, while the command's output buffer still holds it.

ParameterTypeRequiredDescription
accountIdStringYesAccount ID.
sandboxIdStringYesSandbox ID.
executionIdStringYesThe executionId from start.
fromIntegerNoFirst event sequence to return. Default: 0 (everything still buffered).
waitSecondsIntegerNo0–25. While the command runs, wait up to this long for new output before answering. Default: 0.
curl -H 'Authorization: YOUR_TOKEN' \
'https://api.quave.cloud/api/public/v1/sandbox/command?accountId=ACCOUNT_ID&sandboxId=SANDBOX_ID&executionId=EXECUTION_ID&from=0&waitSeconds=20'

Example response:

{
"command": {
"executionId": "EXECUTION_ID",
"state": "exited",
"exitCode": 0,
"startedAt": "2026-10-11T12:00:00Z",
"exitedAt": "2026-10-11T12:41:07Z",
"stdoutBytes": 18234,
"stderrBytes": 0,
"outputTruncated": false,
"events": [
{ "seq": 311, "stdout": "VGVzdHM6IDQ4MiBwYXNzZWQK" },
{ "seq": 312, "exit": { "exitCode": 0 } }
],
"nextSeq": 313,
"complete": true
}
}
FieldTypeDescription
stateStringrunning or exited.
exitCodeIntegerPresent once the command exited.
reasonStringPresent when the command did not exit on its own: deadline_exceeded, canceled, killed (out of memory or another unrequested kill), or agent_shutdown (the sandbox was paused or terminated).
stdoutBytes, stderrBytesIntegerTotal bytes the command wrote so far.
outputTruncatedBooleantrue when the command's output buffer (the most recent 4 MiB) dropped older output.
eventsArrayOutput since from. Each event has a seq and one of stdout or stderr (base64), exit (exitCode, reason), truncated (droppedBytes, droppedEvents: output you will not get), or init (processId). One answer carries at most about 1 MiB; call again with nextSeq for the rest.
nextSeqIntegerThe from to send next.
completeBooleantrue when every event, including exit, has been delivered.

The output is passed through to you and never stored by Quave ONE.

Kill a command​

Send a POST request to /api/public/v1/sandbox/command/kill. Requires the quave:write:dangerous scope and permission to manage the sandbox.

FieldTypeRequiredDescription
accountIdStringYesAccount ID.
sandboxIdStringYesSandbox ID.
executionIdStringYesThe executionId from start.
signalStringNoSIGTERM (default), SIGINT, or SIGKILL. Sent to the command and its child processes.

The answer is { "success": true, "executionId": "...", "signal": "SIGTERM" }. Read the final state with Get a command. Signaling a command that already exited answers 409.

Execution completion webhooks​

Instead of polling Get a command until it exits, you can have Quave ONE call your server when a command finishes. Set executionWebhook when you create the sandbox. Every asynchronous command in that sandbox then triggers one webhook when it finishes. This covers started commands (REST or MCP) and commands and terminals opened through a gateway session. Execute already returns the result in its response, so it does not trigger one.

FieldRequiredDescription
executionWebhook.urlYesPublic https URL on the default port (443). URLs with credentials, a #fragment, an IP address, or an internal hostname (localhost, *.local, *.internal, *.svc) are rejected with 400. So is a host that resolves to a private, loopback, link-local (cloud metadata) or carrier-grade NAT address.
executionWebhook.secretYesSigning secret in the Standard Webhooks format: whsec_ followed by the base64 of 24 to 64 random bytes. Generate one with echo "whsec_$(openssl rand -base64 32)". It is stored encrypted and never returned.

Quave ONE sends a POST with a JSON body:

{
"id": "evt_EXECUTION_RECORD_ID",
"type": "sandbox.execution.completed",
"timestamp": "2026-10-11T12:41:07.000Z",
"data": {
"accountId": "ACCOUNT_ID",
"appId": "APP_ID",
"sandboxId": "SANDBOX_ID",
"executionId": "EXECUTION_ID",
"executionType": "EXEC",
"status": "SUCCEEDED",
"exitCode": 0,
"startedAt": "2026-10-11T12:00:00.000Z",
"finishedAt": "2026-10-11T12:41:07.000Z",
"stdoutBytes": 18234,
"stderrBytes": 0,
"outputTruncated": false
}
}
FieldDescription
data.executionIdThe executionId returned by Start a command. Pass it to Get a command to read the output, which the webhook never carries.
data.executionTypeEXEC for a command, PTY for a terminal.
data.statusSUCCEEDED (exit code 0), FAILED (non-zero exit code), TIMED_OUT (the command's deadline passed), CANCELED, or UNKNOWN (the sandbox ended before Quave ONE could observe the outcome).
data.exitCode, data.reasonAs in Get a command. Present when known.

The body never contains the command line, environment variables or output.

Verify the signature​

Each request carries the Standard Webhooks headers webhook-id, webhook-timestamp (Unix seconds) and webhook-signature (v1, followed by the base64 HMAC-SHA256 of webhook-id.webhook-timestamp.body, keyed with the base64-decoded part of your secret). Any Standard Webhooks library verifies it as is. Without a library, in Node.js:

import { createHmac, timingSafeEqual } from 'node:crypto';

// `rawBody` is the exact request body string, before JSON parsing.
const isValidQuaveWebhook = ({ secret, headers, rawBody }) => {
const id = headers['webhook-id'];
const timestamp = Number(headers['webhook-timestamp']);
// Reject old or replayed deliveries.
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
const expected = Buffer.from(
createHmac('sha256', key)
.update(`${id}.${timestamp}.${rawBody}`)
.digest('base64')
);
return headers['webhook-signature']
.split(' ')
.map((signature) => Buffer.from(signature.replace(/^v1,/, '')))
.some(
(received) =>
received.length === expected.length && timingSafeEqual(received, expected)
);
};

Delivery​

  • The first attempt is sent as soon as Quave ONE records the end of the command. A 2xx answer confirms it.
  • 429, 5xx, timeouts (10 seconds) and connection errors are retried with exponential backoff, starting at 30 seconds and capped at one hour, for up to 8 attempts.
  • Other answers, including redirects (they are never followed), stop delivery.
  • Delivery is at least once: deduplicate on webhook-id.
  • Every attempt resolves your host again and refuses private and internal addresses, so a DNS change cannot point a delivery at an internal network.
  • A failed delivery never affects the command or the sandbox. Get a command keeps working while the command's output buffer has the output.

Private sandboxes and personal AI subscriptions​

A personal AI subscription is a credential that belongs to you, not to an account: for example the Claude Code token from claude setup-token for a Claude Pro or Max plan. Any client acting as you can run with it: your user token, an OAuth app you authorized, or one of your MCP keys through MCP. Nobody else can, account admins and Quave ONE operators included.

  1. Store it once. The value is encrypted and never returned.

    curl -X POST \
    -H 'Authorization: YOUR_TOKEN' \
    -H 'Content-Type: application/json' \
    -d '{
    "name": "Claude Max",
    "envVarName": "CLAUDE_CODE_OAUTH_TOKEN",
    "value": "TOKEN_FROM_CLAUDE_SETUP_TOKEN"
    }' \
    https://api.quave.cloud/api/public/v1/ai-subscription
  2. Create a private sandbox ("private": true on create, with the EPHEMERAL persistence mode).

  3. Pass aiSubscriptionId to execute or start a command. The value is added to that command's environment under envVarName, and never stored with the command.

In a private sandbox, only the user who created it can run commands, open gateway sessions, read command output, or stop commands. Other members get 403, even account admins. They can still list it, read its metadata, and terminate it. A private sandbox keeps nothing on an account volume. A subscription that is not yours answers 404. A subscription in a sandbox that is not private to you answers 403.

Use one subscription per CLI. For a CLI that reads a file instead of a variable, store the file content and write it from the command, for example ["sh", "-c", "mkdir -p ~/.codex && printf %s \"$CODEX_AUTH_JSON\" > ~/.codex/auth.json && codex exec ..."]. Each provider's terms decide how its subscription may be used.

OperationMethod and pathScope
List your subscriptions (no values)GET /api/public/v1/ai-subscriptionsquave:read
CreatePOST /api/public/v1/ai-subscription with name, envVarName, valuequave:write:config
Rename or rotatePATCH /api/public/v1/ai-subscription with aiSubscriptionId and any of name, envVarName, valuequave:write:config
DeleteDELETE /api/public/v1/ai-subscription?aiSubscriptionId=...quave:write:dangerous

MCP: list-ai-subscriptions, create-ai-subscription, update-ai-subscription, delete-ai-subscription, plus private on create-sandbox and aiSubscriptionId on execute-sandbox-command and start-sandbox-command.

On Connect, sandboxes run on your own nodes. Whoever administers those nodes can read the memory of their processes, so the guarantee covers what Quave ONE exposes.

Open a gateway session​

Send a POST request to /api/public/v1/sandbox/session. Requires the quave:write:dangerous scope and permission to manage the sandbox.

FieldTypeRequiredDescription
accountIdStringYesAccount ID.
sandboxIdStringYesSandbox ID.
scopesArray of stringsYesOne or more of exec, file, pty, signal, status.

The response carries endpoint (the region's sandbox gateway), a short-lived token, and expiresAt. Send the token to the gateway as Authorization: Bearer <token> to start and stream long-running processes, send input and signals, open terminals, and read or write workspace files. A paused sandbox is resumed first (409 with data.retryable: true while that is in progress). See Processes, files and terminals for the gateway API.

Sandbox statuses​

StatusDescription
REQUESTEDThe sandbox was requested and is waiting to be allocated.
ALLOCATINGQuave ONE is allocating capacity for the sandbox.
STARTINGThe sandbox is starting.
READYThe sandbox is running and idle. It can accept work.
BUSYThe sandbox is running work.
SUSPENDINGPausing: the workspace is being snapshotted (when the mode snapshots) and the compute released. If the runtime cannot pause, the sandbox goes back to READY or BUSY.
SUSPENDEDPaused. Compute was released and the shared volume is kept. Resume it with Resume a sandbox.
RESUMINGThe sandbox is being deployed again with the same id; its snapshot is restored before it serves.
FAILEDThe sandbox failed. See errorCode and errorRetryable.
TERMINATINGTermination is in progress.
TERMINATEDTerminal status. The sandbox no longer exists and cannot be resumed.

Quave ONE drives these transitions itself. Sandboxes idle for longer than their template's idleTimeoutSeconds are terminated, and so are sandboxes that reach expiresAt, without any call from you. Pause is an explicit call. Resume is explicit too, and also happens automatically when you execute a command on a paused sandbox or connect to it again with the same idempotency key. A paused sandbox does not count toward the account's active-sandbox quota.

Billing​

On Direct, a sandbox is billed for its allocated resources.zClouds while it holds its allocation, at 3× your regional zCloud rate. Usage is billed by the second, with no minimum. There is no fixed sandbox fee. Savings Plans do not apply to sandbox usage, but account credits do. On Connect, sandbox usage is included in the license and is never charged.

StatusBilled
REQUESTED, ALLOCATINGNo. The sandbox has no reserved compute yet.
STARTING, READY, BUSYYes.
SUSPENDING, RESUMINGYes. The compute stays reserved until the pause or resume completes.
SUSPENDEDNo. Compute is released. A paused workspace's shared volume follows the shared storage rules.
FAILED, TERMINATING, TERMINATEDNo.

Each pause and resume starts a new billed interval, so pausing an idle sandbox stops its compute charge right away.

Retryable errors​

Sandbox failures surface as stable product error codes rather than raw runtime messages, so you can branch on them safely. The sandbox object carries errorCode together with errorRetryable, which tells you whether waiting and retrying can succeed.

Error codeRetryableDescription
SANDBOX_SUSPENDEDYesThe sandbox is suspended and must be resumed first.
SANDBOX_NOT_READYYesThe sandbox is not ready to accept work yet.
AGENT_UNREACHABLEYesThe sandbox runtime is temporarily unreachable, for example while it restores a workspace snapshot right after a resume.
SANDBOX_EXPIREDNoThe sandbox reached its hard lifetime and will not start again. Create a new sandbox; reuse its shared volume through volumeName to keep the data.
SANDBOX_NOT_FOUNDNoThe sandbox does not exist in the runtime.
NAMESPACE_DENIEDNoThe sandbox runtime rejected the namespace.
UNAUTHENTICATEDNoThe runtime credential was not accepted.
TOKEN_EXPIREDNoThe runtime credential expired.
INSUFFICIENT_SCOPENoThe runtime credential lacked the required scope.
PATH_ESCAPENoA file path outside the sandbox workspace was rejected.
EXECUTION_NOT_FOUNDNoThe command's executionId is unknown to the sandbox (wrong id, or the sandbox restarted).
EXECUTION_EXITEDNoThe command already exited, so it cannot be signaled.
TOO_MANY_EXECUTIONSYesThe sandbox already runs its maximum of 256 commands. Retry after some finish.
IDEMPOTENCY_CONFLICTNoThe idempotencyKey was already used with a different command. Use a new key.
INVALID_REQUESTNoThe runtime rejected the request, for example an unsupported signal.
UNKNOWNNoUnclassified failure. Unmapped runtime errors fail closed to this code.

Retryable codes are safe to retry with backoff, ideally reusing the same idempotencyKey so a retry reconnects instead of provisioning a second sandbox.

Note: Long-running processes with streamed output, file transfer, and terminals go through a gateway session; see Processes, files and terminals.

Process and temporary disk limits​

Pinned revision resources carry pidLimit and ephemeralDiskMb through to the runtime. The process limit is a hard inherited per-UID limit, including threads and child processes; supervised execution applies it separately to the trusted supervisor and untrusted workload. A very small limit can prevent an agent from starting. Per-sandbox limits are rejected on warm-pool claims, where they cannot be applied to the pre-existing pod.

Temporary disk is bounded by Kubernetes ephemeral-storage accounting and workspace emptyDir size limits. Exceeding the configured size causes asynchronous eviction, not immediate write failure; Kubernetes accounting limitations, including open-but-deleted files, still apply. Persistent volumes retain their separate storage policy.

Atomic sandbox capacity admission​

Every new sandbox reserves capacity in the same Mongo transaction as its durable identity. An account owner fence serializes concurrent requests, including an initially empty sandbox set. REQUESTED, allocating, starting, ready, busy, resuming, suspending, failed and terminating rows count; runtimeAllocationPending also counts even if its status is otherwise inactive. Resuming a paused sandbox uses the same atomic reservation. Terminal rows release capacity only when no external allocation remains pending. Idempotent reconnect at capacity returns the original identity, never a second reservation. Dependency or transaction failures fail closed before workload allocation.

Apps and projects are not capped by the platform. The only platform limit is an account-wide safety ceiling on active sandboxes (default 200), which protects an account from a runaway caller creating sandboxes in a loop. Paused sandboxes do not count. Ask Quave ONE support if you need a higher ceiling.

A refusal returns HTTP 429 with data.reason: "account_concurrency_exceeded" (or project_concurrency_exceeded, see below) and data.retryAfterSeconds: 30. No sandbox or cloud workload is created for a refused request.

There is no per-account sandbox zCloud quota: sandbox node pools autoscale, and sandboxes do not count against the AppEnv zCloudsLimit. Pod CPU/memory/disk/PID limits and template idle/hard lifetimes remain independent hard limits.

A create request may set its own projectId and positive maxProjectConcurrency (see Create or connect a sandbox). They become frozen sandbox metadata and cannot change on idempotent retry; the caller's own maxProjectConcurrency is the only per-project cap, counted per projectId (or per app when projectId is omitted).