Authentication
One bearer header, whether the caller is a person or a machine.
View as MarkdownAuthorization: Bearer <credential>There is one header and one code path. A human's session token and a machine's API key resolve to the same internal principal, so a shared endpoint runs one handler regardless of who called it.
Credentials
| Credential | Looks like | Obtained by |
|---|---|---|
| Session token | A JWT, three dot-separated parts | sctl login, or the dashboard |
| API key | An opaque string | sctl apikey create |
| Worker key | An opaque string | sctl worker provision |
The API distinguishes the two forms by shape: a credential with two dots is validated as a JWT, anything else as an opaque key. You never have to say which you are sending.
Getting a key
sctl apikey create ci-backups --perms backups:read,backups:writeThe secret is shown once and never stored by us. See API keys.
Bootstrapping without a credential
Three endpoints need no token:
| Method | Path | Purpose |
|---|---|---|
GET | /healthz | Liveness |
GET | /readyz | Readiness |
GET | /v1/auth/cli/config | The CLI's device-code parameters |
curl -fsS https://api.saved.sh/v1/auth/cli/config{
"authkit_domain": "auth.saved.sh",
"client_id": "client_01H...",
"scopes": ["openid", "profile", "email", "offline_access"],
"api_version": "v1"
}Who am I
curl -fsS https://api.saved.sh/v1/auth/cli/whoami \
-H "Authorization: Bearer $SAVED_API_KEY"{
"user_id": "018f3c2a-...",
"email": "you@example.com",
"workspace_id": "4b7e1d90-...",
"workos_org_id": "org_01H...",
"role": "admin",
"permissions": ["backups:read", "backups:write", "..."]
}This is the fastest way to debug a 403: the permissions array is exactly what will be
enforced.
role is display only. Authorization reads permissions and never the role, so a custom
role you create is enforced correctly without us knowing it exists.
The full catalog is public to any authenticated caller:
curl -fsS https://api.saved.sh/v1/permissions -H "Authorization: Bearer $SAVED_API_KEY"Workspace scoping
A credential belongs to exactly one workspace. There is no token that spans two, and no header that selects one.
- Session tokens are re-minted when you switch workspace.
- API keys and worker keys are bound to a workspace at creation and cannot be moved.
Many paths carry {workspaceID} anyway. It must match the credential's workspace; another
workspace's ID returns 403 wrong_workspace or 404, never data.
What a machine credential cannot do
| Refused | Why |
|---|---|
workers:* | A compromised key could mint worker credentials |
api-keys:* | One leaked key could mint more |
billing:* | The workspace's cards |
destinations:write, connections:write | Long-lived storage keys, and a consent screen a machine cannot complete |
artifacts:confirm on an API key | Worker-only |
POST /v1/workspaces | Requires a human principal |
Asking for one at key creation fails with 422. A key that acquired one another way has it
stripped on every validation.
Worker credentials
A worker key carries exactly artifacts:upload and artifacts:confirm, and authenticates
both to this API and to the orchestration hub.
curl -fsS https://api.saved.sh/v1/auth/worker/whoami \
-H "Authorization: Bearer $WORKER_TOKEN"{
"worker_id": "0f2c9a1e-...",
"name": "prod-worker-1",
"workspace_id": "4b7e1d90-...",
"namespace": "4b7e1d90-...",
"task_queue": "0f2c9a1e-...",
"hub_address": "hub.saved.sh:443"
}This is the only remote configuration a worker receives, and it carries no secrets.
Revocation
| Credential | Ends when |
|---|---|
| Session token | It expires, or the membership is removed |
| API key | sctl apikey revoke, within the validation cache TTL |
| Worker key | sctl worker rotate or delete, within the same TTL |
Revocation is not instantaneous. Validations are cached for about a minute, so a revoked credential may keep working briefly. Treat revocation as effective within a minute, and if a credential leaked, rotate whatever it could reach as well.
Failure modes
| Status | Code | Meaning |
|---|---|---|
401 | unauthenticated | No Authorization header, or an empty bearer value |
401 | unauthenticated | The credential is invalid, expired or revoked |
403 | forbidden | Valid credential, missing permission. The message names the slug |
403 | wrong_workspace | The credential belongs to another workspace |
403 | forbidden | A machine credential on a human-only route |
A 401 and a 403 mean genuinely different things here: the first is "I do not know who you
are", the second is "I know, and no".