Skip to content

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.

bash
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":...}
CredentialFormActs as
API keypt_live_...One organization, limited by its scopes
Session tokenopaque, or the pt.session_token cookieA signed-in user in their active organization
Admin tokenthe control plane's ADMIN_TOKENPlatform admin; see the Admin API
Bootstrap tokenhtok_...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.

ScopeGrants
*Everything, including keys, members, webhooks and the audit log
sandboxes:read, sandboxes:writeGET, or every method, under /v1/sandboxes
templates:read, templates:writeThe same for /v1/templates
volumes:read, volumes:writeThe same for /v1/volumes
secrets:read, secrets:write, secrets:useuse 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 413 with max_bytes.
  • Action responses (create, expose, resize) are snake_case. Stored rows (GET /v1/sandboxes/{id}, templates) are camelCase. Volumes are always snake_case.

Errors ​

Every error is JSON with a stable code. Branch on code; error is for people and may change.

json
{ "error": "sandbox not running", "code": "sandbox_not_running", "state": "stopped" }
StatusDefault codeCommon specific codes
400bad_requestspec_below_minimum, spec_over_tier_cap, floating_tag_rejected, network_policy_invalid, idempotency_key_invalid
401unauthorizedbootstrap_token_* (host tokens)
403forbiddeninsufficient_scope, sandbox_out_of_scope, organization_suspended
404not_foundsandbox_not_found, template_not_found, volume_not_found
409conflictsandbox_not_running, in_progress, operation_in_progress, name_taken, template_in_use, volume_busy
410goneorganization_deleted, backup_object_missing
413payload_too_large
422unprocessable_entityidempotency_key_reused
429rate_limitedpool_exceeded, org_template_quota_exceeded
500internal
501not_implementedruntime_not_implemented
502, 504upstream_error, upstream_timeoutguest_unreachable, guest_timeout
503unavailablecreation_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.

json
{ "rows": [], "total": 132, "offset": 0, "limit": 50, "has_more": true }

Exceptions:

  • /v1/audit and webhook deliveries always return the envelope.
  • /v1/secrets pages with ?cursor= until the returned cursor is null.
  • /v1/volumes and volume commits return a page; pass ?before= from the previous page. They take no offset.

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.

LimitDefaultApplies toSet with
Writes per organization20/s, burst 60POST, PUT, PATCH, DELETE on /v1/*PT_ORG_MUT_RATE, PT_ORG_MUT_BURST
Spawns per organization20/s, burst 100Create, clone, forklimits.spawn_org_rate_per_s, limits.spawn_org_burst
Spawns per IP5/s, burst 20Create, clone, forkPT_SPAWN_RATE_PER_S, PT_SPAWN_BURST
Guest calls per sandbox50/s, burst 100Exec, run-code, processesPT_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 ​

EndpointReturns
GET /v1/me{orgId, role, userId, orgRole}
GET /v1/regions[{region, label}] for regions with a placeable host
GET /v1/capacitycpu, 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.

FieldTypeDefaultDescription
templatestringpt-baseTemplate name or id; not with image. See below
imageobjectnone{base_image, steps, entrypoint, ready_cmd, size_mb}; builds or reuses a template
namestringnone1–63 chars, unique per organization
cpuinttemplate default1–16
ram_mbinttemplate default128–32768
disk_gbinttemplate default1–100
regionstringtemplate regionOne of GET /v1/regions
envVarsobject{}Alias env; set in exec, run-code and processes
metadataobject{}Up to 16 KiB
parent_idstringnoneCreate as a child sandbox
volumesobjectnone{"/data": {"volume", "read_only", "subdir"}}
ssh_keysstring[][]Written to /root/.ssh/authorized_keys
auto_stop_minutesint0Idle minutes before stop; 0 never stops
auto_archive_daysint7Days stopped before archiving
auto_delete_daysint0Days before deletion; 0 never deletes
auto_resumebooltrueA preview request may wake a stopped sandbox
backup_interval_minint60Minutes between backups while running; 0 turns them off
languagestringnoneDefault run-code language
exposearraynoneUp to 8 {port, public, ttl_seconds}; URLs return in exposed
networkobjectnoneEgress policy applied before first boot; see Network policy
allow_internet_accessbooltruefalse blocks outbound before first boot
secretsarraynoneOrg secrets to attach; needs secrets:use
waitboolfalseSame 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.

bash
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}]}'
json
{
  "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.

QueryDefaultDescription
stateallFilter by state
runtimeallFilter by runtime; an unknown value is 400
parent_idallChildren of this id; none lists roots
limit, offset, paginated50, 0, falseSee Pagination
bash
curl -s "$PT_API_URL/v1/sandboxes?state=running&paginated=true" -H "Authorization: Bearer $PT_TOKEN"

More sandbox endpoints ​

EndpointDescription
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}/childrenDirect child sandboxes
GET /v1/sandboxes/{id}/metricscpu_pct, mem_used_mb, disk_used_mb; null when stale
POST /v1/sandboxes/sweep-failedDelete 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.

EndpointBodyDescription
POST /v1/sandboxes/{id}/stopSave memory and disk, then stop; running only
POST /v1/sandboxes/{id}/pauseAlias of stop
POST /v1/sandboxes/{id}/startResume a stopped or archived sandbox; 202 with Retry-After while unarchiving
POST /v1/sandboxes/{id}/resumeAlias of start
POST /v1/sandboxes/{id}/archiveMove 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.

bash
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.

FieldTypeDefaultDescription
cmdstring or string[]requiredAn array runs directly; a string runs through sh -c
timeout_msint30000100–300000; clamped to 85000 per call
bash
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)"]}'
json
{ "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.

FieldTypeDefaultDescription
codestringrequiredUp to 1 MiB
langstringsandbox language, else pythonpython, node, bash or sh
timeout_msint30000Same limits as exec
bash
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.

EndpointDescription
GET /files?path=Raw bytes; with offset and limit, 206 plus x-total-bytes
PUT /files?path=&mode=0644Write the request body; returns {ok, size, mode}
PUT /files/atomic?path=Same, but readers never see a partial file
DELETE /files?path=&recurse=trueDelete 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}.

bash
curl -s -X PUT "$PT_API_URL/v1/sandboxes/$SBX/files?path=/workspace/app.py" \
  -H "Authorization: Bearer $PT_TOKEN" --data-binary @app.py

Processes 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.

FieldTypeDefaultDescription
cmdstring or string[]requiredSame as exec
cwdstringnoneWorking directory
envobjectnoneExtra environment
ptyboolfalseRun on a pseudo-terminal
cols, rowsintnonePTY size; 1–1000
timeout_msint00 means no deadline; max 86400000
bash
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}
EndpointDescription
GET /processes{procs}
GET /processes/{handle}?wait_ms=The record; waits up to 55000 ms for exit
GET /processes/{handle}/logsOutput from stdout_offset and stderr_offset; *_dropped counts evicted bytes
GET /processes/{handle}/streamServer-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.

bash
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.

FieldTypeDescription
allow_outstring[]Up to 64 hostnames, IPs or CIDRs
deny_outstring[]Up to 64 IPs or CIDRs; no hostnames
rulesobject{"<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.

bash
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.

FieldTypeDefaultDescription
portintrequired1–65535
publicboolfalsetrue drops the token requirement
ttl_secondsint86400Token lifetime; max one year
protocolstringhttptcp exposes a raw, always-public port
bash
curl -s -X POST $PT_API_URL/v1/sandboxes/$SBX/expose \
  -H "Authorization: Bearer $PT_TOKEN" -H 'Content-Type: application/json' \
  -d '{"port":8080}'
json
{ "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.

EndpointDescription
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.

FieldTypeDefaultDescription
scrub_envstring[]noneEnv vars to remove from the copy
clear_tmpboolfalseEmpty /tmp in the copy
clear_ssh_keysboolfalseRemove authorized SSH keys from the copy
bash
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":...}
EndpointBodyDescription
GET /v1/sandboxes/{id}/snapshotsList 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}/fork201; snapshot now and start a copy
POST /v1/sandboxes/{id}/backupBack up the disk to object storage
POST /v1/sandboxes/{id}/restore-from-backupRebuild 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.

FieldTypeDefaultDescription
namestringrequiredLowercase, ^[a-z0-9][a-z0-9._-]*$, up to 64
imagestringrequiredImage ref. latest, lts, stable, main, master, edge and rolling tags are rejected; pin a version or @sha256: digest
entrypointstringimage'sOverride
versionstring1.0.0Floating names are rejected
default_cpuint11–16
default_ram_mbint512128–32768
default_disk_gbint21–100
size_mbint1024Rootfs size; 128–512000
regionstringnonePin the build to a region
bash
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.

opFields
envkey, value
workdirpath
useruser
runcmd
pip, npm, aptpackages, non-empty
kernel_modulesprofile: "container"
copycontent_b64, dst, mode
bash
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.

bash
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 ​

EndpointDescription
GET /v1/templatesYour 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}/cancelCancel 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-patchOverlay uploaded files on an existing template
POST /v1/templates/{id}/deriveBoot 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.

EndpointBody or queryDescription
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/volumeslimit, before, qA page of volumes
GET /v1/volumes/limitsThe 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}/commitslimit, beforeHistory
GET /v1/volumes/{v}/commits/{ref}One commit: id, tag or head
GET /v1/volumes/{v}/difffrom, to, pathChanged 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}/filespath, recursive, atDirectory listing
GET, PUT /v1/volumes/{v}/files/content?path=raw bytes on PUTRead or write one file
GET /v1/volumes/{v}/files/stat?path=atOne entry
POST /v1/volumes/{v}/files/copy, files/move{src, dst, overwrite}New commit
DELETE /v1/volumes/{v}/files?path=recursiveRemove
GET /v1/volumes/{v}/mountsLive mounts
POST /v1/volumes/{v}/commit{mount_id}Commit every writable mount now
GET /v1/volumes/{v}/waitcommit, timeoutWait until mounts show a commit; max 50 s, 408 on timeout
GET /v1/sandboxes/{id}/volumesThe 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=falseUnmount; commits first unless told not to
POST /v1/sandboxes/{id}/volumes/{mount}/commit{wait}Commit the mount now
POST /v1/sandboxes/{id}/volumes/{mount}/pullMove the mount to the volume's head

{mount} is the URL-encoded mount path, such as %2Fdata.

bash
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.

FieldTypeDefaultDescription
namestringrequiredLowercase, starts with a letter
valuestringrequiredThe credential
allowstring[]requiredExact hostnames the value may reach
headersstring[]requiredRequest headers the value is written into
on_echostringblockblock or redact when a response echoes the value
descriptionstringnoneWhat the credential is for
bash
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>).

EndpointDescription
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}/secretsRead 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.

FieldTypeDefaultDescription
urlstringrequiredEndpoint; private addresses are refused
eventsstring[]["*"]Events to deliver
secretstringgeneratedHMAC key, at least 16 chars
bash
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.

bash
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.

EndpointDescription
GET /v1/webhooksList; secrets are never returned
PATCH /v1/webhooks/{id}{enabled, url, events}
DELETE /v1/webhooks/{id}Delete
POST /v1/webhooks/{id}/testSend a ping; 202 {delivery_id}
GET /v1/webhooks/{id}/deliveries?status=pending|success|failed|dead_letter
POST /v1/webhooks/{id}/deliveries/{dlvId}/retryRequeue 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.

FieldTypeDefaultDescription
namestringrequiredUp to 120 chars
scopesstring[]["*"]See API key scopes
expires_atstringneverISO 8601 time in the future
bash
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.

EndpointDescription
GET /v1/auth/api-keysKeys in the active organization, without secrets
DELETE /v1/auth/api-keys/{id}Revoke now
GET /v1/auth/scopesThe scope model

Organizations and members ​

EndpointDescription
GET /v1/auth/meCaller, user, active organization, all organizations, feature flags
GET /v1/auth/orgsOrganizations 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/quotaEffective limits and usage
GET /v1/auth/orgs/membersMembers
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/invitesInvites 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}/previewNo auth
POST /v1/auth/orgs/invites/accept{code}, the invite id
POST /v1/auth/orgs/invites/{id}/declineDecline
GET /v1/auth/orgs/invites/receivedInvites addressed to you
POST /v1/auth/orgs/leaveLeave 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.

QueryDescription
qFree-text match
actionAction prefix, such as sandbox.
actorActor user id
statusok, denied or error
since, untilTime range
limit, offsetSee Pagination
bash
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.