HTTP API

Everything Pando does, it does through this API. The console, the CLI and the MCP server are clients of it and none of them has a capability it lacks (R-261).

Base path: /api/v1. Times are RFC 3339 in UTC. IDs are prefixed and opaque — app_01HQ8…, spec_…, tok_….

Authenticating#

Token — Authorization: Bearer <token>

A token minted from the console or with pando token create. A delegated token acts as the person who made it and is bounded by their live grants (R-058, R-059). A service token is its own principal and holds only what has been shared with it (R-060).

Session — POST /api/v1/sessions, then the session cookie

How the console authenticates. Cookies in Pando's namespace never reach an app (R-173).

Endpoints#

The verb column is the authorization verb the endpoint asks for. Blank means it asks for nothing beyond being signed in. There is no implication graph: holding one verb says nothing about another (R-082).

Session#

Endpoint Verb What it does
POST /api/v1/sessions Sign in with a username and password. Sets the session cookie.
DELETE /api/v1/sessions Sign out, ending this session.
GET /api/v1/setup Whether this installation is waiting for its first administrator (needed). Public.
POST /api/v1/setup Set up a new installation: the first account (username, display_name, password), made an administrator, and signed in. Public, and refused once any account exists (R-046).
GET /api/v1/me Who the caller is, and the install-level verbs they hold.
POST /api/v1/me/password Change your own password. Yours only, whatever verbs you hold.
GET /api/v1/me/apps The apps you can open, which is a different list from the apps you can administer (R-070, R-071). favorite marks the ones you have pinned, section_id the section you filed each under, and sections lists your sections.
PUT /api/v1/me/favorites/{appID} Mark an app you can open as a favorite, pinning it to the top of your launcher. Yours only; it grants nothing (R-341).
DELETE /api/v1/me/favorites/{appID} Unpin an app from your favorites.
POST /api/v1/me/sections Make a section in your launcher: a named, collapsible grouping of apps. Yours only; it grants nothing (R-342).
PATCH /api/v1/me/sections/{sectionID} Rename one of your sections.
DELETE /api/v1/me/sections/{sectionID} Delete one of your sections. Its apps go back to Your apps.
PUT /api/v1/me/sections/{sectionID}/apps/{appID} File an app you can open into one of your sections, moving it out of any other.
DELETE /api/v1/me/sections/{sectionID}/apps/{appID} Take an app out of a section, back to Your apps.

Tokens#

Endpoint Verb What it does
GET /api/v1/tokens Your own tokens. Never anyone else's.
POST /api/v1/tokens Mint a delegated token. It acts as you, is bounded by your live grants, and dies with your account (R-058, R-059). The secret is shown once.
GET /api/v1/tokens/service install.tokens.manage The installation's service tokens.
POST /api/v1/tokens/service install.tokens.manage Mint a service token: its own principal, holding only what is shared with it, outliving whoever created it (R-060). The secret is shown once.
DELETE /api/v1/tokens/{tokenID} Revoke a token. Yours; a service token with install.tokens.manage; anyone else's with install.users.manage.

Apps#

Endpoint Verb What it does
GET /api/v1/apps The apps you can administer.
POST /api/v1/apps app.create Create an app from a repository. Returns immediately in draft while detection runs.
GET /api/v1/apps/{appID} app.view One app: name, source, state and pinned spec.
PATCH /api/v1/apps/{appID} app.spec.edit Rename an app or change its source.
DELETE /api/v1/apps/{appID} app.delete Delete an app. With storage, backup=true keeps a final copy and force=true discards it; without either, the request is refused so the decision is taken rather than assumed (R-204, R-205).
GET /api/v1/apps/{appID}/icon The image on the app's launcher tile. Anyone who can open the app can load it; icon_updated_at on the app says whether there is one and when it changed (R-340).
PUT /api/v1/apps/{appID}/icon app.spec.edit Set the app's tile image. The body is the image itself — PNG, JPEG, WebP or GIF, at most 256 KB. SVG is refused (R-340).
DELETE /api/v1/apps/{appID}/icon app.spec.edit Remove the app's tile image, so the tile goes back to the map generated for it.
GET /api/v1/apps/{appID}/usage app.view What each part of the app is using now — CPU in thousandths of a core, memory and disk in bytes, and each mounted volume's size — beside its limits (0 is none; host_cpu_millis and host_memory_bytes say what none means). A reading, not a history (R-245, R-016). supported: false when the runtime cannot report it.
GET /api/v1/apps/{appID}/status app.view What the app is doing now: its state, and each part separately — running, restarting and how often, health, exit code — so a single crash-looping part is visible rather than averaged into one word.
POST /api/v1/apps/{appID}/start app.restart Set the app's desired state to running. The reconciler converges to it, so it survives a restart.
POST /api/v1/apps/{appID}/stop app.restart Set the app's desired state to stopped.
POST /api/v1/apps/{appID}/restart app.restart Restart the running workloads without changing anything.
GET /api/v1/apps/{appID}/logs app.logs.read The app's own output, from the runtime. tail sets how many lines; workload picks which part of the app, defaulting to the primary one.
GET /api/v1/apps/{appID}/exec app.exec A terminal in the running app, over a websocket. Refused when host policy has turned exec off, including for the owner (R-085).

Detection#

Endpoint Verb What it does
GET /api/v1/apps/{appID}/detection app.view What Pando worked out about the repository: the winning bid, its evidence, the runners-up and any outstanding questions.
POST /api/v1/apps/{appID}/detection/rerun app.spec.edit Run detection again, against the current commit.
GET /api/v1/apps/{appID}/detection/diff app.view What accepting the proposal would change about the running app.
POST /api/v1/apps/{appID}/detection/answers app.spec.edit Answer detection's questions. Each answer is a fact detection could not find, not a preference.
POST /api/v1/apps/{appID}/detection/accept app.spec.edit Accept the proposal, writing a spec revision and pinning it. values sets variables in the same step — {key, value, secret?, workload?} each; a secret goes to the secrets adapter and needs app.secrets.write. Accepting over a configured app needs confirm.
POST /api/v1/apps/{appID}/source app.spec.edit Upload a source archive for an app that has no reachable repository.

Configuration#

Endpoint Verb What it does
GET /api/v1/apps/{appID}/specs app.view Every spec revision, and which one is pinned. Revisions are append-only (R-152).
POST /api/v1/apps/{appID}/specs app.spec.edit Write a new spec revision. It does not deploy and does not become pinned.
GET /api/v1/apps/{appID}/specs/{rev} app.view One revision, in full.
POST /api/v1/apps/{appID}/specs/{rev}/pin app.spec.edit Pin a revision: what the reconciler converges to, and what the next deploy ships.
GET /api/v1/apps/{appID}/specs/{a}/diff/{b} app.view The classified difference between two revisions — what a deploy of it would restart, rebuild or leave alone.
GET /api/v1/apps/{appID}/export app.view The app's configuration as a document, with every secret redacted (R-194).
POST /api/v1/apps/{appID}/plan app.view What a deploy would do, and every reason it would refuse — before anything is created.
GET /api/v1/apps/{appID}/slots app.view The things the app says it needs, and what fills each one (R-130).
PUT /api/v1/apps/{appID}/slots/{key} app.spec.edit Fill a slot: provision one, bind to something already running, or set a value. Takes effect at the next deploy.

Storage#

Endpoint Verb What it does
GET /api/v1/apps/{appID}/volumes app.view The storage this app keeps. It outlives the app (R-204).
POST /api/v1/apps/{appID}/volumes app.spec.edit Declare a path the app keeps between deploys. Anything written outside one is discarded at the next deploy (R-201).
POST /api/v1/apps/{appID}/restore app.deploy Restore this app's storage from one of its backups.

Deploys#

Endpoint Verb What it does
GET /api/v1/apps/{appID}/deployments app.view Every deploy of this app, newest first.
POST /api/v1/apps/{appID}/deployments app.deploy Deploy. Returns 202 with a deployment ID; the build runs behind it. Retrying with the same idempotency key replays the first answer rather than deploying twice (R-262).
GET /api/v1/apps/{appID}/deployments/{depID} app.view One deploy: what it shipped, and how it ended.
GET /api/v1/apps/{appID}/deployments/{depID}/logs app.logs.read The deploy's output as server-sent events, flushed per line while it runs (R-170).
POST /api/v1/apps/{appID}/deployments/rollback app.deploy Deploy the last revision that ran successfully.

Secrets#

Endpoint Verb What it does
GET /api/v1/apps/{appID}/secrets app.view Which secrets this app has, and where each came from. Never their values.
PUT /api/v1/apps/{appID}/secrets/{key} app.secrets.write Set a secret. Rotating one recreates the workload rather than leaving it running with the old value (R-193).
DELETE /api/v1/apps/{appID}/secrets/{key} app.secrets.write Remove a secret.
GET /api/v1/apps/{appID}/secrets/{key}/value app.secrets.read Read one secret's value. Its own verb, separate from managing the app, and audited every time (R-083).

Security#

Endpoint Verb What it does
GET /api/v1/apps/{appID}/security app.view The app's security score, what it was taken from, and the findings behind it (R-310).
POST /api/v1/apps/{appID}/security/scan app.deploy Scan the app now. A write, not a refresh: the score decides whether the next deploy is allowed (R-312, R-314).

Sharing#

Endpoint Verb What it does
GET /api/v1/apps/{appID}/grants app.view Who can reach this app, and who can administer it — two planes, listed separately (R-070, R-071).
POST /api/v1/apps/{appID}/grants app.grants.manage Share the app with a user, a group, a token, or with everyone. The anonymous grant is a real row, refused where host policy forbids it (R-075, R-076).
PATCH /api/v1/apps/{appID}/grants/{grantID} app.grants.manage Change the role a grant for managing the app carries (role_id), one update so the person is never left with nothing in between; or, on the grant to everyone, set passcode ("" removes it). A new passcode asks everyone let in by the old one again (R-075a).
GET /api/v1/apps/{appID}/principals app.grants.manage People and groups to share the app with, matching q (username, name or email; group name), at most 20 of each.
GET /api/v1/apps/{appID}/passcode The name of an app that asks for a passcode, for its passcode page. Public; not-found for any other app (R-075a).
POST /api/v1/apps/{appID}/passcode Enter an app's passcode (passcode). Right, and the browser is let in for a day by a cookie the app never sees; ten wrong tries in fifteen minutes and it waits. Public (R-075a).
DELETE /api/v1/apps/{appID}/grants/{grantID} app.grants.manage Take a grant away.

Identity#

Endpoint Verb What it does
GET /api/v1/users install.view The accounts on this installation.
POST /api/v1/users install.users.manage Create an account.
GET /api/v1/users/{userID} install.view One account. Your own needs no verb.
PATCH /api/v1/users/{userID} install.users.manage Change an account: any of username, display_name, email and status. Username and email only on a local account; a username only with this verb, even your own. Suspension is not deletion (R-049). Your own name and email need no verb.
DELETE /api/v1/users/{userID} install.users.manage Delete an account, with the destruction rules that follow from it (R-282).
PUT /api/v1/users/{userID}/role install.users.manage Give an account an installation role. Deliberately not a field on PATCH: changing someone's status and changing their power are different acts.
DELETE /api/v1/users/{userID}/role install.users.manage Take an installation role away. The last administrator cannot be demoted.
POST /api/v1/users/{userID}/password install.users.manage Reset another local account's password (password), ending every session it holds. must_change_password defaults to true: whoever set it hands it over, and its holder chooses their own at the next sign-in. Your own is POST /me/password.
POST /api/v1/passwords/generate install.users.manage A strong random password, 18 to 22 characters with upper and lower case, digits and symbols, for creating or resetting an account. Stores nothing.
GET /api/v1/users/{userID}/apps install.view The apps an account has something on: its role for managing each, directly or through a group, whether it can use each, and whether you can change that (can_manage). Only apps you can see are listed. Your own needs nothing.
GET /api/v1/groups install.view Groups, whether Pando's own or an identity adapter's (R-078).
POST /api/v1/groups install.users.manage Create a group.
PUT /api/v1/groups/{groupID}/members install.users.manage Set a group's members.
PUT /api/v1/groups/{groupID}/members/{userID} install.users.manage Add one account to a group. It then holds everything the group holds.
DELETE /api/v1/groups/{groupID}/members/{userID} install.users.manage Remove one account from a group. Refused when it would leave nobody who can manage accounts (R-088).
PUT /api/v1/groups/{groupID}/role install.users.manage Give a group an installation role (role_id), which everyone in it holds.
DELETE /api/v1/groups/{groupID}/role install.users.manage Take a group's installation role away. Refused when it would leave nobody who can manage accounts (R-088).
GET /api/v1/groups/{groupID}/apps install.view A group's app grants: the role everyone in it has on each app, whether they can open it, and whether you can change that (can_manage). Only apps you can see. Share an app with a group through POST /apps/{appID}/grants with principal_kind: group.
DELETE /api/v1/groups/{groupID} install.users.manage Delete a group. Everything shared with it goes with it: its members lose that access and keep anything given to them another way. Refused if it would leave nobody who can manage accounts (R-088).
GET /api/v1/roles install.view Roles, built in and custom. By default the ones granted across the installation; scope=app gives the ones granted on an app, and scope=all both. Built-in roles are immutable (R-081).
POST /api/v1/roles install.users.manage Compose a custom role from verbs (R-082).
DELETE /api/v1/roles/{roleID} install.users.manage Delete a custom role, and every grant of it: whoever held it loses what it allowed. Built-in roles cannot be deleted (R-081). Refused if it would leave nobody who can manage accounts (R-088).
GET /api/v1/verbs install.view Every verb, by scope, for composing a role. There is no implication graph: holding one says nothing about another (R-082).

Installation#

Endpoint Verb What it does
GET /api/v1/adapters install.view The adapters configured here and what they can currently do — live capabilities, not stored configuration. Names which credentials are set, never their values. pending_restart marks one saved since Pando started, which is not yet what runs; restart_needed says any is.
GET /api/v1/adapters/kinds install.view The kinds of adapter this build of Pando can run, and the settings each takes — which are credentials (write-only, stored encrypted), which are required, and the default each takes when left empty or an example.
POST /api/v1/restart install.adapters.manage Restart Pando: finish the requests in flight, then start again, loading the adapters and the configuration file afresh. Apps behind Pando are unreachable for the seconds it takes. Environment variables are not re-read. Returns before the restart; started_at on GET /api/v1/adapters changes once it is back.
POST /api/v1/adapters install.adapters.manage Configure an adapter. Settings go in config; credentials such as an API key go in credentials, which is write-only and stored encrypted.
GET /api/v1/capacity install.view What the host has, and what is committed to apps (R-242).
GET /api/v1/policy install.view Host policy. Reading the rules you work under is not the same privilege as changing them (R-274).
PUT /api/v1/policy install.policy.manage Replace host policy. Policy is a floor, never an override (R-272).
POST /api/v1/policy/preview install.policy.manage Which apps a candidate policy would block, before it is saved.
GET /api/v1/config install.view The configuration Pando started with (R-271): every non-secret setting, its value and where it came from — an environment variable, the config file, or the default — and the host policy fields fixed there, which cannot be changed through the API while they are set. Secrets are never listed.
GET /api/v1/audit install.audit.read The audit log, newest first. Filters combine: action (a prefix), principal_id (who did it, including through a token), principal_kind (user, token, system or anonymous), app_id, target_kind and target_id (what it was done to), involving (an ID that is the actor or the target — everything to do with one account), and since/until (RFC 3339; since inclusive, until exclusive). Pages with before. Append-only: no endpoint edits or deletes an event, and the database refuses it too (R-027).
GET /api/v1/backups install.backup.manage The backups this installation holds.
POST /api/v1/backups install.backup.manage Take a backup now.
POST /api/v1/backups/{backupID}/verify install.backup.manage Check a backup before it is needed, rather than at the moment of disaster (R-216).
POST /api/v1/backups/{backupID}/restore install.backup.manage Restore from a backup. Verified first: an incomplete one is refused rather than half-applied (R-215).

Reference#

Endpoint Verb What it does
GET /api/v1/reference This document: every endpoint, every CLI command, every MCP tool and every error code, built from the running binary.

Errors#

Every error crossing this boundary carries the same envelope: a stable machine code, a human message, an optional remedy, details and the request_id that finds the log line. Branch on the code; the message may be reworded.

Code HTTP Meaning
VALID_DANGLING_MOUNT 400 A workload mounts a volume the spec does not declare.
VALID_DANGLING_SLOT_REF 400 The spec refers to a slot it does not declare.
VALID_DEPENDENCY_CYCLE 400 The workloads depend on each other in a cycle.
VALID_ENV_AMBIGUOUS 400 An environment variable is set twice with different values.
VALID_INVALID 400 The request or spec is malformed.
VALID_PRIMARY_WORKLOAD 400 A spec must name exactly one primary workload.
AUTH_INVALID 401 The credential presented is not valid.
AUTH_REQUIRED 401 No credential was presented, or the session has expired.
AUTH_TOKEN_INVALID 401 The token is unknown, revoked or expired.
AUTH_TOKEN_ORPHANED 401 The token's owner was suspended or deleted, so the token no longer resolves to anyone (R-059).
PERM_DENIED 403 Authenticated, but not permitted to do this.
PERM_PASSCODE_REQUIRED 403 The app is shared with everyone who knows its passcode, and this request has not shown it. A browser is sent to the passcode page; entering it there lets the visitor in.
PERM_VERB_REQUIRED 403 The caller holds no grant carrying the verb this action needs.
POLICY_ANONYMOUS_GRANT_FORBIDDEN 403 Host policy does not allow apps to be shared with everyone (R-076).
POLICY_EXEC_DISABLED 403 Host policy has turned off terminal access, including for an app's owner (R-085).
POLICY_SOURCE_NOT_ALLOWED 403 Host policy does not allow apps from this source (R-092).
NOT_FOUND 404 No such object, or none the caller may see.
CAPACITY_NO_FREE_PORT 409 Port-mode routing has no free port in the configured range.
CAPACITY_WOULD_OVERSUBSCRIBE 409 Running this would commit more of the host than is left (R-242).
PLAN_ADAPTER_NOT_CONFIGURED 409 The spec names an adapter this installation does not have.
PLAN_CAPABILITY_UNSUPPORTED 409 The spec asks for something the chosen adapter does not do (R-254).
PLAN_COMPOSE_CONSTRUCT_REJECTED 409 The compose file uses a construct Pando will not translate (R-099).
PLAN_NO_ADAPTER_MEETS_POLICY 409 No configured adapter can satisfy this spec under host policy (R-024, R-114).
PLAN_SECURITY_BELOW_THRESHOLD 409 This installation requires a security score, and this app is below it or has never been scanned (R-314).
PLAN_SLOT_UNFILLED 409 A required dependency has nothing filling it, so the deploy would start an app that cannot connect (R-132).
STATE_APP_EXITED 409 The app started and then stopped, so the deploy has nothing to send traffic to.
STATE_BACKUP_DECISION_REQUIRED 409 The app has storage and the request did not say whether to keep a final backup of it (R-204, R-205).
STATE_INVALID 409 The object is in a state this action does not apply to.
BACKUP_DECRYPT_FAILED 422 The backup could not be decrypted with the key supplied.
BACKUP_INCOMPLETE 422 The backup is missing part of what it claims to hold, so it was not applied (R-215).
BUILD_FAILED 422 The build ran and did not succeed. Its log is the answer.
BUILD_LISTENS_ON_LOOPBACK 422 The built app listens only on 127.0.0.1 inside its container, where nothing outside it can reach it.
BUILD_TIMEOUT 422 The build exceeded the time allowed for it (R-119).
INTERNAL 500 Pando failed in a way it did not expect. The request ID finds the log line.
RATE_LIMITED 500 Too many attempts in a short time — at a passcode, for example. Wait a few minutes and try again.
ADAPTER_FAILED 502 The adapter was reached and failed.
ADAPTER_UNAVAILABLE 502 The adapter needed for this is not configured or not reachable.