saved.sh

REST API

One uniform surface, bearer-authenticated, versioned under /v1.

Everything the dashboard and the CLI do goes through this API. There is no private back channel and no second surface: they are clients of the same endpoints you get.

Base URL and auth

https://api.saved.sh/v1

Every request carries a bearer token. There is exactly one authentication header, whether the caller is a person or a machine:

curl https://api.saved.sh/v1/workspaces \
  -H "Authorization: Bearer $SAVED_API_KEY"
CallerCredential
A person, from the dashboard or the CLIA session token, obtained by signing in
A machine: CI, a script, the SDK, a workerAn API key, scoped to one workspace

Create a key with the least permission that does the job:

saved apikey create ci-backups --permissions backups:trigger,runs:read

A key belongs to one workspace and carries a fixed set of permissions. A call outside them is refused with 403 naming the permission that was missing, not a bare denial.

Conventions

  • IDs are UUIDs. Ours, not a vendor's.
  • Timestamps are RFC 3339, UTC.
  • Every mutation is audited, with the actor recorded.
  • Every creating endpoint checks a quota first and refuses with 409 quota_exceeded when one is reached.

Health

MethodPathReturns
GET/healthzLiveness
GET/readyzReadiness

These are the only unauthenticated endpoints, along with GET /v1/auth/cli/config, which bootstraps the CLI's device-code sign-in.

Workspaces, members and roles

MethodPath
POST GET/v1/workspaces
GET PATCH DELETE/v1/workspaces/{wid}
GET/v1/workspaces/{wid}/members
PATCH DELETE/v1/workspaces/{wid}/members/{id}
POST GET/v1/workspaces/{wid}/invitations
GET POST PATCH DELETE/v1/workspaces/{wid}/roles[/{slug}]
GET/v1/permissions
GET/v1/workspaces/{wid}/quotas

GET /v1/workspaces/{wid}/quotas returns the effective limits and current usage, as a complete set with defaults filled in. A limit of 0 means unlimited.

Workers and API keys

MethodPath
POST GET/v1/workers
GET DELETE/v1/workers/{id}
POST/v1/workers/{id}/rotate
POST GET/v1/api-keys
GET DELETE/v1/api-keys/{id}

Backups

A backup is created in two steps. POST reserves the name, the kind and a UUID in draft; PATCH supplies the source and configuration and moves it to active.

MethodPathPermission
POST/v1/workspaces/{wid}/backupsbackups:write
GET/v1/workspaces/{wid}/backupsbackups:read
GET/v1/backups/{bid}backups:read
PATCH/v1/backups/{bid}backups:write
POST/v1/backups/{bid}/pause, /resumebackups:write
POST/v1/backups/{bid}/triggerbackups:write
DELETE/v1/backups/{bid}backups:write
// POST /v1/workspaces/{wid}/backups  →  201
{ "name": "prod-db", "kind": "local" }

// response
{ "id": "8f14e45f-...", "name": "prod-db", "state": "draft", "kind": "local" }

Kind is immutable. It decides who holds your source credentials, so changing it would silently move a secret across a trust boundary. PATCH has no kind field. The name is not write-once and may be changed.

Runs and artifacts

MethodPathPermission
POST/v1/runsartifacts:upload
POST/v1/runs/{run_id}/upload-urlartifacts:upload
POST/v1/runs/{run_id}/confirmartifacts:confirm
GET/v1/runs/{run_id}backups:read
GET/v1/workspaces/{wid}/artifactsartifacts:read
POST/v1/artifacts/{aid}/download-urlartifacts:download
DELETE/v1/artifacts/{aid}artifacts:delete

Uploads and downloads never pass through this API. You ask for a presigned URL and move the bytes directly to and from object storage, so the payload is not relayed by us and large artifacts are not bounded by an HTTP request.

Only manual backups call POST /v1/runs. A local or cloud worker already holds a run id from the scheduler and starts at upload-url.

Errors

Failures return a JSON body with a stable machine-readable code. Read the code, not the prose.

{ "code": "quota_exceeded", "message": "backups_per_workspace limit reached (5)" }
StatusMeaning
400Malformed request
401Missing or invalid credential
403Authenticated, but lacking the required permission
404No such object, or it belongs to another workspace
409Conflict: a duplicate name, or quota_exceeded
429Rate limited

A 404 deliberately covers "exists, but not yours". Distinguishing the two would leak whether an id exists in someone else's workspace.

Next

On this page