Studio HTTP and SSE API¶
The studio's browser UI is a thin client over a local JSON + Server-Sent-Events API. This page documents that API — for scripting against a running studio, for debugging a stuck job, and for understanding what the UI is actually doing.
The API adds no logic of its own: every endpoint maps onto the same engine operation the equivalent CLI command runs. Anything you can do here you can do from the CLI, and vice versa.
Base URL and how to authenticate¶
The studio binds loopback only, on the port krites studio
reports (or the one recorded in <user config dir>/krites/studio.lock).
Every route under /api/ requires the per-launch session credential, presented
either way:
| Method | Header / cookie |
|---|---|
| Cookie | Cookie: krites_session=<token> |
| Bearer | Authorization: Bearer <token> |
The token is 32 random bytes, base64url-encoded, minted once per studio launch. It is never printed, logged, or placed in a URL. The browser gets it because loading any non-API page (the SPA shell, its assets) sets the cookie on the response.
To script against a running studio, load the studio in a browser once and reuse that cookie.
GET /api/v1/version is the only public route — deliberately, so a launcher
can probe liveness without a credential. Everything else returns 401 without
one.
Shoots and the library¶
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/shoots |
List known shoots with verdict tallies |
POST |
/api/v1/shoots |
Ingest a path and add it to the library (201) |
GET |
/api/v1/shoots/{id} |
One shoot's summary |
POST |
/api/v1/shoots/{id}/rename |
Change the display name |
DELETE |
/api/v1/shoots/{id} |
Forget the shoot from the library — never deletes files |
POST |
/api/v1/shoots/{id}/reset |
Soft or hard reset |
POST |
/api/v1/shoots/{id}/xmp |
Write XMP sidecars beside the originals |
Frames, verdicts and previews¶
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/shoots/{id}/frames |
Every frame with its verdict, reasons and signals |
PUT |
/api/v1/shoots/{id}/verdicts |
Set verdict or rating on many frames at once |
PUT |
/api/v1/shoots/{id}/frames/{frame}/verdict |
Set one frame's verdict |
GET |
/api/v1/shoots/{id}/frames/{frame}/preview |
JPEG preview |
GET |
/api/v1/shoots/{id}/frames/{frame}/analysis |
Face boxes and located issues |
POST |
/api/v1/shoots/{id}/frames/{frame}/remove |
Inpaint a region, return the preview |
GET |
/api/v1/shoots/{id}/clusters |
Near-duplicate burst clusters |
POST |
/api/v1/shoots/{id}/clusters/{cid}/keep |
Re-pick a burst's keeper |
The preview route takes query parameters:
| Parameter | Values | Effect |
|---|---|---|
size |
loupe |
1280 px longest side instead of the 320 px thumbnail |
develop |
removals, original |
Render with removals only, or the untouched original — the before/after wipe |
overlay |
peaking |
Focus-peaking overlay, returned as PNG |
Previews are JPEG at quality 82, served with Cache-Control: private, max-age=3600
and X-Content-Type-Options: nosniff.
Long-running jobs¶
Cull, export and batch review are background jobs. Starting one returns 202
with {"jobId": "...", "attached": false}.
| Method | Path | Purpose |
|---|---|---|
POST |
/api/v1/shoots/{id}/cull |
Start a cull |
POST |
/api/v1/shoots/{id}/export |
Start an export |
POST |
/api/v1/shoots/{id}/review |
Start a batch review |
GET |
/api/v1/jobs |
Every running job |
GET |
/api/v1/jobs/{jobId} |
Snapshot of one job — the polling fallback |
GET |
/api/v1/jobs/{jobId}/events |
SSE event stream |
POST |
/api/v1/jobs/{jobId}/cancel |
Request cancellation (202, or 409 if not running) |
GET |
/api/v1/shoots/{id}/jobs |
This shoot's jobs, plus interrupted ones |
Single-frame review (POST /api/v1/shoots/{id}/frames/{frame}/review) is
synchronous, not a job — it returns 501 with no provider configured and
503 when the provider is unreachable.
Job states¶
running, succeeded, failed, cancelled. The shoot-scoped job list can also
report interrupted, which is not a real state but a breadcrumb: a job marker
under .krites/jobs/ with no live job behind it, meaning the studio was killed
mid-run.
Starting the same job twice attaches instead of duplicating¶
Jobs are single-flight per (kind, shoot, target). Asking for a cull of a shoot
that is already culling returns the live job id with "attached": true
rather than starting a second one. Batch review keys on a hash of the sorted
frame selection, so the same selection attaches and a different one starts a new
job.
There is otherwise no concurrency limit — no worker pool, no maximum in-flight. Three different shoots can cull simultaneously.
The SSE event stream¶
GET /api/v1/jobs/{jobId}/events is the only SSE endpoint in the studio.
Event names¶
| Event | Meaning |
|---|---|
progress |
A progress update; also sent immediately on subscribe |
done |
Finished successfully |
failed |
Finished with an error — named failed, not error |
cancelled |
Stopped by a cancel request |
Each frame carries the job snapshot as JSON in data:, and the snapshot's
update timestamp in nanoseconds as id:.
The idle heartbeat is 10 seconds¶
An idle stream emits the SSE comment : ping every 10 seconds to keep the
connection warm through proxies and browser idle timeouts. It is a comment, not
an event, so a client that only listens for named events never sees it.
This is a compiled-in constant, not a configuration key — there is no config setting that changes it.
Closing the stream does not cancel the job¶
Disconnecting only unsubscribes. The job keeps running, and reconnecting to the
same job id resumes: a running job immediately re-sends its current snapshot as a
progress event, and a job that finished while you were away replays its
terminal event once and closes.
A finished job is retained for 5 minutes so a late reconnect still gets its outcome. After that the job id is pruned and gone.
To actually stop a job, call POST /api/v1/jobs/{jobId}/cancel.
Jobs do not survive a studio restart¶
The job registry is in memory. Restarting the studio loses every running job.
What survives is the durable breadcrumb under the shoot's .krites/jobs/, which
is why GET /api/v1/shoots/{id}/jobs can tell you a cull was interrupted rather
than silently forgetting it.
Settings¶
| Method | Path | Scope |
|---|---|---|
GET/PUT |
/api/v1/settings/profile |
Global default cull profile |
GET/PUT |
/api/v1/settings/face |
Face/eye provider |
GET/PUT |
/api/v1/settings/review |
AI review provider — the API key goes to the OS keychain, never the config file |
GET/PUT |
/api/v1/settings/remove |
Object-removal provider |
GET/PUT |
/api/v1/settings/diagnostics |
Metrics and pprof |
GET/PUT |
/api/v1/shoots/{id}/profile |
Per-shoot cull-profile overrides. A PUT re-culls, preserving overrides |
GET |
/api/v1/providers |
Which providers are on — the privacy indicator |
Exports¶
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/shoots/{id}/exports |
List rendered files in export/ |
GET |
/api/v1/shoots/{id}/exports/{name}/preview |
Preview one rendered file |
POST |
/api/v1/shoots/{id}/exports/reset |
Delete everything under export/ |
Lifecycle and the filesystem picker¶
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/version |
Running version — public, no credential |
POST |
/api/v1/shutdown |
Acknowledge, then stop gracefully |
POST |
/api/v1/update |
Self-update and re-exec (501 when not wired) |
GET |
/api/v1/update/available |
Check-only probe; never returns 500 |
GET |
/api/v1/fs/list |
Browse server-side directories |
GET |
/api/v1/fs/picker |
Whether a native folder dialog is available |
POST |
/api/v1/fs/pick |
Open the native dialog (409 when there is none) |
Limits and timeouts¶
| Limit | Value | Notes |
|---|---|---|
| Request body | 1 MiB | Framework default, applied to every request |
| Request header | 1 MiB | Framework default |
| Read timeout | 5 s | Framework default |
| Idle timeout | 120 s | Framework default |
| Write timeout | disabled | Deliberately — the default would truncate SSE streams and long inpaints |
| SSE heartbeat | 10 s | Compiled in |
| Finished-job retention | 5 min | Compiled in |
| Inpaint deadline | 120 s | Configurable — remove.timeout |
| Review deadline | 180 s | Configurable — review.timeout |
| Graceful shutdown | 5 s | Compiled in |
There is no upload size cap specific to any route and no bound on the
preview cache. Previews accumulate under each shoot's .krites/previews/ and
nothing prunes them; they are keyed by edit state, so a stale one is never served,
but the directory only shrinks if you delete it.
/metrics and pprof¶
Off by default. With diagnostics.metrics.enabled set, GET /metrics serves
Prometheus metrics; with diagnostics.pprof.enabled set, /debug/pprof/ is
served too. Both are behind the same session credential — they are never an
unauthenticated open port.
Routes register at launch, so turning either on takes effect on the next studio start, not on the running one.
What this API is not¶
- Not remote. It binds loopback and refuses any other host. There is no configuration that changes that.
- Not multi-user. One session token per launch, no accounts, no per-user state. Two browsers sharing a machine share the studio.
- Not versioned for compatibility. The
/api/v1/prefix marks the current shape; it is an internal contract between the binary and the SPA embedded in it, and it changes without a deprecation window. Script against it knowing that an upgrade may move things. - Not reachable over MCP. An agent connected via
krites mcptalks to a separate server with a separate, narrower tool surface.