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.
| Parameter | Default | What it carries |
|---|---|---|
kind | screenshots | What sort of set this is. |
branch | — | The branch the work happened on. |
commit | — | The commit that produced it. |
pr | — | Pull-request number. |
issue | — | Tracker reference — a Plane issue gets one upserted comment per set. |
title | — | Human title for the set. |
repo | — | Repository 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 adds — per-shot
src[],state,device{}andjourney, plus session-levelcommitandbranch.
JSON endpoints
| Endpoint | What it does |
|---|---|
GET /api/v1/projects | Every 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/sets | Generate a set key. |
PUT · GET /api/v1/projects/{project}/index | Push or read the @ autocomplete index. |
GET /api/v1/library | The cross-set screenshot search canvas. Also /facets and /screen?key=. |