Skip to content

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 mcp talks to a separate server with a separate, narrower tool surface.