REST API
One uniform surface, bearer-authenticated, versioned under /v1.
View as MarkdownEverything 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.
https://api.saved.sh/v1Authentication
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"| Caller | Credential |
|---|---|
| A person, from the dashboard or the CLI | A session token, obtained by signing in |
| A machine: CI, a script | An API key, scoped to one workspace |
| A worker | Its own key, carrying two permissions |
sctl apikey create ci-backups --perms backups:read,backups:writeA call outside a credential's permissions is refused with 403 naming the permission that
was missing, rather than a bare denial. The catalog is public at GET /v1/permissions, so
naming it leaks nothing and makes a least-privilege key debuggable.
See Authentication.
Conventions
- IDs are UUIDs. Ours, not a vendor's.
- Timestamps are RFC 3339, UTC.
- Request bodies must be
application/json, and unknown fields are rejected. - Every response carries
X-Request-ID, echoed into error bodies. Quote it when asking for help. - Every mutation is audited, with the actor recorded.
- Creating endpoints check a quota first and refuse with
409 quota_exceeded.
Lists
Collections return an envelope, never a bare array:
{
"data": [ ... ],
"next_cursor": "018f4d1b-..."
}| Parameter | Meaning |
|---|---|
limit | 1 to 200. Defaults to 50 |
cursor | The next_cursor from the previous page |
next_cursor is null on the last page.
Endpoints
Unauthenticated
| Method | Path | Returns |
|---|---|---|
GET | /healthz | Liveness |
GET | /readyz | Readiness |
GET | /v1/auth/cli/config | Bootstraps the CLI's device-code sign-in |
Everything below requires a bearer token.
Session
| Method | Path | Permission |
|---|---|---|
GET | /v1/auth/cli/whoami | Authenticated |
GET | /v1/permissions | Authenticated |
GET | /v1/auth/worker/whoami | artifacts:confirm |
GET | /v1/auth/worker/config | artifacts:confirm |
Workspaces, members and roles
| Method | Path | Permission |
|---|---|---|
POST GET | /v1/workspaces | Authenticated human |
GET | /v1/workspaces/{wid} | workspaces:read |
PATCH DELETE | /v1/workspaces/{wid} | workspaces:write |
GET | /v1/workspaces/{wid}/members | members:read |
PATCH DELETE | /v1/workspaces/{wid}/members/{membershipID} | members:write |
POST GET | /v1/workspaces/{wid}/invitations | members:invite / members:read |
DELETE | /v1/workspaces/{wid}/invitations/{id} | members:invite |
POST | /v1/workspaces/{wid}/invitations/{id}/resend | members:invite |
GET | /v1/workspaces/{wid}/roles | roles:read |
POST | /v1/workspaces/{wid}/roles | roles:write |
PATCH DELETE | /v1/workspaces/{wid}/roles/{slug} | roles:write |
GET | /v1/workspaces/{wid}/quotas | workspaces:read |
Workers and API keys
| Method | Path | Permission |
|---|---|---|
POST | /v1/workers | workers:write |
GET | /v1/workers | workers:read |
GET | /v1/workers/{id} | workers:read |
POST | /v1/workers/{id}/rotate | workers:write |
DELETE | /v1/workers/{id} | workers:revoke |
POST | /v1/api-keys | api-keys:write |
GET | /v1/api-keys[/{id}] | api-keys:read |
DELETE | /v1/api-keys/{id} | api-keys:revoke |
Backups
| Method | Path | Permission |
|---|---|---|
POST | /v1/workspaces/{wid}/backups | backups:write |
GET | /v1/workspaces/{wid}/backups | backups:read |
GET | /v1/backups/{bid} | backups:read |
PATCH | /v1/backups/{bid} | backups:write |
POST | /v1/backups/{bid}/pause, /resume, /trigger | backups:write |
DELETE | /v1/backups/{bid} | backups:write |
Runs and artifacts
| Method | Path | Permission |
|---|---|---|
POST | /v1/runs | artifacts:upload |
POST | /v1/runs/{rid}/upload-url | artifacts:upload |
POST | /v1/runs/{rid}/upload-parts, /upload-complete, /upload-abort | artifacts:upload |
POST | /v1/runs/{rid}/confirm | artifacts:confirm |
GET | /v1/backups/{bid}/runs | backups:read |
GET | /v1/runs/{rid} | backups:read |
GET | /v1/workspaces/{wid}/artifacts | artifacts:read |
GET | /v1/backups/{bid}/artifacts | artifacts:read |
GET | /v1/artifacts/{aid} | artifacts:read |
POST | /v1/artifacts/{aid}/download-url | artifacts:download |
DELETE | /v1/artifacts/{aid} | artifacts:delete |
Destinations and connections
| Method | Path | Permission |
|---|---|---|
POST | /v1/destinations | destinations:write |
GET | /v1/destinations[/{id}] | destinations:read |
PATCH | /v1/destinations/{id} | destinations:write |
POST | /v1/destinations/{id}/verify | destinations:write |
DELETE | /v1/destinations/{id} | destinations:write |
GET | /v1/connections/providers | connections:read |
POST | /v1/connections | connections:write |
GET | /v1/connections[/{id}] | connections:read |
DELETE | /v1/connections/{id} | connections:write |
destinations:write and connections:write are human-only. A machine key cannot hold
them. See permissions.
Billing, usage and audit
| Method | Path | Permission |
|---|---|---|
GET POST | /v1/workspaces/{wid}/payment-methods | billing:read / billing:write |
POST | /v1/workspaces/{wid}/payment-methods/confirm | billing:write |
POST | /v1/workspaces/{wid}/payment-methods/{id}/default | billing:write |
DELETE | /v1/workspaces/{wid}/payment-methods/{id} | billing:write |
GET | /v1/workspaces/{wid}/invoices[/upcoming] | billing:read |
GET | /v1/workspaces/{wid}/credit | billing:read |
POST | /v1/workspaces/{wid}/coupons | billing:write |
GET | /v1/workspaces/{wid}/usage | billing:read |
GET | /v1/workspaces/{wid}/audit | audit:read |
billing:* is human-only.
Bulk data never passes through this API
Uploads and downloads use presigned URLs. You ask for one and move the bytes directly to or from object storage.
That is structural rather than a policy: the payload is not relayed by us, and a large artifact is not bounded by an HTTP request to our API.
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, nested under error.
{
"error": {
"code": "quota_exceeded",
"message": "this workspace already has 5 backups",
"request_id": "req_018f3c2a-..."
}
}Read the code, not the prose. See Errors for the full list.