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
- List sandbox templates and select one with status
READY. - Register a
SANDBOXworkload using the template UID asimage. - Deploy the workload.
- Poll the workload until its state is
running. - Use exec, files, terminals, desktop, or exposed ports.
- 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-onlyUSER— template published by your organization; editable and deletable
Template statuses:
PENDING— publishing or preparingREADY— available for sandbox creationFAILED— template preparation failed
List templates
GET /orgs/{org_slug}/sandbox-templates
Optional query parameters:
limit— positive integer page sizecursor— template UID returned by the previous pagekind—FRESHorUSERstatus—PENDING,READY, orFAILED
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:
typemust beSANDBOX.namemust be lowercase alphanumeric with optional internal hyphens and no more than 32 characters.imageis the UID of aREADYsandbox template.resource_nameis optional because the template fixes the resource shape.project_idis optional.ssh_keyscontains organization SSH key UIDs.portscontains TCP or UDP guest ports. Do not sendrouting; sandbox ports use the sandbox networking path automatically.sandbox_config.idle_timeout_secpauses the sandbox after inactivity.sandbox_config.ttl_secdeletes 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:
nameproject_idssh_keysportssandbox_config.ttl_secsandbox_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
}
cmdis required and limited to 64 KiB.timeout_secdefaults to60and must be between1and600.
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 operation401— missing, invalid, expired, or already-consumed credentials or ticket403— attempting to modify a read-only platform template404— workload, template, terminal, file, or sandbox runtime not found409— invalid lifecycle state, unavailable sandbox, or template still publishing413— file payload exceeds 256 MiB429— terminal or sandbox session limit reached502— the Nexus sandbox gateway failed503— no sandbox capacity is available
Frequently useful reasons:
WORKLOAD_SANDBOX_TEMPLATE_NOT_FOUNDWORKLOAD_SANDBOX_TEMPLATE_NOT_READYWORKLOAD_SANDBOX_NOT_DEPLOYEDWORKLOAD_SANDBOX_INVALID_STATEWORKLOAD_SANDBOX_NO_CAPACITYWORKLOAD_SANDBOX_NO_DESKTOPWORKLOAD_SANDBOX_TARGET_NOT_FOUNDWORKLOAD_SANDBOX_UNAVAILABLEWORKLOAD_ACCESS_TICKET_TTL_INVALIDWORKLOAD_TERMINAL_SIZE_INVALID