Skip to main content

Sandboxes

Sandboxes are isolated Linux workloads created from managed sandbox templates. The API supports lifecycle controls, forking, command execution, file access, interactive terminals, and browser desktops.

Base URL and authentication​

All routes on this page use:

https://api.targon.com/tha/v3

REST requests require an organization-scoped bearer token:

Authorization: Bearer <YOUR_API_TOKEN>

JSON requests also require:

Content-Type: application/json

When targeting a specific deployment, include:

X-Deployment-Type: master

The allowed deployment values are master and shadow.

Replace {org_slug} with your organization slug and {workload_uid} with the sandbox workload UID.

Unknown JSON fields are rejected.

Typical workflow​

  1. List sandbox templates and select one with status READY.
  2. Register a SANDBOX workload using the template UID as image.
  3. Deploy the workload.
  4. Poll the workload until its state is running.
  5. Use exec, files, terminals, desktop, or exposed ports.
  6. Freeze, thaw, fork, publish, or delete the sandbox as needed.

Templates​

Sandbox templates define the image, resources, pricing, and optional desktop support available to a sandbox.

Template kinds:

  • FRESH — platform template available to organizations; read-only
  • USER — template published by your organization; editable and deletable

Template statuses:

  • PENDING — publishing or preparing
  • READY — available for sandbox creation
  • FAILED — template preparation failed

List templates​

GET /orgs/{org_slug}/sandbox-templates

Optional query parameters:

  • limit — positive integer page size
  • cursor — template UID returned by the previous page
  • kind — FRESH or USER
  • status — PENDING, READY, or FAILED

The default page size is 1000.

Example:

curl "https://api.targon.com/tha/v3/orgs/acme/sandbox-templates?status=READY&limit=50" \
-H "Authorization: Bearer $TARGON_API_TOKEN"

Response:

{
"items": [
{
"uid": "sbt-python",
"name": "python",
"display_name": "Python",
"description": "Python development environment",
"kind": "FRESH",
"status": "READY",
"resource_name": "cpu-small",
"cost_per_hour": 0.12,
"frozen_cost_per_hour": 0.01,
"created_at": "2026-09-23T12:00:00Z",
"updated_at": "2026-09-23T12:00:00Z"
}
],
"next_cursor": null
}

Desktop support is reported by the workload desktop endpoint after deployment; it is not included in the template response.

Get a template​

GET /orgs/{org_slug}/sandbox-templates/{sandbox_template_uid}

Returns one sandbox template.

Update a user template​

PATCH /orgs/{org_slug}/sandbox-templates/{sandbox_template_uid}

Body:

{
"display_name": "Python tools",
"description": "Python environment with our standard toolchain"
}

At least one field is required. display_name is limited to 128 characters. Only USER templates can be updated; FRESH templates return 403.

Delete a user template​

DELETE /orgs/{org_slug}/sandbox-templates/{sandbox_template_uid}

Returns 204 No Content.

Only USER templates can be deleted. A template with status PENDING returns 409 until publishing finishes.

Create a sandbox​

Register the workload:

POST /orgs/{org_slug}/workloads

Body:

{
"type": "SANDBOX",
"name": "agent-eval",
"image": "sbt-python",
"project_id": "prj-example",
"ssh_keys": ["shk-example"],
"ports": [
{
"port": 3000,
"protocol": "TCP"
}
],
"sandbox_config": {
"idle_timeout_sec": 300,
"ttl_sec": 3600
}
}

Sandbox-specific fields:

  • type must be SANDBOX.
  • name must be lowercase alphanumeric with optional internal hyphens and no more than 32 characters.
  • image is the UID of a READY sandbox template.
  • resource_name is optional because the template fixes the resource shape.
  • project_id is optional.
  • ssh_keys contains organization SSH key UIDs.
  • ports contains TCP or UDP guest ports. Do not send routing; sandbox ports use the sandbox networking path automatically.
  • sandbox_config.idle_timeout_sec pauses the sandbox after inactivity.
  • sandbox_config.ttl_sec deletes the sandbox after its maximum lifetime.

Timeout values are non-negative seconds. A missing value or 0 disables that timeout. When both values are enabled, idle_timeout_sec must be shorter than ttl_sec. Port 22 is reserved for SSH.

The create response is a workload object in the registered state. Creation does not deploy the sandbox automatically.

Deploy the sandbox​

POST /orgs/{org_slug}/workloads/{workload_uid}/deploy

No request body is required. The response is the updated workload operation. Poll the workload until state.status becomes running.

List sandboxes​

GET /orgs/{org_slug}/workloads?type=SANDBOX

The workload list supports limit, cursor, status, project_id, and name filters. The default page size is 1000. The response contains items and next_cursor.

Get a sandbox​

GET /orgs/{org_slug}/workloads/{workload_uid}

The response includes the common workload fields plus:

{
"type": "SANDBOX",
"sandbox_config": {
"template_uid": "sbt-python",
"parent_workload_uid": "wrk-parent",
"ttl_sec": 3600,
"idle_timeout_sec": 300
},
"frozen_cost_per_hour": 0.01
}

parent_workload_uid is present only for forks.

Get sandbox state​

GET /orgs/{org_slug}/workloads/{workload_uid}/state

Returns the current state, message, service URLs, public IP, SSH port, and replica counts.

Update a sandbox​

PATCH /orgs/{org_slug}/workloads/{workload_uid}

Sandbox workloads can update:

  • name
  • project_id
  • ssh_keys
  • ports
  • sandbox_config.ttl_sec
  • sandbox_config.idle_timeout_sec

Example:

curl -X PATCH \
"https://api.targon.com/tha/v3/orgs/acme/workloads/wrk-example" \
-H "Authorization: Bearer $TARGON_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"ports": [
{"port": 3000, "protocol": "TCP"}
],
"sandbox_config": {
"idle_timeout_sec": 600,
"ttl_sec": 7200
}
}'

Updates are applied without redeploying the sandbox. Unsupported fields return 400 WORKLOAD_SANDBOX_UPDATE_UNSUPPORTED_FIELD.

Set project_id to an empty string to remove the project assignment. An empty ssh_keys array removes all attached SSH keys.

Attach or detach one SSH key​

PUT /orgs/{org_slug}/workloads/{workload_uid}/ssh-keys/{ssh_key_uid}

Attaches an organization SSH key. For a deployed sandbox, NTHA also installs the key in the guest.

DELETE /orgs/{org_slug}/workloads/{workload_uid}/ssh-keys/{ssh_key_uid}

Returns 204 No Content.

Lifecycle​

Freeze​

POST /orgs/{org_slug}/workloads/{workload_uid}/freeze

Freezes a deployed sandbox. The workload state moves from running to frozen while its environment remains available for a later thaw.

Thaw​

POST /orgs/{org_slug}/workloads/{workload_uid}/thaw

Restores a frozen sandbox and returns it to running.

Delete​

DELETE /orgs/{org_slug}/workloads/{workload_uid}

Returns 204 No Content. Deletion is permanent. Deleting a parent does not delete its child forks.

Fork a sandbox​

POST /orgs/{org_slug}/workloads/{workload_uid}/fork

Every body field is optional. An empty body creates a {parent-name}-fork child and copies the parent's project, ports, SSH keys, and timeout settings. To customize the child:

{
"name": "agent-eval-fork",
"project_id": "prj-example",
"sandbox_config": {
"idle_timeout_sec": 300,
"ttl_sec": 1800
}
}

The parent must be deployed and available. Forking creates and deploys the child in one operation. Poll the returned workload UID until it becomes running.

Publish a template​

POST /orgs/{org_slug}/workloads/{workload_uid}/publish

Body:

{
"name": "python-tools",
"display_name": "Python tools",
"description": "Reusable Python development environment"
}

name is required and must contain 1–64 letters, digits, dots, underscores, or hyphens, beginning with a letter or digit. display_name is optional and limited to 128 characters.

The source sandbox must be running. The endpoint returns 202 Accepted with a USER template in the PENDING state. Poll the template until it becomes READY or FAILED.

Execute a command​

POST /orgs/{org_slug}/workloads/{workload_uid}/exec

Body:

{
"cmd": "python --version",
"timeout_sec": 60
}
  • cmd is required and limited to 64 KiB.
  • timeout_sec defaults to 60 and must be between 1 and 600.

Response:

{
"stdout": "Python 3.12.4\n",
"stderr": "",
"code": 0,
"timed_out": false
}

Read and write files​

File paths must be absolute guest paths. File content is base64-encoded. Individual files are limited to 256 MiB.

Read a file​

GET /orgs/{org_slug}/workloads/{workload_uid}/files?path=/workspace/app.py

Response:

{
"path": "/workspace/app.py",
"content_b64": "cHJpbnQoImhlbGxvIikK"
}

Write a file​

PUT /orgs/{org_slug}/workloads/{workload_uid}/files

Body:

{
"path": "/workspace/app.py",
"content_b64": "cHJpbnQoImhlbGxvIikK"
}

Returns 204 No Content.

Access tickets​

Browser WebSocket clients cannot attach an Authorization header. Mint a short-lived access ticket immediately before opening a terminal or desktop WebSocket:

POST /orgs/{org_slug}/workloads/{workload_uid}/access-tickets

Optional body:

{
"ttl_sec": 60
}

The TTL defaults to 60 seconds and must be between 1 and 300 seconds.

Response:

{
"ticket": "sat_example",
"expires_at": "2026-09-23T12:01:00Z"
}

Tickets are:

  • Single-use
  • Scoped to one organization and workload
  • Consumed during the WebSocket handshake
  • Invalid after expiration

Mint a new ticket for every connection or reconnection attempt.

Terminal sessions​

List terminals​

GET /orgs/{org_slug}/workloads/{workload_uid}/terminals

Response:

[
{
"id": "term-1",
"pid": 4242,
"started_at": "2026-09-23T12:00:00Z",
"exited": false
}
]

Exited sessions also include exit_code.

Create a terminal​

POST /orgs/{org_slug}/workloads/{workload_uid}/terminals

The body is optional:

{
"cols": 120,
"rows": 40
}

The default size is 80 columns by 24 rows. Each dimension must be between 1 and 1000.

Delete a terminal​

DELETE /orgs/{org_slug}/workloads/{workload_uid}/terminals/{terminal_id}

Returns 204 No Content. Terminal IDs may contain letters, digits, dots, underscores, and hyphens, up to 64 characters.

Connect to a terminal​

wss://api.targon.com/tha/v3/orgs/{org_slug}/workloads/{workload_uid}/terminals/{terminal_id}/ws?ticket={ticket}

The WebSocket carries raw PTY data as binary frames. Create the terminal and mint the ticket immediately before connecting.

Server-side clients may use the bearer header instead of a ticket.

Desktop sessions​

Check desktop availability​

GET /orgs/{org_slug}/workloads/{workload_uid}/desktop

Desktop-enabled response:

{
"available": true,
"port": 5901,
"listening": true,
"ws_url": "wss://api.targon.com/tha/v3/orgs/acme/workloads/wrk-example/desktop/ws"
}

Headless template response:

{
"available": false
}

If available is true but listening is false, the desktop service is still starting. Poll this endpoint before connecting.

Connect to the desktop​

Append a new access ticket to the returned ws_url:

wss://api.targon.com/tha/v3/orgs/{org_slug}/workloads/{workload_uid}/desktop/ws?ticket={ticket}

The WebSocket carries raw RFB bytes as binary frames. Use an RFB client such as noVNC. No websockify layer is required.

When using noVNC, do not request the binary WebSocket subprotocol; NTHA does not negotiate a subprotocol.

Errors​

Errors use this JSON shape:

{
"error": "Human-readable message",
"reason": "MACHINE_READABLE_REASON"
}

Common HTTP statuses:

  • 400 — invalid body, query, timeout, path, state, or unsupported operation
  • 401 — missing, invalid, expired, or already-consumed credentials or ticket
  • 403 — attempting to modify a read-only platform template
  • 404 — workload, template, terminal, file, or sandbox runtime not found
  • 409 — invalid lifecycle state, unavailable sandbox, or template still publishing
  • 413 — file payload exceeds 256 MiB
  • 429 — terminal or sandbox session limit reached
  • 502 — the Nexus sandbox gateway failed
  • 503 — no sandbox capacity is available

Frequently useful reasons:

  • WORKLOAD_SANDBOX_TEMPLATE_NOT_FOUND
  • WORKLOAD_SANDBOX_TEMPLATE_NOT_READY
  • WORKLOAD_SANDBOX_NOT_DEPLOYED
  • WORKLOAD_SANDBOX_INVALID_STATE
  • WORKLOAD_SANDBOX_NO_CAPACITY
  • WORKLOAD_SANDBOX_NO_DESKTOP
  • WORKLOAD_SANDBOX_TARGET_NOT_FOUND
  • WORKLOAD_SANDBOX_UNAVAILABLE
  • WORKLOAD_ACCESS_TICKET_TTL_INVALID
  • WORKLOAD_TERMINAL_SIZE_INVALID