API Reference

Explore the full Spunto REST API — all endpoints, request bodies, and response schemas.

The Spunto API is a REST API. All endpoints require a Bearer token (except public auth routes).

The full interactive API reference is available at:

https://spunto.net/api/docs

It's generated automatically from the OpenAPI 3.1 spec — always up to date.

All API requests (except /api/auth/*) require:

Authorization: Bearer <token>

Two kinds of token are accepted:

  • Session token — issued when you sign in with Google. The same token is set as an httpOnly session cookie by the auth flow — browser requests include it automatically. Carries full access (no scoping).
  • API key (spk_...) — a scoped, revocable token generated from the dashboard for headless access (scripts, CI, AI agents). See the API Keys page for how to create and use one.

https://spunto.net

All endpoints start with /api/.

PATCH means partial: a request body only touches the fields it names, and everything it leaves out is kept as-is. Renaming a project is {"name": "..."} — its repositories, features and extensions stay where they were.

Which means emptying something has to be said out loud:

  • a list is cleared by sending it explicitly empty — {"forwardPorts": []}
  • a free-text field is cleared by sending "" — {"description": ""} stores null (whitespace counts as empty). Omitting the key keeps the old value; it never means "erase it".

A project's shared secrets ride in the same body, and are partial in the same way — a list of changes, not the desired final state:

PATCH /api/orgs/:orgId/projects/:projectId
{
  "secrets": [
    { "name": "API_TOKEN", "value": "sk-..." },   // write (creates or replaces, by name)
    { "name": "OLD_TOKEN", "value": null }        // remove
  ]
}

Any name you don't mention keeps whatever it had. Values are write-only: they are never returned by any endpoint. Writing them is owner/admin only, and an API key needs the secrets:write scope on top of projects:write — the same gates as the dedicated /secrets routes, which remain available for one-off changes.

GET    /api/orgs              List organizations
POST   /api/orgs              Create organization
GET    /api/orgs/:orgId       Get organization details
GET    /api/orgs/:orgId/members  List members

GET    /api/orgs/:orgId/projects                         List projects
POST   /api/orgs/:orgId/projects                         Create project
GET    /api/orgs/:orgId/projects/:projectId              Get project
PATCH  /api/orgs/:orgId/projects/:projectId              Update project
DELETE /api/orgs/:orgId/projects/:projectId              Delete project
POST   /api/orgs/:orgId/projects/:projectId/build        Trigger pre-build on all nodes
GET    /api/orgs/:orgId/projects/:projectId/image-builds List build records

GET    /api/orgs/:orgId/projects/:projectId/workers                      List workers
POST   /api/orgs/:orgId/projects/:projectId/workers                      Spawn worker
GET    /api/orgs/:orgId/projects/:projectId/workers/:workerId            Get worker
POST   /api/orgs/:orgId/projects/:projectId/workers/:workerId/stop       Stop worker
POST   /api/orgs/:orgId/projects/:projectId/workers/:workerId/start      Start worker
DELETE /api/orgs/:orgId/projects/:projectId/workers/:workerId            Destroy worker
GET    /api/orgs/:orgId/projects/:projectId/workers/:workerId/logs       Docker logs
GET    /api/orgs/:orgId/projects/:projectId/workers/:workerId/stats      CPU + memory
GET    /api/orgs/:orgId/projects/:projectId/workers/:workerId/git-status Git status
GET    /api/orgs/:orgId/projects/:projectId/workers/:workerId/ports      Listening ports
POST   /api/orgs/:orgId/projects/:projectId/workers/:workerId/ssh-token  Generate SSH token

GET    /api/orgs/:orgId/nodes                     List nodes
POST   /api/orgs/:orgId/nodes                     Register node
GET    /api/orgs/:orgId/nodes/:nodeId             Get node
DELETE /api/orgs/:orgId/nodes/:nodeId             Delete node
POST   /api/orgs/:orgId/nodes/:nodeId/token       Rotate token
POST   /api/orgs/:orgId/nodes/:nodeId/drain       Drain node
GET    /api/orgs/:orgId/nodes/:nodeId/inventory   Live container inventory

GET    /api/orgs/:orgId/deployments                              List deployments
POST   /api/orgs/:orgId/deployments                              Create deployment
GET    /api/orgs/:orgId/deployments/:deploymentId                Get deployment
PATCH  /api/orgs/:orgId/deployments/:deploymentId                Update deployment
DELETE /api/orgs/:orgId/deployments/:deploymentId                Delete deployment
POST   /api/orgs/:orgId/deployments/:deploymentId/start          Start all services
POST   /api/orgs/:orgId/deployments/:deploymentId/stop           Stop all services
POST   /api/orgs/:orgId/deployments/:deploymentId/deploy         Deploy all services

GET    /api/orgs/:orgId/deployments/:deploymentId/services                    List services
POST   /api/orgs/:orgId/deployments/:deploymentId/services                    Add service
PATCH  /api/orgs/:orgId/deployments/:deploymentId/services/:serviceId         Update service
DELETE /api/orgs/:orgId/deployments/:deploymentId/services/:serviceId         Remove service
POST   /api/orgs/:orgId/deployments/:deploymentId/services/:serviceId/start   Start
POST   /api/orgs/:orgId/deployments/:deploymentId/services/:serviceId/stop    Stop
POST   /api/orgs/:orgId/deployments/:deploymentId/services/:serviceId/deploy  Deploy
POST   /api/orgs/:orgId/deployments/:deploymentId/services/:serviceId/verify-domain  Verify domain
GET    /api/orgs/:orgId/deployments/:deploymentId/services/:serviceId/logs    Service logs

GET    /api/orgs/:orgId/api-keys           List keys
POST   /api/orgs/:orgId/api-keys           Create a key (token shown once)
DELETE /api/orgs/:orgId/api-keys/:keyId    Revoke a key

Session-only — an API key cannot be used to manage other API keys. See API Keys.

GET    /api/users/me            Get current user (dotfilesRepo)
PATCH  /api/users/me            Update user settings
GET    /api/users/me/secrets    List secret names
POST   /api/users/me/secrets    Create / replace secret
DELETE /api/users/me/secrets/:secretId  Delete secret

Anything live is a WebSocket upgrade rather than a REST call. These endpoints are in the OpenAPI spec like the rest of the API — look for (WebSocket) in the summary; each one documents its query parameters and the exact shape of the frames it exchanges.

EndpointDescription
GET /api/workers/:id/terminal?mode=terminalAttach to a persistent terminal session in a workspace
GET /api/workers/:id/terminal?mode=logsFollow the workspace container's logs
GET /api/services/:id/terminalFollow a deployed service's logs
GET /api/services/:id/shellInteractive shell in a deployed service
GET /api/job-runs/:runId/logsDeploy / hook / cron run output, live or replayed
GET /api/image-builds/:id/logsImage build log, with the build's steps

Authenticate the handshake exactly like a REST call — Authorization: Bearer <token>, with a session token, or an API key scoped workers:exec on the workspace terminal. A browser can't set headers on a WebSocket, so from the browser the session cookie is used instead.

A workspace terminal is persistent: the program keeps running when you disconnect, and reattaching lands on it with its scrollback — which is what makes it usable from a phone.

GET    /api/orgs/:orgId/projects/:projectId/workers/:workerId/terminals        List sessions
POST   /api/orgs/:orgId/projects/:projectId/workers/:workerId/terminals        Create a session
DELETE /api/orgs/:orgId/projects/:projectId/workers/:workerId/terminals/:name  Kill a session

List the sessions, then attach to one by name. Closing the socket detaches; it doesn't kill it.

const ws = new WebSocket(`wss://spunto.net/api/workers/${workerId}/terminal?session=main`)
 
ws.onmessage = (e) => {
  const frame = JSON.parse(e.data)
  // "ready" once attached, then "output" frames — base64-encoded raw bytes.
  if (frame.type === "output") term.write(atob(frame.data))
}
 
ws.send(JSON.stringify({ type: "input", data: btoa("ls -la\n") }))
ws.send(JSON.stringify({ type: "resize", cols: 80, rows: 24 }))

When you want the last N lines rather than a stream, both logs have a plain REST form — GET …/workers/:workerId/logs?tail=200 and GET …/services/:serviceId/logs?tail=200, returning text/plain.

Not part of the API surface — they authenticate with infrastructure tokens, not yours.

EndpointDescription
GET /api/agent/connect?token=Node agent connection (node token)
GET /api/tunnel/relayProxy ↔ agent relay

Note

For the full interactive reference with request/response schemas and a built-in HTTP client, visit /api/docs.