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.
https://api.pols.soorg
- GETThe org the API key belongs to, with its quotas, current consumption and the key's role
/v1/org - GETCPU, memory and disk use summed over the org's running sandboxes
/v1/org/stats
api-keys
- GETList the org's API keys (secrets are never returned)
/v1/api-keys - POSTRefused; create API keys on the website's account page
/v1/api-keys - DELETERefused; revoke API keys on the website's account page
/v1/api-keys/{key}
sandboxes
- GETList sandboxes
/v1/sandboxes - POSTCreate a sandbox from a template and start it
/v1/sandboxes - GETGet a sandbox
/v1/sandboxes/{sandbox} - DELETEDelete a sandbox and its disk
/v1/sandboxes/{sandbox} - PATCHRename a sandbox
/v1/sandboxes/{sandbox} - GETA sandbox's current CPU, memory and disk use and its recent history
/v1/sandboxes/{sandbox}/stats - POSTStop a sandbox (snapshot, then free; stopped time is not metered)
/v1/sandboxes/{sandbox}/stop - POSTResume a stopped sandbox, or wake one in standby
/v1/sandboxes/{sandbox}/resume - POSTFork a sandbox into a new running sandbox with a copy of its disk
/v1/sandboxes/{sandbox}/fork
timeline
- GETThe sandbox's timeline, newest first
/v1/sandboxes/{sandbox}/events - PUTSet the sandbox's timeline content-capture setting
/v1/sandboxes/{sandbox}/timeline - GETThe thumbnail of a screenshot event
/v1/sandboxes/{sandbox}/events/{event}/thumbnail
exec
files
- GETRead a file from a running sandbox
/v1/sandboxes/{sandbox}/files - PUTCreate or overwrite a file in a running sandbox
/v1/sandboxes/{sandbox}/files
computer
- GETTake a PNG screenshot of the sandbox's whole 1920x1080 desktop
/v1/sandboxes/{sandbox}/computer/screenshot - POSTClick, double-click, drag, type, press keys or scroll on the sandbox's desktop
/v1/sandboxes/{sandbox}/computer/actions - GETWho has control of the sandbox's desktop
/v1/sandboxes/{sandbox}/desktop/control
browser
- GETConnect to the sandbox's Chrome DevTools Protocol endpoint (WebSocket)
/v1/sandboxes/{sandbox}/browser/cdp - POSTGet a short-lived WebSocket URL for the sandbox's Chrome DevTools Protocol endpoint
/v1/sandboxes/{sandbox}/browser/cdp
edge
- GETList the sandbox's published ports
/v1/sandboxes/{sandbox}/ports - PUTPublish a sandbox port over HTTPS, or change its visibility
/v1/sandboxes/{sandbox}/ports/{port} - DELETEStop publishing a port
/v1/sandboxes/{sandbox}/ports/{port} - POSTGet a short-lived login link for a published port
/v1/sandboxes/{sandbox}/ports/{port}/link - POSTGet a link to the sandbox's desktop in the browser
/v1/sandboxes/{sandbox}/desktop - GETHow to reach the sandbox over SSH through the gateway
/v1/sandboxes/{sandbox}/ssh
ssh-keys
- GETList the org's SSH public keys
/v1/ssh-keys - POSTRegister an SSH public key for the org's sandboxes
/v1/ssh-keys - DELETERemove an SSH public key
/v1/ssh-keys/{key}
vault
webhooks
- GETList the org's webhook endpoints (secrets are never returned)
/v1/webhooks - POSTRefused; create webhook endpoints on the website's account page
/v1/webhooks - DELETERefused; delete webhook endpoints on the website's account page
/v1/webhooks/{webhook} - GETThe endpoint's recent deliveries and their outcome
/v1/webhooks/{webhook}/deliveries
billing
- GETThe org's billing details and what VIES said about its VAT ID
/v1/billing - PUTSet who the org's invoices are made out to
/v1/billing - GETThe org's credit balance and the credit it was granted
/v1/balance - GETThe org's latest top-ups of its prepaid credit
/v1/billing/topups - POSTStart a top-up of the org's prepaid credit, paid on Mollie's checkout
/v1/billing/topups - GETA top-up and where its payment stands
/v1/billing/topups/{topup} - GETThe org's subscription and its latest payments
/v1/billing/subscription - POSTStart the monthly subscription, its first payment on Mollie's checkout
/v1/billing/subscription - DELETECancel the subscription at the end of the period paid for
/v1/billing/subscription - POSTSet up a new payment method for the subscription, on Mollie's checkout
/v1/billing/subscription/payment-method - GETThe org's invoices
/v1/invoices - GETAn invoice
/v1/invoices/{invoice} - GETAn invoice's PDF, the document itself
/v1/invoices/{invoice}/pdf
templates
- GETList system templates and the org's own templates
/v1/templates - POSTSave a sandbox's disk as a named template
/v1/templates - GETGet a template
/v1/templates/{template} - DELETEDelete one of the org's templates
/v1/templates/{template}