vitrinkaDocsBack to the vitrína

HTTP API

Every surface is reachable over plain HTTP, and publishing needs nothing but tar and curl. This is the contract the CLI, the MCP server and the recorders all speak.

Authentication

Every request is authenticated. Machine clients carry a workspace-scoped bearer token; browsers carry a session cookie. The tenant router resolves the workspace from the /w/<slug> path or from the token itself, then role-gates the request.

curl -sS -H "Authorization: Bearer $VITRINKA_TOKEN" \
  "$VITRINKA_URL/api/v1/projects"

Board writes are additionally attributed, which is not the same as authenticated: an agent token's granted identity wins, then a declared X-Board-Actor, then the signed-in user. A browser cannot use X-Board-Actor to act as someone else.

Health

The container's own health check. No authentication.

curl -sS "$VITRINKA_URL/healthz"
# {"ok":true,"mode":"multitenant"}

Publishing a set

The canonical one-liner. It syncs a directory as a set and is idempotent — re-run it to replace the content in place.

tar czf - -C ./screenshots . | curl -sS -X PUT --data-binary @- \
  -H "Authorization: Bearer $VITRINKA_TOKEN" \
  "$VITRINKA_URL/api/v1/sets/myapp/main/s-20260823-1431/content?kind=screenshots&commit=a34ec8e&pr=42"
  • The key is yours{key} is a client-generated alias matching [a-zA-Z0-9._-]{1,64}, so you know the share URL before the first byte uploads.
  • No key handy?POST /api/v1/sets?project=…&branch=… generates one for you.
  • First upload → `201`with a fresh version. Re-PUT of the same key → 200, same version, content replaced.

Publish parameters

All optional, all query-string. They are what stops a screenshot from being orphaned from the code that made it.

ParameterDefaultWhat it carries
kindscreenshotsWhat sort of set this is.
branchThe branch the work happened on.
commitThe commit that produced it.
prPull-request number.
issueTracker reference — a Plane issue gets one upserted comment per set.
titleHuman title for the set.
repoRepository the set came from.

MAX_SET_MB (default 200) caps both the request body and the unpacked size.

Publish response

{
  "url": "…",
  "project": "myapp",
  "branch": "main",
  "version": 7,
  "key": "s-20260823-1431",
  "kind": "screenshots",
  "files": 24,
  "bytes": 5312004,
  "created": "…"
}

The manifest

A manifest.json inside the tarball drives gallery badges, routes, notes and day grouping. A set with no manifest still ingests — you lose the metadata, not the upload.

{
  "version": 1,
  "shots": [
    { "file": "01-cart.png", "surface": "cart", "route": "/cart", "note": "discount applies twice", "ts": "…" }
  ]
}
  • Version 2 addsper-shot src[], state, device{} and journey, plus session-level commit and branch.

JSON endpoints

EndpointWhat it does
GET /api/v1/projectsEvery project.
GET /api/v1/sets/{project}[/{branch}]Set listings.
GET /api/v1/sets/{project}/{branch}/{selector}One set's metadata.
PATCH /api/v1/sets/{project}/{branch}/{selector}Edit {"name","title"} — the name is a lowercase slug, unique per project and branch.
POST /api/v1/setsGenerate a set key.
PUT · GET /api/v1/projects/{project}/indexPush or read the @ autocomplete index.
GET /api/v1/libraryThe cross-set screenshot search canvas. Also /facets and /screen?key=.

Share URLs

These are the human-facing addresses a set gets. They are not under /api/v1 — they are the pages you send to somebody.

URLWhat it serves
/{project}/{branch}/{version}The set viewer — a gallery, or a sandboxed iframe when the set has an index.html.
/{project}/{branch}/latest302 to the newest version.
/{project}/{branch}/{key}302 to that set's canonical version URL.
/{project}/{branch}/{version}.zipA streamed zip of the whole set.
/{project}/{branch}/{version}/files/{path}Raw bytes, with an ETag and a long cache.
/{project}/{branch}/{version}/raw/{path}The same bytes, CSP-sandboxed — this is the iframe source for HTML artifacts.
/boards · /boards/{slug}Annotation boards.
/libraryThe cross-set screenshot search canvas.

A set can carry an editable name — a lowercase slug, unique per project and branch — so …/myapp/main/payout-flow works alongside …/myapp/main/2.