API Keys
Generate scoped Bearer tokens for headless access — scripts, CI pipelines, and AI agents.
API keys are long-lived Bearer tokens for programmatic access to the Spunto API — for scripts, CI pipelines, or AI agents that need to operate the platform without a browser session. They're scoped to a single organization and to a specific set of permissions, and can be revoked independently of any user's login session.
Open Account Settings
From the dashboard, go to Account Settings and find the API keys card.
Pick an organization
A key is always scoped to one organization. If you belong to several, pick the one the key should act on.
Choose owner type
Personal keys act as you — they stop working if you lose membership in the org. Org (service account) keys belong to the organization itself and keep working even if the creator leaves; creating one requires the admin or owner role.
Select scopes
Pick the narrowest set of scopes the script or agent actually needs (see Scopes below).
Copy the token
The full token (spk_...) is shown exactly once, right after creation. Store it somewhere safe — Spunto only ever stores a hash of it, so it can't be shown again. If you lose it, revoke the key and create a new one.
Personal (ownerType: "user") | Org service account (ownerType: "org") | |
|---|---|---|
| Acts as | You | The organization |
| Who can create it | Any org member, for themselves | admin / owner only |
| Survives creator leaving the org | No — tied to your membership | Yes |
| Typical use | Personal scripts, your own CLI tooling | Long-running CI, an autonomous agent deployed in prod |
Scopes are coarse and resource-based, in the form resource:action. A request needs at least one matching scope (or the * wildcard) to pass — sessions created via the dashboard login (JWT/cookie) are unaffected and always have full access; scoping only applies to API keys.
| Scope | Grants |
|---|---|
* | Every scope below — full access |
projects:read | List/get projects, image builds, version history |
projects:write | Create/update/delete projects, trigger builds, restore versions |
workers:read | List/get workers, logs, stats, git status, ports |
workers:write | Spawn/stop/start/rebuild/delete workers |
workers:exec | Running commands (POST .../exec), terminal sessions and SSH tokens — shell access inside a worker |
deployments:read | List/get deployments and services, service logs, job runs |
deployments:write | Create/update/delete deployments and services, start/stop/deploy, run jobs |
deployments:access | Reach a private service's own URL without a browser session (see below) |
nodes:read | List/get worker nodes, inventory |
nodes:write | Create/delete nodes, rotate tokens, drain — infrastructure-level access |
A request is matched against GET → :read, every other HTTP method → :write, except terminal/SSH-token endpoints under /workers/..., which always require :exec regardless of method.
Send it as a standard Bearer token — exactly like a session JWT:
curl -H "Authorization: Bearer spk_..." \
https://spunto.net/api/orgs/{orgId}/projectscurl -X POST \
-H "Authorization: Bearer spk_..." \
-H "Content-Type: application/json" \
-d '{ "name": "ci-worker", "image": "..." }' \
https://spunto.net/api/orgs/{orgId}/projects/{projectId}/workersA request with a missing, malformed, revoked, or expired key returns 401 Unauthorized. A request with a valid key but a missing scope returns 403 Forbidden.
A service deployed with Access control: private asks a human for a Spunto login before it lets anything through. A script, a CI job or an agent has no browser to complete that flow — it sends an API key on the request to the service instead:
curl -H "X-Spunto-Token: spk_..." https://svc-{serviceId}.spunto.net/api/healthAuthorization: Bearer spk_... works too; X-Spunto-Token is the better default because it leaves Authorization free for the deployed app's own authentication. Either way, the Spunto credential is stripped before the request reaches your container — your app never sees the key. A bearer token that isn't a Spunto credential is forwarded untouched.
The key gets in when all three hold:
- it belongs to the organization that owns the deployment;
- it carries
deployments:access(or*) —deployments:readis deliberately not enough, it grants reading a service's configuration, not talking to the app; - for a personal key, its owner is on the service's allowlist — a key never reaches further than the human behind it. An org-owned key acts for the organization itself, so the scope is what authorizes it.
Anything else gets 403. Revoking the key closes the door within ~10 seconds (the proxy caches granted decisions for that long; denials are never cached).
GET /api/orgs/:orgId/api-keys List keys (token values are never returned, only a `tokenPrefix`)
POST /api/orgs/:orgId/api-keys Create a key — { name, ownerType, scopes } → { ...key, token }
DELETE /api/orgs/:orgId/api-keys/:keyId Revoke a keyThe full request/response schemas (including the ApiKey/ApiKeyCreated shapes) are in the interactive reference — look for the ApiKeys tag at /api/docs, or browse the raw spec at /openapi.json.
curl -X DELETE \
-H "Authorization: Bearer <session token, not an API key>" \
https://spunto.net/api/orgs/{orgId}/api-keys/{keyId}Revocation is immediate — the very next request made with that token returns 401. The key row isn't deleted, only marked revoked, so it stays visible (and auditable) in the list.
- No expiration support yet. Keys are valid until revoked — there's no "expires in 90 days" option in the dashboard or API today.
- No per-project/per-deployment restriction. A scope like
workers:writeapplies to every project in the org, not a single one — includingdeployments:access, which opens every private service the key's owner may reach, not one chosen service. - No rate limiting. A misbehaving script or agent loop using an API key is bound by the same limits as everything else — i.e. none yet.