REST 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
Section titled “OpenAPI specification”The full API is described by an OpenAPI 3.0 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.jsonon 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’sprebuildhook, served alongside the docs - Interactive reference (this site):
GET /openapi/— client-side Swagger UI loading/openapi.json(read-only, no try-it)
Static copy
Section titled “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
Section titled “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):
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-projectUnconfigured servers (no auth adapter) accept all requests.
Endpoint groups
Section titled “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) |
| Admin | /api/v1/admin | Site-wide operations (retention purge) |
AI agents can use this API through the MCP server, 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
Section titled “Walkthrough: upload a build”The core CI flow in three calls:
-
List (or create) the project
Terminal window curl -sS $STORYSHELF_URL/api/v1/projects -
Upload the built Storybook — create the build with JSON metadata, then stream the zip
Terminal window 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.zipThe PUT responds with the
Build(status: "pending",HTTP 202) — capture runs asynchronously. Uploads larger than the server’smaxUploadBytes(default 1 GiB) are rejected with413. -
Follow the build to its terminal state
Terminal window 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=:Terminal window 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/branchfilters).
Review flow
Section titled “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):
curl -sS -X POST $STORYSHELF_URL/api/v1/projects/my-app/builds/<buildId>/snapshots/<snapshotId>/approveOr approve everything at once:
curl -sS -X POST $STORYSHELF_URL/api/v1/projects/my-app/builds/<buildId>/approve-allIf 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):
curl -sS -X POST $STORYSHELF_URL/api/v1/projects/my-app/builds/<buildId>/rediffor 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:
curl -sS $STORYSHELF_URL/api/v1/projects/my-app/builds/<buildId>/snapshots/<snapshotId>/imageLabel types
Section titled “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:
# List label typescurl -sS $STORYSHELF_URL/api/v1/projects/my-app/label-types
# Create a label typecurl -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 typecurl -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
Section titled “Errors and status codes”400— request body failed Zod validation (fields in the responsemessage)401— missing or invalid bearer token403— authenticated but the project role is insufficient404— unknown project, build, snapshot, or comment202— build accepted, capture queued (not done yet)
Every error body is { "message": string }.
OpenAPI in your own tools
Section titled “OpenAPI in your own tools”Because the spec is a static JSON document, you can drive code generation or SDK tooling from it:
curl -sS $STORYSHELF_URL/api/v1/openapi.json > openapi.jsonPoint 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.