---
seo:
  description: >-
    Control plane API for pols.so developer sandboxes: full Ubuntu 24.04 VMs
    with root, systemd and Docker.
sidebar:
  label: Overview
title: pols.so API
---
Control plane API for pols.so developer sandboxes: full Ubuntu 24.04 VMs
with root, systemd and Docker.

This file is the source of truth. The Go server interface and client
(`pkg/api`), the TypeScript client (`clients/typescript`) and the Python
client's types (`clients/python/src/pols/_schema.py`) are generated from
it with `make generate`.

Authentication: `/v1` endpoints require an org-scoped API key as a
bearer token (`Authorization: Bearer pols_...`), except the public
`/v1/openapi.yaml`. The CDP WebSocket endpoint also accepts the connection
token described under `GET /v1/sandboxes/{sandbox}/browser/cdp`.

Roles: an org has users, each an `owner`, `admin` or `member` (see the
`Role` schema), invited by email on the website
(`https://my.pols.so/account/members`). Every user makes their own API
keys there, and a key acts with the current role of its user: changing
the role changes what the key may do, and removing the user from the org
revokes their keys. Every role may use sandboxes, templates, SSH keys
and the vault's names through the API; `GET /v1/api-keys` lists a
member's own keys only. `GET /v1/org` says the role of the calling key.

Lifecycle calls are asynchronous. They record the desired state and return
at once; a reconciler drives the runtime to it. Poll `GET
/v1/sandboxes/{sandbox}` until `status` equals `desired_state`, or until a
standby sandbox is stably stopped with `status=stopped` and
`desired_state=standby` (or is `error` unless deletion is pending).
Conflicting stop/resume requests return 409 until the current transition
finishes.

Wherever a path takes `{sandbox}` or `{template}`, either the ID
(`sbx_...`, `tpl_...`) or the org-unique name is accepted.

Computer use: every sandbox has a 1920x1080 X11 desktop and Google
Chrome. `/computer/*` takes screenshots of the whole desktop and drives
its mouse and keyboard; coordinates are pixels in that 1920x1080 space,
with (0, 0) at the top left. `/browser/cdp` connects Playwright or
Puppeteer to the sandbox's Chrome over the Chrome DevTools Protocol,
proxied by the control plane; Chrome's debugging port itself only
listens inside the sandbox.

Edge: the edge serves sandbox content on its own domain (the sandbox
domain, `on.pols.so` in production), never on the API's or the site's
domain. A published port is at
`https://<sandbox>-<port>.<sandbox domain>`, where `<sandbox>` is the
sandbox's `host_label` (a generated name with a random suffix, like
`braveotterk3f9`) or the sandbox ID with `_` replaced by `-`; the
desktop is at `<sandbox>-desktop` and the CDP endpoint at
`<sandbox>-cdp`. Both forms reach the same sandbox with the same login
links and tokens. A name given to a sandbox is never part of an
address, and the host label never changes, a rename included; no host
label is ever given to another sandbox. The VM's own hostname stays the
ID form (`sbx-...`). An unknown sandbox, a port that is not published
and a private port without a valid login link or session all get the
same 404 page. Ports are
private unless published as public: open them with a login link from
`POST /v1/sandboxes/{sandbox}/ports/{port}/link`, whose token the edge
exchanges for a cookie scoped to that one host. The desktop takes no
cookie: its link from `POST /v1/sandboxes/{sandbox}/desktop` carries a
token in the URL fragment, which the edge's desktop page presents when
it connects. `ssh <sandbox ID>@<gateway>`
reaches a running sandbox with any key registered under `/v1/ssh-keys`
by a user who is still an active user of the org.

Standby: a running sandbox that nothing uses for the deployment's
standby period (15 minutes by default) while its CPU and outbound
traffic stay low goes
to status `standby`: its VM is frozen with its memory kept, so
it uses no CPU. On the standard engine it is billed for the full RAM
its size reserves; on Cloud Hypervisor (an operator opt-in per org)
its memory is written to disk and billed as disk instead, with no RAM
charge (`GET /v1/usage`). Its disk keeps being billed. Use means a call on
its contents (exec, files, computer use, CDP, a desktop link) or SSH and HTTP through the
edge. Any of those wakes it, and so do resuming, forking and saving it
as a template: the call waits until the sandbox runs again, usually
within a second, and answers 503 `waking` if that takes longer than the
deployment allows. Reading it (`GET`) does not wake it. A sandbox in
standby keeps its place among the org's running sandboxes, so waking it
never fails on that quota; its standby time does not count against the
monthly sandbox-hours. While billing is off for the org, once those are
used up it cannot wake (403 `quota_exceeded`); once billing is on and
its credit is used up, it cannot wake (402 `insufficient_credit`).
Either way it is stopped with the running ones. If its VM stopped meanwhile (a host reboot), it
shows status `stopped` with desired state `standby`, gives up its place
and is billed for nothing; the next use boots it like a resume, with
the running quota checked, except a fork or template save, which copies
its disk without booting it.

Vault: each org has a vault of passwords and environment variables,
encrypted at rest. Its owners and admins add, replace and delete entries
on the website (`https://my.pols.so/account`); API keys can only list their names
(`GET /v1/vault`) and name them in `secrets` when creating or forking a
sandbox, which gets each one as an environment variable of that name.
No endpoint returns a value.

Webhooks: the org's owners and admins register HTTPS endpoints on the website
(`https://my.pols.so/account`) and pick the events each one gets:
`sandbox.ready`, `sandbox.stopped`, `sandbox.error` and
`sandbox.expired`. API keys can only list endpoints and their delivery
log (`GET /v1/webhooks` and
`GET /v1/webhooks/{webhook}/deliveries`). Each delivery is a POST of a
`WebhookEvent` JSON body with a
`Pols-Signature: t=<unix seconds>,v1=<hex>` header:
the HMAC-SHA256, keyed with the endpoint's secret (`whsec_...`, shown
once when it is created), of the timestamp, a `.` and the raw body.
Check it in constant time against the raw body before parsing it,
refuse timestamps more than 5 minutes from your clock, and process
each event `id` once: a delivery is retried until an endpoint answers
2xx within 10 seconds, with backoff for about a day, so the same event
can arrive more than once and out of order.

Billing: `GET /v1/billing` and `PUT /v1/billing` read and set who the
org's invoices are made out to, as do the billing page on the website
(`https://my.pols.so/billing`); only owners' and admins' keys may (403
`forbidden` otherwise). A business's EU or Northern Ireland VAT ID is
checked against VIES when it is saved. When VIES does not answer,
the details are saved anyway with the VAT ID `unverified`, and it is
checked again later. The details and VIES's answer decide how VAT
applies to the org's top-ups and subscription (`TaxTreatment`), and
each paid top-up and subscription month gets an invoice
(`GET /v1/invoices`), a PDF kept for 10 years. The subscription
(`/v1/billing/subscription`) costs EUR 20 a month and includes EUR 25
of usage each month, of which at most one month's rolls over into the
next.

Limits: `/v1` calls are rate limited per client address and per org,
sandbox and template lifecycle calls more tightly, and an org may have
only so many exec, file, computer and CDP calls in progress at once.
Over a limit the API answers 429 `rate_limited` with a `Retry-After`
header. A request body that stalls is answered with 408 `timeout`.

Errors and headers: a failure of the sandbox host is answered with 502
`runtime_error` and a fixed message; the details are only in the
server's log. A `{sandbox}` or `{template}` that is neither a well-formed
ID nor a name is 404 `not_found`. Names and descriptions must be UTF-8
text without control characters other than tab, and exec arguments
and paths must not contain NUL. Every response carries
`X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`,
`Content-Security-Policy: default-src 'none'; frame-ancestors 'none'`
and `Cache-Control: no-store`, except that `/v1/openapi.yaml` may be
cached.

Version 0.1.0

Base URL: `https://api.pols.so`

## org

- [`GET /v1/org`](/docs/api/org/get-org/) — The org the API key belongs to, with its quotas, current consumption and the key's role.
- [`GET /v1/org/stats`](/docs/api/org/get-org-stats/) — CPU, memory and disk use summed over the org's running sandboxes.

## api-keys

- [`GET /v1/api-keys`](/docs/api/api-keys/list-api-keys/) — List the org's API keys (secrets are never returned).
- [`POST /v1/api-keys`](/docs/api/api-keys/create-api-key/) — Refused; create API keys on the website's account page. Deprecated.
- [`DELETE /v1/api-keys/{key}`](/docs/api/api-keys/revoke-api-key/) — Refused; revoke API keys on the website's account page. Deprecated.

## sandboxes

- [`GET /v1/sandboxes`](/docs/api/sandboxes/list-sandboxes/) — List sandboxes.
- [`POST /v1/sandboxes`](/docs/api/sandboxes/create-sandbox/) — Create a sandbox from a template and start it.
- [`GET /v1/sandboxes/{sandbox}`](/docs/api/sandboxes/get-sandbox/) — Get a sandbox.
- [`DELETE /v1/sandboxes/{sandbox}`](/docs/api/sandboxes/delete-sandbox/) — Delete a sandbox and its disk.
- [`PATCH /v1/sandboxes/{sandbox}`](/docs/api/sandboxes/update-sandbox/) — Rename a sandbox.
- [`GET /v1/sandboxes/{sandbox}/stats`](/docs/api/sandboxes/get-sandbox-stats/) — A sandbox's current CPU, memory and disk use and its recent history.
- [`POST /v1/sandboxes/{sandbox}/stop`](/docs/api/sandboxes/stop-sandbox/) — Stop a sandbox (snapshot, then free; stopped time is not metered).
- [`POST /v1/sandboxes/{sandbox}/resume`](/docs/api/sandboxes/resume-sandbox/) — Resume a stopped sandbox, or wake one in standby.
- [`POST /v1/sandboxes/{sandbox}/fork`](/docs/api/sandboxes/fork-sandbox/) — Fork a sandbox into a new running sandbox with a copy of its disk.

## timeline

- [`GET /v1/sandboxes/{sandbox}/events`](/docs/api/timeline/list-sandbox-events/) — The sandbox's timeline, newest first.
- [`PUT /v1/sandboxes/{sandbox}/timeline`](/docs/api/timeline/set-sandbox-timeline-settings/) — Set the sandbox's timeline content-capture setting.
- [`GET /v1/sandboxes/{sandbox}/events/{event}/thumbnail`](/docs/api/timeline/get-sandbox-event-thumbnail/) — The thumbnail of a screenshot event.

## exec

- [`POST /v1/sandboxes/{sandbox}/exec`](/docs/api/exec/exec-sandbox/) — Run a command in a running sandbox.

## files

- [`GET /v1/sandboxes/{sandbox}/files`](/docs/api/files/read-sandbox-file/) — Read a file from a running sandbox.
- [`PUT /v1/sandboxes/{sandbox}/files`](/docs/api/files/write-sandbox-file/) — Create or overwrite a file in a running sandbox.

## computer

- [`GET /v1/sandboxes/{sandbox}/computer/screenshot`](/docs/api/computer/get-computer-screenshot/) — Take a PNG screenshot of the sandbox's whole 1920x1080 desktop.
- [`POST /v1/sandboxes/{sandbox}/computer/actions`](/docs/api/computer/run-computer-action/) — Click, double-click, drag, type, press keys or scroll on the sandbox's desktop.
- [`GET /v1/sandboxes/{sandbox}/desktop/control`](/docs/api/computer/get-desktop-control/) — Who has control of the sandbox's desktop.

## browser

- [`GET /v1/sandboxes/{sandbox}/browser/cdp`](/docs/api/browser/connect-cdp/) — Connect to the sandbox's Chrome DevTools Protocol endpoint (WebSocket).
- [`POST /v1/sandboxes/{sandbox}/browser/cdp`](/docs/api/browser/create-cdp-connection/) — Get a short-lived WebSocket URL for the sandbox's Chrome DevTools Protocol endpoint.

## edge

- [`GET /v1/sandboxes/{sandbox}/ports`](/docs/api/edge/list-ports/) — List the sandbox's published ports.
- [`PUT /v1/sandboxes/{sandbox}/ports/{port}`](/docs/api/edge/publish-port/) — Publish a sandbox port over HTTPS, or change its visibility.
- [`DELETE /v1/sandboxes/{sandbox}/ports/{port}`](/docs/api/edge/unpublish-port/) — Stop publishing a port.
- [`POST /v1/sandboxes/{sandbox}/ports/{port}/link`](/docs/api/edge/create-port-link/) — Get a short-lived login link for a published port.
- [`POST /v1/sandboxes/{sandbox}/desktop`](/docs/api/edge/create-desktop-link/) — Get a link to the sandbox's desktop in the browser.
- [`GET /v1/sandboxes/{sandbox}/ssh`](/docs/api/edge/get-ssh-access/) — How to reach the sandbox over SSH through the gateway.

## ssh-keys

- [`GET /v1/ssh-keys`](/docs/api/ssh-keys/list-ssh-keys/) — List the org's SSH public keys.
- [`POST /v1/ssh-keys`](/docs/api/ssh-keys/create-ssh-key/) — Register an SSH public key for the org's sandboxes.
- [`DELETE /v1/ssh-keys/{key}`](/docs/api/ssh-keys/delete-ssh-key/) — Remove an SSH public key.

## vault

- [`GET /v1/vault`](/docs/api/vault/list-vault-entries/) — List the org's vault entries (values are never returned).

## webhooks

- [`GET /v1/webhooks`](/docs/api/webhooks/list-webhooks/) — List the org's webhook endpoints (secrets are never returned).
- [`POST /v1/webhooks`](/docs/api/webhooks/create-webhook/) — Refused; create webhook endpoints on the website's account page. Deprecated.
- [`DELETE /v1/webhooks/{webhook}`](/docs/api/webhooks/delete-webhook/) — Refused; delete webhook endpoints on the website's account page. Deprecated.
- [`GET /v1/webhooks/{webhook}/deliveries`](/docs/api/webhooks/list-webhook-deliveries/) — The endpoint's recent deliveries and their outcome.

## billing

- [`GET /v1/billing`](/docs/api/billing/get-billing/) — The org's billing details and what VIES said about its VAT ID.
- [`PUT /v1/billing`](/docs/api/billing/put-billing-details/) — Set who the org's invoices are made out to.
- [`GET /v1/balance`](/docs/api/billing/get-balance/) — The org's credit balance and the credit it was granted.
- [`GET /v1/billing/topups`](/docs/api/billing/list-top-ups/) — The org's latest top-ups of its prepaid credit.
- [`POST /v1/billing/topups`](/docs/api/billing/create-top-up/) — Start a top-up of the org's prepaid credit, paid on Mollie's checkout.
- [`GET /v1/billing/topups/{topup}`](/docs/api/billing/get-top-up/) — A top-up and where its payment stands.
- [`GET /v1/billing/subscription`](/docs/api/billing/get-subscription/) — The org's subscription and its latest payments.
- [`POST /v1/billing/subscription`](/docs/api/billing/start-subscription/) — Start the monthly subscription, its first payment on Mollie's checkout.
- [`DELETE /v1/billing/subscription`](/docs/api/billing/cancel-subscription/) — Cancel the subscription at the end of the period paid for.
- [`POST /v1/billing/subscription/payment-method`](/docs/api/billing/change-subscription-payment-method/) — Set up a new payment method for the subscription, on Mollie's checkout.
- [`GET /v1/invoices`](/docs/api/billing/list-invoices/) — The org's invoices.
- [`GET /v1/invoices/{invoice}`](/docs/api/billing/get-invoice/) — An invoice.
- [`GET /v1/invoices/{invoice}/pdf`](/docs/api/billing/get-invoice-pdf/) — An invoice's PDF, the document itself.

## templates

- [`GET /v1/templates`](/docs/api/templates/list-templates/) — List system templates and the org's own templates.
- [`POST /v1/templates`](/docs/api/templates/create-template/) — Save a sandbox's disk as a named template.
- [`GET /v1/templates/{template}`](/docs/api/templates/get-template/) — Get a template.
- [`DELETE /v1/templates/{template}`](/docs/api/templates/delete-template/) — Delete one of the org's templates.

## usage

- [`GET /v1/usage`](/docs/api/usage/get-usage/) — Priced CPU, RAM and disk usage.

## system

- [`GET /healthz`](/docs/api/system/get-health/) — Liveness check.
