---
title: "REST API"
description: "One uniform surface, bearer-authenticated, versioned under /v1."
url: "https://saved.sh/docs/api"
---

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.

```
https://api.saved.sh/v1
```

## Authentication [#authentication]

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

```bash
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   |

```bash
sctl apikey create ci-backups --perms backups:read,backups:write
```

<Callout>
  A 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.
</Callout>

See [Authentication](/docs/api/authentication).

## Conventions [#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 [#lists]

Collections return an envelope, never a bare array:

```json
{
  "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 [#endpoints]

### Unauthenticated [#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 [#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 [#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 [#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 [#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 [#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 [#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](/docs/security/permissions#machine-limits).

### Billing, usage and audit [#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 [#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.

<Callout type="warn">
  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`.
</Callout>

## Errors [#errors]

Failures return a JSON body with a stable machine-readable `code`, nested under `error`.

```json
{
  "error": {
    "code": "quota_exceeded",
    "message": "this workspace already has 5 backups",
    "request_id": "req_018f3c2a-..."
  }
}
```

**Read the `code`, not the prose.** See [Errors](/docs/api/errors) for the full list.

## Next [#next]

<Cards>
  <Card href="/docs/api/authentication" title="Authentication" description="Tokens, keys, and what each may hold." />

  <Card href="/docs/api/errors" title="Errors" description="Every code, and what to do about it." />

  <Card href="/docs/api/backups" title="Backups" description="The two-step create, and configuring." />

  <Card href="/docs/cli" title="CLI" description="The same surface from a terminal." />
</Cards>
