HTTP API
Every endpoint an application or agent calls, as curl. specs/openapi.yaml has every field, and the Guides show the SDK form of each task.
Authentication
Send a bearer credential on every /v1/* request. The examples read the same two variables as the SDKs.
export PT_API_URL=https://api.example.com
export PT_TOKEN=pt_live_...
curl -s $PT_API_URL/v1/me -H "Authorization: Bearer $PT_TOKEN"
# {"orgId":"org_...","role":"org","userId":...,"orgRole":...}| Credential | Form | Acts as |
|---|---|---|
| API key | pt_live_... | One organization, limited by its scopes |
| Session token | opaque, or the pt.session_token cookie | A signed-in user in their active organization |
| Admin token | the control plane's ADMIN_TOKEN | Platform admin; see the Admin API |
| Bootstrap token | htok_... | A new sandbox host enrolling itself |
API key scopes
A key carries a list of scopes, ["*"] by default. GET /v1/auth/scopes returns the live model.
| Scope | Grants |
|---|---|
* | Everything, including keys, members, webhooks and the audit log |
sandboxes:read, sandboxes:write | GET, or every method, under /v1/sandboxes |
templates:read, templates:write | The same for /v1/templates |
volumes:read, volumes:write | The same for /v1/volumes |
secrets:read, secrets:write, secrets:use | use binds a secret to a sandbox |
<resource>, <resource>:* | Every action on that resource |
sandbox:<id> | Only that sandbox and its direct children |
write implies read; it never implies use. A missing scope returns 403 insufficient_scope with a required field.
Requests and responses
- Bodies are JSON. File uploads are raw bytes.
- A create body is capped at 1 MiB. Uploads are capped at 128 MiB, and a write through the host proxy may be capped lower; both answer
413withmax_bytes. - Action responses (create, expose, resize) are
snake_case. Stored rows (GET /v1/sandboxes/{id}, templates) arecamelCase. Volumes are alwayssnake_case.
Errors
Every error is JSON with a stable code. Branch on code; error is for people and may change.
{ "error": "sandbox not running", "code": "sandbox_not_running", "state": "stopped" }| Status | Default code | Common specific codes |
|---|---|---|
| 400 | bad_request | spec_below_minimum, spec_over_tier_cap, floating_tag_rejected, network_policy_invalid, idempotency_key_invalid |
| 401 | unauthorized | bootstrap_token_* (host tokens) |
| 403 | forbidden | insufficient_scope, sandbox_out_of_scope, organization_suspended |
| 404 | not_found | sandbox_not_found, template_not_found, volume_not_found |
| 409 | conflict | sandbox_not_running, in_progress, operation_in_progress, name_taken, template_in_use, volume_busy |
| 410 | gone | organization_deleted, backup_object_missing |
| 413 | payload_too_large | |
| 422 | unprocessable_entity | idempotency_key_reused |
| 429 | rate_limited | pool_exceeded, org_template_quota_exceeded |
| 500 | internal | |
| 501 | not_implemented | runtime_not_implemented |
| 502, 504 | upstream_error, upstream_timeout | guest_unreachable, guest_timeout |
| 503 | unavailable | creation_disabled, capacity |
in_progress means another stop, start, pause or archive is already running; retry it. Resize uses operation_in_progress. capacity comes from start, clone, fork and resize. spec_over_tier_cap carries field, requested and cap. A validation failure sets error to "<field>: <message>".
Pagination
Lists take limit (default 50, max 200) and offset, and return a bare array. Add paginated=true for an envelope.
{ "rows": [], "total": 132, "offset": 0, "limit": 50, "has_more": true }Exceptions:
/v1/auditand webhook deliveries always return the envelope./v1/secretspages with?cursor=until the returnedcursorisnull./v1/volumesand volume commits return a page; pass?before=from the previous page. They take nooffset.
Idempotency
POST /v1/sandboxes takes an Idempotency-Key header of 8–255 characters, remembered per organization and caller.
- Same key and body: the original sandbox, with
"replayed": true. - Same key, different body:
422 idempotency_key_reused.
Rate limits
Limits are token buckets per control plane process. A 429 carries Retry-After.
| Limit | Default | Applies to | Set with |
|---|---|---|---|
| Writes per organization | 20/s, burst 60 | POST, PUT, PATCH, DELETE on /v1/* | PT_ORG_MUT_RATE, PT_ORG_MUT_BURST |
| Spawns per organization | 20/s, burst 100 | Create, clone, fork | limits.spawn_org_rate_per_s, limits.spawn_org_burst |
| Spawns per IP | 5/s, burst 20 | Create, clone, fork | PT_SPAWN_RATE_PER_S, PT_SPAWN_BURST |
| Guest calls per sandbox | 50/s, burst 100 | Exec, run-code, processes | PT_SBX_RPC_RATE, PT_SBX_RPC_BURST |
The guest-call limit answers 429 rate_limited with bucket: "sandbox_rpc". Process stdin, signal and resize have their own per-organization bucket and do not count as writes. Admins are exempt from these limits. GET /v1/auth/orgs/quota shows the organization's resource quotas and usage.
Identity and fleet
| Endpoint | Returns |
|---|---|
GET /v1/me | {orgId, role, userId, orgRole} |
GET /v1/regions | [{region, label}] for regions with a placeable host |
GET /v1/capacity | cpu, ramMb, diskGb, each {total, free, maxOneHost} |
GET /v1/runtimes | {runtimes}; microvm is the default |
Sandboxes
POST /v1/sandboxes
Create a sandbox. It returns 201 once the sandbox is placed, or after it reaches wait_for_state.
| Field | Type | Default | Description |
|---|---|---|---|
template | string | pt-base | Template name or id; not with image. See below |
image | object | none | {base_image, steps, entrypoint, ready_cmd, size_mb}; builds or reuses a template |
name | string | none | 1–63 chars, unique per organization |
cpu | int | template default | 1–16 |
ram_mb | int | template default | 128–32768 |
disk_gb | int | template default | 1–100 |
region | string | template region | One of GET /v1/regions |
envVars | object | {} | Alias env; set in exec, run-code and processes |
metadata | object | {} | Up to 16 KiB |
parent_id | string | none | Create as a child sandbox |
volumes | object | none | {"/data": {"volume", "read_only", "subdir"}} |
ssh_keys | string[] | [] | Written to /root/.ssh/authorized_keys |
auto_stop_minutes | int | 0 | Idle minutes before stop; 0 never stops |
auto_archive_days | int | 7 | Days stopped before archiving |
auto_delete_days | int | 0 | Days before deletion; 0 never deletes |
auto_resume | bool | true | A preview request may wake a stopped sandbox |
backup_interval_min | int | 60 | Minutes between backups while running; 0 turns them off |
language | string | none | Default run-code language |
expose | array | none | Up to 8 {port, public, ttl_seconds}; URLs return in exposed |
network | object | none | Egress policy applied before first boot; see Network policy |
allow_internet_access | bool | true | false blocks outbound before first boot |
secrets | array | none | Org secrets to attach; needs secrets:use |
wait | bool | false | Same as ?wait_for_state=running |
Values below the template's cpu, ram_mb or disk_gb defaults are refused with 400: the template default is also the minimum. Resize follows the same rule.
The default template is pt-base, or PT_DEFAULT_TEMPLATE when set. If neither is ready, the organization's first default create builds python:3.12-slim (1 vCPU, 512 MiB, 2 GiB) before the sandbox boots.
Query: wait_for_state holds the response until that state, for up to wait_timeout_ms (default 30000, max 60000). A wait that runs out returns 202 with wait_timed_out: true.
curl -s -X POST "$PT_API_URL/v1/sandboxes?wait_for_state=running" \
-H "Authorization: Bearer $PT_TOKEN" -H 'Content-Type: application/json' \
-H 'Idempotency-Key: job-42-attempt-1' \
-d '{"template":"pt-base","name":"agent-42","cpu":2,"ram_mb":2048,"expose":[{"port":8080}]}'{
"id": "sbx_01JABCDEF0123456789ABCDEFG",
"state": "running",
"exposed": [{ "port": 8080, "url": "https://8080-01jabcdef....sbx.example.com/?t=...", "public": false }]
}Errors: 409 name_taken, 429 pool_exceeded, 503 unavailable with error: "no capacity" (plus region and excluded_hosts) when no host has room, 503 creation_disabled when an operator paused creates.
GET /v1/sandboxes
List sandboxes, newest first. Rows omit envVars.
| Query | Default | Description |
|---|---|---|
state | all | Filter by state |
runtime | all | Filter by runtime; an unknown value is 400 |
parent_id | all | Children of this id; none lists roots |
limit, offset, paginated | 50, 0, false | See Pagination |
curl -s "$PT_API_URL/v1/sandboxes?state=running&paginated=true" -H "Authorization: Bearer $PT_TOKEN"More sandbox endpoints
| Endpoint | Description |
|---|---|
GET /v1/sandboxes/{id} | Full row (camelCase), plus agent_ready, runtime_ready, runtime_reason |
PATCH /v1/sandboxes/{id} | {name}; null clears it |
PATCH /v1/sandboxes/{id}/env | {env: {KEY: "value"}}; a null value removes the key |
DELETE /v1/sandboxes/{id} | Returns {id, state: "stopping"}; 409 sandbox_has_children if it has children |
GET /v1/sandboxes/{id}/children | Direct child sandboxes |
GET /v1/sandboxes/{id}/metrics | cpu_pct, mem_used_mb, disk_used_mb; null when stale |
POST /v1/sandboxes/sweep-failed | Delete every failed sandbox in the organization |
States: pending, provisioning, starting, running, stopping, stopped, resuming, migrating, restoring, archiving, archived, unarchiving, failed-provision, failed-start, lost, deleted.
Lifecycle
stop, pause, start and resume take wait_for_state and wait_timeout_ms.
| Endpoint | Body | Description |
|---|---|---|
POST /v1/sandboxes/{id}/stop | Save memory and disk, then stop; running only | |
POST /v1/sandboxes/{id}/pause | Alias of stop | |
POST /v1/sandboxes/{id}/start | Resume a stopped or archived sandbox; 202 with Retry-After while unarchiving | |
POST /v1/sandboxes/{id}/resume | Alias of start | |
POST /v1/sandboxes/{id}/archive | Move a stopped sandbox's disk to object storage | |
POST /v1/sandboxes/{id}/resize | {cpu, ram_mb, disk_gb} | Cold-boots at the new size; memory is not kept |
POST /v1/sandboxes/{id}/ssh-keys | {keys} | Replace /root/.ssh/authorized_keys |
A stopped sandbox can change all three resize fields. A running one can only grow disk_gb, and disk never shrinks.
curl -s -X POST "$PT_API_URL/v1/sandboxes/$SBX/stop?wait_for_state=stopped" -H "Authorization: Bearer $PT_TOKEN"Exec
POST /v1/sandboxes/{id}/exec
Run a command and wait for it. The process group is killed when the call returns.
| Field | Type | Default | Description |
|---|---|---|---|
cmd | string or string[] | required | An array runs directly; a string runs through sh -c |
timeout_ms | int | 30000 | 100–300000; clamped to 85000 per call |
curl -s -X POST $PT_API_URL/v1/sandboxes/$SBX/exec \
-H "Authorization: Bearer $PT_TOKEN" -H 'Content-Type: application/json' \
-d '{"cmd":["python3","-c","print(6*7)"]}'{ "result": { "ok": true, "stdout": "42\n", "stderr": "", "exit_code": 0 } }A non-zero exit_code is still HTTP 200. A clamped call echoes timeout_ms and timeout_ms_clamped: true. Run longer work as a process.
POST /v1/sandboxes/{id}/run-code
Run a snippet through an interpreter. The interpreter must exist in the template.
| Field | Type | Default | Description |
|---|---|---|---|
code | string | required | Up to 1 MiB |
lang | string | sandbox language, else python | python, node, bash or sh |
timeout_ms | int | 30000 | Same limits as exec |
curl -s -X POST $PT_API_URL/v1/sandboxes/$SBX/run-code \
-H "Authorization: Bearer $PT_TOKEN" -H 'Content-Type: application/json' \
-d '{"lang":"node","code":"console.log(1+1)"}'
# {"stdout":"2\n","stderr":"","exit_code":0,...}Files
Paths are absolute guest paths in ?path=. Segments of .. and NUL bytes are rejected. The sandbox must be running.
| Endpoint | Description |
|---|---|
GET /files?path= | Raw bytes; with offset and limit, 206 plus x-total-bytes |
PUT /files?path=&mode=0644 | Write the request body; returns {ok, size, mode} |
PUT /files/atomic?path= | Same, but readers never see a partial file |
DELETE /files?path=&recurse=true | Delete a path |
GET /files/stat?path= | Size, mode, mtime and type |
GET /files/list?path= | {entries: [{name, size, mode, is_dir}]} |
POST /files/mkdir?path= | Create with parents |
GET /files/find?path=&pattern=*.py&max=500 | {matches} |
GET /files/grep?path=&pattern=TODO&max=500 | {lines} |
POST /files/replace | {path, find, replace, glob}; regex replace across files |
GET /files/watch?path=&interval_ms=&max_seconds= | Server-sent events: ready, then change with {ts, type, path} |
Paths in the table are relative to /v1/sandboxes/{id}.
curl -s -X PUT "$PT_API_URL/v1/sandboxes/$SBX/files?path=/workspace/app.py" \
-H "Authorization: Bearer $PT_TOKEN" --data-binary @app.pyProcesses and terminal
POST /v1/sandboxes/{id}/processes
Start a process the guest agent owns. It outlives the request, and its record stays readable after it exits.
| Field | Type | Default | Description |
|---|---|---|---|
cmd | string or string[] | required | Same as exec |
cwd | string | none | Working directory |
env | object | none | Extra environment |
pty | bool | false | Run on a pseudo-terminal |
cols, rows | int | none | PTY size; 1–1000 |
timeout_ms | int | 0 | 0 means no deadline; max 86400000 |
curl -s -X POST $PT_API_URL/v1/sandboxes/$SBX/processes \
-H "Authorization: Bearer $PT_TOKEN" -H 'Content-Type: application/json' \
-d '{"cmd":"python3 -m http.server 8080","cwd":"/workspace"}'
# 201 {"handle":"...","pid":123,"pty":false}| Endpoint | Description |
|---|---|
GET /processes | {procs} |
GET /processes/{handle}?wait_ms= | The record; waits up to 55000 ms for exit |
GET /processes/{handle}/logs | Output from stdout_offset and stderr_offset; *_dropped counts evicted bytes |
GET /processes/{handle}/stream | Server-sent events: stdout, stderr, exit, error, end |
POST /processes/{handle}/stdin | {data, encoding, close} |
POST /processes/{handle}/signal | {signal, group} |
POST /processes/{handle}/resize | {cols, rows}; PTY only, else 409 not_a_pty |
DELETE /processes/{handle} | Kill it; the record stays |
Offsets are byte counts since the process started. Send back the returned offsets to continue where you stopped.
GET /v1/sandboxes/{id}/terminal
Upgrade to a WebSocket running a shell on a PTY. Frames are raw bytes both ways, and the call needs sandboxes:write.
websocat -H "Authorization: Bearer $PT_TOKEN" "wss://api.example.com/v1/sandboxes/$SBX/terminal"A browser can pass ?token= instead of the header. Close codes: 1008 forbidden, 1011 sandbox or host unavailable, 1012 control plane restarting.
Network policy
POST /v1/sandboxes/{id}/network
Replace a running sandbox's egress policy. Sandboxes have internet access until you set one, and {} clears it.
| Field | Type | Description |
|---|---|---|
allow_out | string[] | Up to 64 hostnames, IPs or CIDRs |
deny_out | string[] | Up to 64 IPs or CIDRs; no hostnames |
rules | object | {"<host>": [{"transform": {"headers": {...}}}]}; headers to inject |
To allow only some hosts, deny everything and list the exceptions. A hostname in allow_out needs 0.0.0.0/0 in deny_out, or the call returns 400.
curl -s -X POST $PT_API_URL/v1/sandboxes/$SBX/network \
-H "Authorization: Bearer $PT_TOKEN" -H 'Content-Type: application/json' \
-d '{"deny_out":["0.0.0.0/0"],"allow_out":["pypi.org","files.pythonhosted.org"]}'The body is either bare, as above, or wrapped like create: {"allow_internet_access": false, "network": {"allow_out": [...]}}. Never mix the two. A top-level allow_internet_access switches to the wrapped form, so a bare allow_out next to it is ignored and everything is blocked.
GET returns the policy, with owned (yours) apart from managed (added by secret bindings). The older POST /egress-policy takes {mode, cidrs}.
Preview URLs
POST /v1/sandboxes/{id}/expose
Expose a port of a running sandbox through the edge proxy.
| Field | Type | Default | Description |
|---|---|---|---|
port | int | required | 1–65535 |
public | bool | false | true drops the token requirement |
ttl_seconds | int | 86400 | Token lifetime; max one year |
protocol | string | http | tcp exposes a raw, always-public port |
curl -s -X POST $PT_API_URL/v1/sandboxes/$SBX/expose \
-H "Authorization: Bearer $PT_TOKEN" -H 'Content-Type: application/json' \
-d '{"port":8080}'{ "url": "https://8080-01jabcdef0123456789abcdefg.sbx.example.com/?t=eyJ...", "port": 8080, "public": false }The host is <port>-<sandbox id without sbx_>.<PT_SBX_DOMAIN>. A private URL needs its token as ?t= or an x-pt-token header, and a bad token gets 404.
| Endpoint | Description |
|---|---|
DELETE /v1/sandboxes/{id}/expose/{port} | Unexpose; every token for that port stops working |
POST /v1/sandboxes/{id}/expose/revoke | {token, reason}; revoke one token |
Snapshots, clones and backups
POST /v1/sandboxes/{id}/snapshot
Snapshot a sandbox. Returns 202 if the snapshot is still running after 180 s.
| Field | Type | Default | Description |
|---|---|---|---|
scrub_env | string[] | none | Env vars to remove from the copy |
clear_tmp | bool | false | Empty /tmp in the copy |
clear_ssh_keys | bool | false | Remove authorized SSH keys from the copy |
curl -s -X POST $PT_API_URL/v1/sandboxes/$SBX/snapshot \
-H "Authorization: Bearer $PT_TOKEN" -H 'Content-Type: application/json' \
-d '{"scrub_env":["API_KEY"],"clear_ssh_keys":true}'
# {"id":"snap_...","sandbox_id":"sbx_...","size_bytes":...}| Endpoint | Body | Description |
|---|---|---|
GET /v1/sandboxes/{id}/snapshots | List snapshots | |
DELETE /v1/sandboxes/{id}/snapshots/{snapId} | Delete one | |
POST /v1/sandboxes/{id}/restore | {snapshot_id} | Roll this sandbox back; latest by default |
POST /v1/sandboxes/{id}/clone | {snapshot_id} | 201; new sandbox from a snapshot |
POST /v1/sandboxes/{id}/fork | 201; snapshot now and start a copy | |
POST /v1/sandboxes/{id}/backup | Back up the disk to object storage | |
POST /v1/sandboxes/{id}/restore-from-backup | Rebuild from the last backup on any host |
Only snapshot scrubs. To fork a scrubbed copy, snapshot with scrubbing, then clone that snapshot_id.
Templates
Builds are asynchronous. Each from-* call returns 202 {id, name, state: "building"}; poll GET /v1/templates/{id} until state is ready or failed.
POST /v1/templates/from-image
Build a template from a container image.
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | Lowercase, ^[a-z0-9][a-z0-9._-]*$, up to 64 |
image | string | required | Image ref. latest, lts, stable, main, master, edge and rolling tags are rejected; pin a version or @sha256: digest |
entrypoint | string | image's | Override |
version | string | 1.0.0 | Floating names are rejected |
default_cpu | int | 1 | 1–16 |
default_ram_mb | int | 512 | 128–32768 |
default_disk_gb | int | 2 | 1–100 |
size_mb | int | 1024 | Rootfs size; 128–512000 |
region | string | none | Pin the build to a region |
curl -s -X POST $PT_API_URL/v1/templates/from-image \
-H "Authorization: Bearer $PT_TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"py312","image":"python:3.12-slim","default_ram_mb":1024}'
# 202 {"id":"tpl_...","name":"py312","state":"building"}A name you already own, ready or building, is returned with existing: true instead of rebuilt.
POST /v1/templates/from-spec
Build from a base image plus declarative steps. Each step names its kind in op.
op | Fields |
|---|---|
env | key, value |
workdir | path |
user | user |
run | cmd |
pip, npm, apt | packages, non-empty |
kernel_modules | profile: "container" |
copy | content_b64, dst, mode |
curl -s -X POST $PT_API_URL/v1/templates/from-spec \
-H "Authorization: Bearer $PT_TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"agent-base","base_image":"ubuntu:24.04","steps":[
{"op":"apt","packages":["git","curl"]},{"op":"workdir","path":"/workspace"}]}'POST /v1/templates/from-build
Build from an uploaded Docker context. Get an upload URL first; the client uploads straight to object storage.
R=$(curl -s -X POST $PT_API_URL/v1/templates/from-build/presign -H "Authorization: Bearer $PT_TOKEN")
tar czf ctx.tar.gz -C ./myapp .
curl -s -X PUT --upload-file ctx.tar.gz "$(echo "$R" | jq -r .upload_url)"
curl -s -X POST $PT_API_URL/v1/templates/from-build \
-H "Authorization: Bearer $PT_TOKEN" -H 'Content-Type: application/json' \
-d "{\"name\":\"myapp\",\"context_s3_key\":\"$(echo "$R" | jq -r .context_s3_key)\"}"upload_url is signed for PT_S3_PUBLIC_ENDPOINT when set, else PT_S3_ENDPOINT, so the client must reach that endpoint.
More template endpoints
| Endpoint | Description |
|---|---|
GET /v1/templates | Your organization's and global templates; ?name=, paginated |
GET /v1/templates/{id} | Includes buildLogs and buildSpec for your own templates |
PATCH /v1/templates/{id} | {version, default_cpu, default_ram_mb, default_disk_gb, ready_cmd, entrypoint} |
DELETE /v1/templates/{id} | Deprecate; 409 template_in_use while sandboxes use it |
POST /v1/templates/{id}/cancel | Cancel a running build |
POST /v1/templates/from-git | {name, repo, ref, subdir, dockerfile, token}; build a repo's Dockerfile; token is never stored |
POST /v1/templates/from-patch | Overlay uploaded files on an existing template |
POST /v1/templates/{id}/derive | Boot the parent, capture memory, so sandboxes start warm |
Volumes
A volume is a named, versioned file store. Every /v1/volumes route needs object storage and returns 503 unavailable without it.
{v} is a volume name or id. Names are up to 63 letters, digits, ., _ or -, starting with a letter or digit; vol_*, tmp-* and limits are reserved.
| Endpoint | Body or query | Description |
|---|---|---|
POST /v1/volumes | {name, sync_mode} | 201; sync_mode is drive (default) or git |
PUT /v1/volumes/{name} | {sync_mode} | Open or create: 200 existing, 201 created |
GET /v1/volumes | limit, before, q | A page of volumes |
GET /v1/volumes/limits | The organization's limits and counts | |
GET, PATCH, DELETE /v1/volumes/{v} | PATCH {name, sync_mode} | DELETE is 204, or 409 volume_busy while mounted |
GET /v1/volumes/{v}/commits | limit, before | History |
GET /v1/volumes/{v}/commits/{ref} | One commit: id, tag or head | |
GET /v1/volumes/{v}/diff | from, to, path | Changed paths |
GET /v1/volumes/{v}/tags | {tags} | |
PUT, DELETE /v1/volumes/{v}/tags/{tag} | PUT {commit} | Name a commit; head by default |
POST /v1/volumes/{v}/restore | {to} | New head commit |
POST /v1/volumes/{v}/fork | {name, at} | 201 new volume |
GET /v1/volumes/{v}/files | path, recursive, at | Directory listing |
GET, PUT /v1/volumes/{v}/files/content?path= | raw bytes on PUT | Read or write one file |
GET /v1/volumes/{v}/files/stat?path= | at | One entry |
POST /v1/volumes/{v}/files/copy, files/move | {src, dst, overwrite} | New commit |
DELETE /v1/volumes/{v}/files?path= | recursive | Remove |
GET /v1/volumes/{v}/mounts | Live mounts | |
POST /v1/volumes/{v}/commit | {mount_id} | Commit every writable mount now |
GET /v1/volumes/{v}/wait | commit, timeout | Wait until mounts show a commit; max 50 s, 408 on timeout |
GET /v1/sandboxes/{id}/volumes | The sandbox's mounts | |
POST /v1/sandboxes/{id}/volumes/{mount} | {volume, read_only, subdir, ref} | Mount into a running sandbox |
DELETE /v1/sandboxes/{id}/volumes/{mount} | commit=false | Unmount; commits first unless told not to |
POST /v1/sandboxes/{id}/volumes/{mount}/commit | {wait} | Commit the mount now |
POST /v1/sandboxes/{id}/volumes/{mount}/pull | Move the mount to the volume's head |
{mount} is the URL-encoded mount path, such as %2Fdata.
curl -s -X PUT $PT_API_URL/v1/volumes/datasets \
-H "Authorization: Bearer $PT_TOKEN" -H 'Content-Type: application/json' -d '{}'Secrets
POST /v1/secrets
Store an org secret. The egress proxy writes it into outbound headers for allowed hosts; it never enters the guest, and no endpoint returns it.
Creating, updating, rotating, deleting and binding secrets needs an org owner or admin, or an approved service key. Members get 403 owner_admin_only, whatever their key's scopes.
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | Lowercase, starts with a letter |
value | string | required | The credential |
allow | string[] | required | Exact hostnames the value may reach |
headers | string[] | required | Request headers the value is written into |
on_echo | string | block | block or redact when a response echoes the value |
description | string | none | What the credential is for |
curl -s -X POST $PT_API_URL/v1/secrets \
-H "Authorization: Bearer $PT_TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"llm_key","value":"sk-...","allow":["api.example.com"],"headers":["Authorization"]}'Secrets return 503 until the control plane has a keyring (PT_SECRETS_ACTIVE_KID and PT_SECRETS_KEY_<KID>).
| Endpoint | Description |
|---|---|
GET /v1/secrets | {items, total, cursor}; limit, cursor |
GET, PATCH, DELETE /v1/secrets/{id} | Changing allow, headers or on_echo also needs secrets:use |
POST /v1/secrets/{id}/versions | {value}; rotate |
POST /v1/secrets/bindings | {sandbox_id, alias, secret_id}; needs secrets:use |
DELETE /v1/secrets/bindings/{id} | Revoke a binding |
GET, PUT /v1/sandboxes/{id}/secrets | Read or replace a sandbox's attached set |
DELETE /v1/sandboxes/{id}/secrets/{alias} | Detach one |
Webhooks
POST /v1/webhooks
Subscribe an endpoint to events. An organization can have 20.
| Field | Type | Default | Description |
|---|---|---|---|
url | string | required | Endpoint; private addresses are refused |
events | string[] | ["*"] | Events to deliver |
secret | string | generated | HMAC key, at least 16 chars |
curl -s -X POST $PT_API_URL/v1/webhooks \
-H "Authorization: Bearer $PT_TOKEN" -H 'Content-Type: application/json' \
-d '{"url":"https://example.com/hook","events":["sandbox.state_updated"]}'
# 201 {"id":"wh_...","url":"...","events":[...],"secret_returned_once":"..."}WARNING
secret_returned_once is the only time the secret is shown. Store it before you discard the response.
Events: sandbox.created, sandbox.state_updated, sandbox.deleted, snapshot.created, snapshot.removed, template.state_updated.
Each delivery is a POST of {event, org_id, ts, delivery_id, data} with x-pt-signature: sha256=<hex>, the HMAC-SHA256 of the raw body. Reject a stale ts or a repeated delivery_id to stop replays.
echo -n "$RAW_BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | sed 's/^.* /sha256=/'Delivery and retries
Each attempt has a 5 s timeout and does not follow redirects. Failures retry after 30 s, 2 min, 10 min, 1 h and 6 h, then the delivery becomes dead_letter. ts changes on every attempt; delivery_id does not. Headers also carry x-pt-event, x-pt-delivery-id and x-pt-delivery-webhook-id.
| Endpoint | Description |
|---|---|
GET /v1/webhooks | List; secrets are never returned |
PATCH /v1/webhooks/{id} | {enabled, url, events} |
DELETE /v1/webhooks/{id} | Delete |
POST /v1/webhooks/{id}/test | Send a ping; 202 {delivery_id} |
GET /v1/webhooks/{id}/deliveries | ?status=pending|success|failed|dead_letter |
POST /v1/webhooks/{id}/deliveries/{dlvId}/retry | Requeue a failed or dead-letter delivery |
API keys
POST /v1/auth/api-keys
Mint a key in the active organization. Account routes, including this one, need the * scope or a session.
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | Up to 120 chars |
scopes | string[] | ["*"] | See API key scopes |
expires_at | string | never | ISO 8601 time in the future |
curl -s -X POST $PT_API_URL/v1/auth/api-keys \
-H "Authorization: Bearer $PT_TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"ci","scopes":["sandboxes:*","templates:read"]}'
# {"id":"key_...","prefix":"pt_live_...","token":"pt_live_...","scopes":[...],"expiresAt":null}token is shown once.
| Endpoint | Description |
|---|---|
GET /v1/auth/api-keys | Keys in the active organization, without secrets |
DELETE /v1/auth/api-keys/{id} | Revoke now |
GET /v1/auth/scopes | The scope model |
Organizations and members
| Endpoint | Description |
|---|---|
GET /v1/auth/me | Caller, user, active organization, all organizations, feature flags |
GET /v1/auth/orgs | Organizations the user belongs to; session only |
POST /v1/auth/orgs | {name}; create an organization you own; session only |
POST /v1/auth/orgs/switch | {orgId}; change the session's active organization |
GET /v1/auth/orgs/quota | Effective limits and usage |
GET /v1/auth/orgs/members | Members |
PATCH /v1/auth/orgs/members/{userId} | {role}; owner only |
DELETE /v1/auth/orgs/members/{userId} | Remove a member and revoke their keys here |
POST /v1/auth/orgs/members/invite | {email, role}; returns acceptUrl |
GET /v1/auth/orgs/invites | Invites sent from this organization |
PATCH, DELETE /v1/auth/orgs/invites/{id} | Change role or expiry (max 7 days), or revoke |
GET /v1/auth/orgs/invites/{id}/preview | No auth |
POST /v1/auth/orgs/invites/accept | {code}, the invite id |
POST /v1/auth/orgs/invites/{id}/decline | Decline |
GET /v1/auth/orgs/invites/received | Invites addressed to you |
POST /v1/auth/orgs/leave | Leave the active organization |
acceptUrl works without email delivery. Send it to the invitee yourself.
Audit log
GET /v1/audit
Read the organization's audit log, as an owner or admin. Always paginated.
| Query | Description |
|---|---|
q | Free-text match |
action | Action prefix, such as sandbox. |
actor | Actor user id |
status | ok, denied or error |
since, until | Time range |
limit, offset | See Pagination |
curl -s "$PT_API_URL/v1/audit?action=sandbox.&limit=20" -H "Authorization: Bearer $PT_TOKEN"Each row has action, resourceType, resourceId, actorEmail, status, ip, metadata and createdAt.