Skip to content

REST API

View as Markdown

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

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.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)

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.

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

Two auth mechanisms are supported:

MechanismUseTransport
Bearer tokenCI (storyshelf), scriptsAuthorization: Bearer <token> header
Session cookieThe web UICookie: 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):

Terminal window
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.

GroupPath prefixTypical use
Projects/api/v1/projectsCreate and manage Storybook projects
Builds/api/v1/projects/{slug}/buildsUpload builds, list, retry, review
Snapshots/api/v1/projects/{slug}/builds/{id}/snapshotsInspect and approve/reject individual screenshots
Comments/api/v1/projects/{slug}/builds/{id}/commentsReview discussion threads (build, story, snapshot)
Reviews/api/v1/projects/{slug}/builds/{id}/reviews, …/changesetBuild-level verdicts (approver and admin), the changeset
Label types/api/v1/projects/{slug}/label-typesProject-defined build label schemas
Members/api/v1/projects/{slug}/membersProject-scoped roles
Tokens/api/v1/projects/{slug}/tokensCI access tokens
Webhooks/api/v1/projects/{slug}/webhooksOutbound event notifications
Insights/api/v1/projects/{slug}/builds/{id}/insights, /api/v1/projects/{slug}/insights/healthAI build triage and project health (AI insights)
Admin/api/v1/adminSite-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.

The core CI flow in three calls:

  1. List (or create) the project

    Terminal window
    curl -sS $STORYSHELF_URL/api/v1/projects
  2. 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.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

    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/branch filters).

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):

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

Or approve everything at once:

Terminal window
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):

Terminal window
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:

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

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:

Terminal window
# 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.

  • 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 }.

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

Terminal window
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.