# REST API

> Every endpoint of the StoryShelf REST API, with an interactive OpenAPI console.

Source: https://storyshelf.js.org/guides/api/

StoryShelf exposes a versioned REST API under `/api/v1`. JSON in, JSON out. HTML pages live at `/`; the API is deliberately separate (ADR 0011).

## OpenAPI specification

The full API is described by an [OpenAPI 3.0](https://spec.openapis.org/oas/v3.0.3) document — built from the router types themselves via `@hono/zod-openapi`, so the spec can’t drift from the implementation.

- **Live spec:** `GET /api/v1/openapi.json` on any running server
- **Interactive console:** `GET /api/v1/docs` (Swagger UI) on any running server
- **Static copy (this site):** `GET /openapi.json` — the build-time artifact generated by the website’s `prebuild` hook, served alongside the docs
- **Interactive reference (this site):** `GET /openapi/` — client-side Swagger UI loading `/openapi.json` (read-only, no try-it)

## Static copy

A build-time copy is also emitted for CI, codegen, and SDK tooling without a live server. The website’s `prebuild` hook runs an `openapi` script that generates it from the router source via `packages/app/scripts/generate-openapi.ts` (no package wiring — the script lives where `@storyshelf/app`’s deps resolve) and publishes it at `/openapi.json` here.

So the spec is available three ways: the live endpoint (`GET /api/v1/openapi.json`), the same generator script run manually, and this site’s static copy.

## Base URL and authentication

All API endpoints are relative to your server root, e.g. `https://shelf.example.com/api/v1/...`.

Two auth mechanisms are supported:

| Mechanism      | Use                        | Transport                                                  |
| -------------- | -------------------------- | ---------------------------------------------------------- |
| Bearer token   | CI (`storyshelf`), scripts | `Authorization: Bearer <token>` header                     |
| Session cookie | The web UI                 | `Cookie: storyshelf_session=...` (set by the auth adapter) |

Create a token from the project settings page (**Tokens** tab) or with the CLI (requires site-admin token):

```sh
npx storyshelf create --url $STORYSHELF_URL --name MyProject --token $STORYSHELF_ADMIN_TOKEN
# or initialize client config for an existing project:
npx storyshelf init --url $STORYSHELF_URL --slug my-project
```

Unconfigured servers (no auth adapter) accept all requests.

## Endpoint groups

| Group       | Path prefix                                                                               | Typical use                                                                       |
| ----------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| Projects    | `/api/v1/projects`                                                                        | Create and manage Storybook projects                                              |
| Builds      | `/api/v1/projects/{slug}/builds`                                                          | Upload builds, list, retry, review                                                |
| Snapshots   | `/api/v1/projects/{slug}/builds/{id}/snapshots`                                           | Inspect and approve/reject individual screenshots                                 |
| Comments    | `/api/v1/projects/{slug}/builds/{id}/comments`                                            | Review discussion threads (build, story, snapshot)                                |
| Reviews     | `/api/v1/projects/{slug}/builds/{id}/reviews`, `…/changeset`                              | Build-level verdicts (approver and admin), the changeset                          |
| Label types | `/api/v1/projects/{slug}/label-types`                                                     | Project-defined build label schemas                                               |
| Members     | `/api/v1/projects/{slug}/members`                                                         | Project-scoped roles                                                              |
| Tokens      | `/api/v1/projects/{slug}/tokens`                                                          | CI access tokens                                                                  |
| Webhooks    | `/api/v1/projects/{slug}/webhooks`                                                        | Outbound event notifications                                                      |
| Insights    | `/api/v1/projects/{slug}/builds/{id}/insights`, `/api/v1/projects/{slug}/insights/health` | AI build triage and project health ([AI insights](https://storyshelf.js.org/ai/)) |
| Admin       | `/api/v1/admin`                                                                           | Site-wide operations (retention purge)                                            |

AI agents can use this API through the [MCP server](https://storyshelf.js.org/ai/mcp/), which wraps the read and comment endpoints.

Every operation in `/api/v1/openapi.json` carries `x-storyshelf-class`: `hosting` (always), `testing` (404 without the `testing` option) or `review` (404 for a project with review off), so a client can tell which routes an instance or project hides.

## Walkthrough: upload a build

The core CI flow in three calls:

1. **List (or create) the project**

   ```sh
   curl -sS $STORYSHELF_URL/api/v1/projects
   ```

2. **Upload the built Storybook** — create the build with JSON metadata, then stream the zip

   ```sh
   CREATE=$(curl -sS -X POST $STORYSHELF_URL/api/v1/projects/my-app/builds \
     -H "Authorization: Bearer $STORYSHELF_TOKEN" \
     -H "Content-Type: application/json" \
     -d "{\"gitSha\":\"$GITHUB_SHA\",\"gitBranch\":\"$GITHUB_REF_NAME\"}")
   UPLOAD_URL=$(echo "$CREATE" | jq -r .uploadUrl)
   curl -sS -X PUT "$STORYSHELF_URL$UPLOAD_URL" \
     -H "Authorization: Bearer $STORYSHELF_TOKEN" \
     -H "Content-Type: application/zip" \
     --data-binary @storybook-static.zip
   ```

   The PUT responds with the `Build` (`status: "pending"`, `HTTP 202`) — capture runs asynchronously. Uploads larger than the server’s `maxUploadBytes` (default 1 GiB) are rejected with `413`.

3. **Follow the build to its terminal state**

   ```sh
   curl -sS $STORYSHELF_URL/api/v1/projects/my-app/builds/<buildId>
   ```

   Query builds by `?status=` and `?branch=`, and filter by a label value with `?labelKey=` + `?labelValue=`:

   ```sh
   curl -sS "$STORYSHELF_URL/api/v1/projects/my-app/builds?branch=main"
   curl -sS "$STORYSHELF_URL/api/v1/projects/my-app/builds?labelKey=environment&labelValue=staging"
   ```

   The label filter returns only builds carrying that label value (AND with any `status`/`branch` filters).

## Review flow

Snapshots with a pixel diff are `status: changed`. Approving a changed snapshot copies it to the branch baseline (per-branch baselines with default-branch fallback, ADR 0009):

```sh
curl -sS -X POST $STORYSHELF_URL/api/v1/projects/my-app/builds/<buildId>/snapshots/<snapshotId>/approve
```

Or approve everything at once:

```sh
curl -sS -X POST $STORYSHELF_URL/api/v1/projects/my-app/builds/<buildId>/approve-all
```

If the baseline changed after the diff was computed (for example a merge to the default branch re-baselined the story), approving returns `409` with `code: "baseline_changed"` rather than overwriting a baseline you did not review. Recompute the diffs against the current baselines from the stored screenshots (no re-render; builds are never recaptured automatically):

```sh
curl -sS -X POST $STORYSHELF_URL/api/v1/projects/my-app/builds/<buildId>/rediff
```

or approve anyway with `?force=true` on the `approve` / `approve-all` endpoint. `approve-all` skips snapshots with a changed baseline and lists their ids in `skipped`.

Raw PNGs are served per snapshot — image, pixel-diff overlay, and baseline:

```sh
curl -sS $STORYSHELF_URL/api/v1/projects/my-app/builds/<buildId>/snapshots/<snapshotId>/image
```

## Label types

Project-defined build label schemas live under `/api/v1/projects/{slug}/label-types` (see the **Labels** concept page for what labels are for). List and create them, then update a type’s presentation with `PATCH`:

```sh
# List label types
curl -sS $STORYSHELF_URL/api/v1/projects/my-app/label-types

# Create a label type
curl -sS -X POST $STORYSHELF_URL/api/v1/projects/my-app/label-types \
  -H "Authorization: Bearer $STORYSHELF_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "key": "environment", "name": "Environment", "color": "#0f766e" }'

# Update a label type (any of name / linkTemplate / color)
curl -sS -X PATCH $STORYSHELF_URL/api/v1/projects/my-app/label-types/environment \
  -H "Authorization: Bearer $STORYSHELF_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "color": "#0f766e", "linkTemplate": "https://console.example.com/env/{value}" }'

# Delete a label type
curl -sS -X DELETE $STORYSHELF_URL/api/v1/projects/my-app/label-types/environment \
  -H "Authorization: Bearer $STORYSHELF_TOKEN"
```

The built-in `persistent` and `branch` label types are seeded and read-only — `PATCH`/`DELETE` on them returns `400`. `PATCH` returns the updated `LabelType`; `DELETE` returns `204`.

## Errors and status codes

- `400` — request body failed Zod validation (fields in the response `message`)
- `401` — missing or invalid bearer token
- `403` — authenticated but the project role is insufficient
- `404` — unknown project, build, snapshot, or comment
- `202` — build accepted, capture queued (not done yet)

Every error body is `{ "message": string }`.

## OpenAPI in your own tools

Because the spec is a static JSON document, you can drive code generation or SDK tooling from it:

```sh
curl -sS $STORYSHELF_URL/api/v1/openapi.json > openapi.json
```

Point your favorite OpenAPI generator (or the Swagger UI console at `/api/v1/docs`) at the file. The schemas at the bottom of the document (e.g. `Project`, `Build`, `Snapshot`) are the canonical shapes for the API types.
