Repository navigation
HTTP API
This is the current route, access, transport, and error contract. The API is an
internal contract between the bundled Labello clients and server. It has no URL
version prefix and does not promise compatibility for independent external
clients. Coordinate contract changes across labello-client, labello-api,
all UI callers, and tests. Route tables follow the router and handler tests;
update them with route, role, header, limit, or response changes.
JSON field names and enum wire values are defined by:
-
crates/labello-client/src/dto/for access, workflow, media, and offline request/response DTOs; -
crates/labello-client/src/import.rsfor import and migration DTOs; -
crates/labello-client/src/export.rsfor export DTOs; and - versioned
labello-domaintypes for persisted workflow, dataset, snapshot, and offline artifacts.
Type names in the route tables refer to those definitions. JSON uses each type's Serde contract; callers must not infer field names from Rust identifiers. Binary image/preview and snapshot-file responses are identified explicitly.
The browser authenticates with the HttpOnly session cookie created by local
development login or GitHub OAuth. Session below means a valid cookie.
Dataset access is role-based:
- Any role: at least one role in the dataset.
-
Data admin: the dataset's
data_adminrole. - Assigned actor: the authenticated user owns the exact live assignment; its kind requires annotator or reviewer authority.
- Bootstrap admin: a server-configured bootstrap administrator. Import additionally limits job access to the creating owner and filters server roots by configured owner.
All unsafe methods (POST, PUT, PATCH, and DELETE) pass CSRF middleware.
When a session cookie is present, the request must have exactly one x-csrf-token matching the session.
A supplied Origin must match the configured allowlist; native clients may omit it. /auth/local-admin is the
middleware exception, but independently requires an allowed Origin. OAuth
state and its flow cookie are validated at the callback. The flow cookie is
created and expired at the parent path of the validated public OAuth callback
URI, including any proxy prefix. See
callback configuration for the accepted path
syntax. Internal authentication route paths remain unchanged.
CORS allows only configured origins, credentials, the implemented HTTP method
set, and these non-simple request headers: content-type, x-csrf-token,
idempotency-key, upload-offset, upload-length, and digest.
The default body limit is 128 MiB. Import control JSON is limited to 1 MiB,
browser file registration JSON to 8 MiB, and an import chunk to the advertised
uploadChunkBytes limit. These nested limits replace the default for their
routes.
Every response carries x-request-id. A supplied ID is retained only when it is
a single, nonempty value of at most 128 ASCII letters, digits, hyphens or
underscores. Missing or invalid IDs are replaced with a generated UUID before
logging and response propagation. Successful JSON responses use the status
selected by the handler, normally 200. Logout returns 204. OAuth endpoints
redirect. Binary endpoints return their media type instead of JSON.
Errors have this JSON shape:
{"error":"safe public message"}The stable status mapping is:
| Status | Meaning |
|---|---|
| 400 | Malformed request, invalid ID/path/assignment, or domain validation failure mapped as bad input |
| 401 | Missing/invalid session, CSRF/origin failure, dataset-role denial, create-dataset bootstrap denial, or identity mismatch |
| 403 | Authenticated actor lacks bootstrap-administrator authority for import |
| 404 | Route-owned resource or storage artifact was not found |
| 409 | Existing destination, state/assignment conflict, unavailable import capability, or keybinding conflict |
| 413 | Request exceeds the applicable body or storage limit |
| 422 | Structurally valid import input cannot be processed |
| 500 | Internal, unexpected storage, upstream HTTP, or serialization failure |
Internal failures return internal server error; diagnostics remain in
redacted logs. Clients must display the x-request-id, not raw internal state.
| Method and path | Access | Input → output |
|---|---|---|
GET /stats |
Session; each included dataset requires any role | No input → DatasetStatistics[], each with dataset identity/name, task/class definitions, imbalance configuration, and unchanged DatasetStats; Cache-Control: no-store
|
GET /keybindings |
Session, own account | No input → global KeybindingSet, defaults when not saved; Cache-Control: no-store
|
PUT /keybindings |
Session, same user; CSRF |
KeybindingSet → normalized global KeybindingSet
|
GET /keybindings/legacy |
Session, own account; any role in source dataset | No input → saved LegacyKeybindings[] for explicit migration; empty after a global save; Cache-Control: no-store
|
GET /presence |
Authenticated, server-wide | No input → active lease holders, optional GitHub logins/account IDs, and their dataset IDs/names; ServerPresence, Cache-Control: no-store
|
GET /health |
Public | No input → {"ok":true,"service":"labello"}
|
GET /build-information |
Public, no session or CSRF token | No input → compiled artifact releaseTag and sourceCommit, independently of readiness; Cache-Control: no-store
|
GET /deployment/readiness |
Public; production API is loopback-bound | No input → bounded release identity, schema version, dataset-root traversal, and authentication-store load state; HTTP 503 when a probe fails |
GET /auth/options |
Public | No input → AuthOptions
|
POST /auth/local-admin |
Public, allowed origin, feature enabled | No body → SessionInfo plus session cookie |
GET /auth/github/login |
Public, OAuth enabled |
OAuthLoginRequest query → temporary provider redirect plus flow cookie |
GET /auth/github/callback |
Public, valid OAuth state/flow cookie |
OAuthCallbackRequest query → configured browser redirect plus session cookie |
GET /me |
Session | No input → SessionInfo, Cache-Control: no-store
|
POST /logout |
Session optional; CSRF when cookie present | No body → 204 plus expired session cookie |
GET /datasets |
Session | No input → DatasetSummary[] filtered to accessible datasets |
POST /datasets |
Bootstrap admin |
CreateDatasetRequest → DatasetMetadata
|
GET /datasets/{dataset_id} |
Any role | No input → role-sanitized DatasetMetadata
|
GET /datasets/{dataset_id}/users |
Data admin | No input → DatasetUser[]
|
PUT /datasets/{dataset_id}/roles |
Data admin |
SetDatasetRolesRequest → DatasetUser
|
GET /datasets/{dataset_id}/admin |
Data admin | No input → full configuration DatasetMetadata
|
PUT /datasets/{dataset_id}/admin |
Data admin |
UpdateDatasetConfigRequest → full configuration DatasetMetadata
|
POST /datasets/{dataset_id}/ingest |
Data admin | No body → IngestReport
|
POST /datasets/{dataset_id}/ingest-jobs |
Data admin | No body → IngestJob
|
GET /datasets/{dataset_id}/ingest-jobs/{job_id} |
Data admin | No input → IngestJob for the same dataset |
POST /datasets/{dataset_id}/uploads |
Data admin |
multipart/form-data, root and ingest query → IngestReport
|
GET /datasets/{dataset_id}/snapshots |
Data admin | No input → DatasetSnapshot[]
|
POST /datasets/{dataset_id}/snapshots |
Data admin | No body → DatasetSnapshot
|
GET /datasets/{dataset_id}/snapshots/{snapshot_id}/files/{*file_path} |
Data admin | No input → attachment bytes listed by the snapshot manifest |
GET /datasets/{dataset_id}/tasks |
Any role | No input → TaskDefinition[]
|
POST /datasets/{dataset_id}/tasks |
Data admin |
TaskDefinition → TaskDefinition
|
GET /datasets/{dataset_id}/prelabels |
Any role | No input → PrelabelConfig[]
|
POST /datasets/{dataset_id}/prelabels |
Data admin |
PrelabelConfig → PrelabelConfig
|
GET /datasets/{dataset_id}/images |
Any role |
ImageExplorerQuery → ImageExplorerPage
|
GET /datasets/{dataset_id}/stats |
Any role | No input → DatasetStats
|
GET /datasets/{dataset_id}/stats/me |
Any role | No user selector → CurrentUserActivity for the authenticated account and server UTC day; Cache-Control: no-store
|
GET /datasets/{dataset_id}/keybindings |
Any role | Compatibility alias for the authenticated user's global KeybindingSet
|
PUT /datasets/{dataset_id}/keybindings |
Any role, same user | Compatibility alias saving the global KeybindingSet
|
POST /datasets/{dataset_id}/prelabel-model-check |
Data admin | Unsaved { location } managed filename → PrelabelModelInspection; control JSON limited to 4 KiB; no inference image, configuration save, or hint mutation |
GET /datasets/{dataset_id}/prelabel-management |
Data admin | No input → PrelabelAdminState
|
POST /datasets/{dataset_id}/prelabel-management |
Data admin | Preflight/start/cancel/retry/reset/resume command → PrelabelAdminState
|
KeybindingSet.bindings contains the primary chord for every active action.
panDragModifier selects the modifier used with primary-button drag to pan and
defaults to control. Middle-button drag is always available. Older requests
that omit panDragModifier remain valid and receive the default during
deserialization.
GET /stats enumerates managed datasets and applies the same role check as the
individual statistics route. Bootstrap-admin status alone grants no statistics
access. It returns a complete authorized set or an error; unreadable dataset
configuration or statistics cannot silently reduce the total. This is a sequential
read of dataset projections, not a transactionally synchronized server snapshot.
The client aggregates additive totals and UTC contributor days by stable user ID.
Task/class breakdowns, assignment balance, daily scoring multipliers and focus
remain dataset-scoped. Images present in two datasets count in both.
Global shortcut saving needs no dataset membership. Before the first global save, the legacy route lists only the user's saved bindings in currently authorized datasets. The UI offers an explicit source selection or saving new settings; GET never chooses one dataset's preferences or overwrites old files. Supported legacy records normalize through the existing storage migration. Once saved, the global value wins on every route; see persistence.
Optional DatasetStats.contributors maps user IDs to displayName, optional
githubUserId for avatars, and chronological UTC history rows containing
day, labeled, reviewed, accepted, and rejected. An empty map means no
activity; an absent field means contributor statistics are unavailable.
DatasetStats.scoringVersion = 1 advertises contribution scoring. Each history
row adds a defaultable score object: labels is a count, and labeling,
reviewing, deductions, and corrections are integer hundredths of a point.
Net points are labeling + reviewing + corrections - deductions. Optional
scoringFocus contains startsAt, endsAt, and nullable taskId; an expired
selection must not be advertised as active. Missing scoring support is distinct
from zero points. Policy and historical credit are defined in
Contribution scoring. These fields use the existing authenticated,
dataset-authorized statistics endpoint.
Counts derive from existing events: labeling counts completed Objects and Overview items per person, image and task. A whole skeleton is one item. Legacy image/task submissions occupy the Overview slot, so compatibility events and later receipts cannot double count them. Item reviews count once per person, image, task and item; legacy reviews retain distinct-decision counting. Partial saves, Skip, retries and unchanged history reconfirmations add no credit. Accepted/rejected counts belong to the reviewed human work. Corrections count as rejections; imports and automatic work earn no human credit. Activity uses the submission/review date; revised decisions count on commit and retain earlier activity. Clients calculate periods, ranks, and acceptance percentages. Existing response fields, authorization, and persisted formats remain unchanged.
Clients derive streaks from contributor labeling and review history.
Role mutation retains bootstrap-administrator protections implemented by the handler; a data administrator cannot use this route to bypass those rules.
DatasetMetadata.imbalance and UpdateDatasetConfigRequest.imbalance use the
direct maxDifference representation documented in
Server configuration. The
administration update route rejects ratio and tagged-policy shapes. Assignment
statistics include the current annotation and review counts plus the task IDs
blocked by the enforced window.
DatasetMetadata.preloadQueueSize and the administration update field of the
same name are integers from 1 through 200. Missing values default to two. The
existing DataAdmin authorization applies; invalid ranges return 400.
POST /datasets accepts optional schemaSourceDatasetId. Omitted or null
creates empty classes and tasks. A source requires DataAdmin access in addition
to the existing bootstrap-admin creation permission. The server reloads its
current configuration and validates the copied definitions before initializing
the destination. Missing, unauthorized, or invalid sources fail creation;
they never fall back to an empty schema.
The copy preserves class and task IDs, names, class relationships, workflow settings, instructions text, and skeleton definitions. Manual box-guide migration configuration is cleared so copied skeleton tasks support ordinary annotation and their configured review flow without imported guide boxes or target sets. Tutorial example-image references and prelabel bindings are also cleared. Images, annotation/workflow history, source roles, ingestion paths, prelabel model configuration, and dataset balance settings are not copied. The destination keeps normal fresh-dataset identity, defaults, and initial administrator roles. The source and destination are independent after creation.
GET /build-information returns only releaseTag and sourceCommit. Each is
nullable: absent or invalid compiled metadata is null, never the shared Cargo
package version. Tags contain at most 64 ASCII letters, digits, dots, underscores,
plus signs or hyphens; commits contain exactly 40 or 64 hexadecimal characters.
The reserved tag development represents missing release metadata and is exposed
as null. The response is under 200 bytes, performs no persistence or
authentication probe, and remains HTTP 200 when /deployment/readiness returns
503. Existing credentialed CORS rules also apply to this public route.
The client uses a ten-second request timeout, validates the two-field DTO and rejects responses over 1024 bytes. The response must not expose readiness, configuration, authentication state, paths, or other server details.
Authenticated GET /presence returns ServerPresence with Cache-Control: no-store. Each PresentUser contains a stable internal userId, optional
githubLogin and githubUserId from the stored account, and the IDs/names of
datasets where that user holds an unexpired active annotation or review lease.
githubUserId is optional public avatar metadata; clients must not derive it
from the internal ID. The header displays GitHub profile photos, with initials
while photos are missing, loading or unavailable. Details use @githubLogin
when nonempty, otherwise userId; display names do not override GitHub handles.
Older responses without these optional fields retain the same ID detail fallback
and show initials instead of a photo.
Migration uses those same assignment kinds. The requesting user is included
under the same lease rules, even when they are the only active user. Users are
deduplicated and sorted by internal ID, with datasets sorted by ID. Presentation
metadata never changes identity or assignment ownership.
This endpoint deliberately exposes active usernames and dataset names across server dataset-role boundaries to authenticated users. It grants no dataset access and returns no assignment IDs, image identities, geometry or session information. Query parameters cannot select a different requesting user. Unauthenticated requests fail. An unreadable registered dataset fails the request instead of reporting a potentially false empty presence list.
Presence reads never claim, renew or release leases. Closing a browser does not release its leases automatically; the existing lease-expiry policy applies.
CurrentUserActivity identifies datasetId, authenticated userId, the inclusive
UTC window.start, exclusive window.end, server sampledAt, and two integer
counts: annotationTasksSubmitted and finalTaskReviews. The route has no
client-selected user or time window. It requires a current dataset role and
returns a fixed-size aggregate, never another user's history. A scan crossing
midnight retries once for the new day, then returns a retryable error if the
window changes again. The HTTP client
uses the existing 20-second statistics timeout and credentialed session.
Annotation activity counts committed normal or guided-migration submissions,
including completion with no review stage. Review activity counts committed
final task or migration-confirmation decisions, approved or rejected. Each
counter deduplicates dataset/image/task/user within that UTC day. Saves, imports,
skips and object decisions do not count. A committed correction submission
counts its final rejection, once per user/image/task/day. Historical immediate
reviewer corrections alone do not count. Reopening,
later rejection and superseding a decision do not retract historical activity.
Same-day replacement commits count once; a later-day submission or final-review
replacement counts on its new commit day. Compound review revisions use the
event's server commit time, not timestamps of locally staged review records.
Dataset-wide DatasetStats semantics remain unchanged.
Current task review configuration contains workflow, either none or
approval, and allowReviewerCorrections. Approval requires one reviewer.
Historical-only configuration fields, roles, task states and assignment kinds
cannot be introduced through current mutation endpoints.
DatasetStats reports pendingTasks, inProgressTasks, awaitingReviewTasks,
needsCorrectionTasks, and completedTasks. Each perTask entry uses pending,
inProgress, awaitingReview, needsCorrection, and completed, plus its
existing provenance and migration statistics. Each eligible image/task occupies
one state count. The task table and summary use this same contract. Audit review
records and contributor activity remain available through their existing APIs.
GET /datasets/{dataset_id}/images/{image_id}/reasons requires a session and any
role in that dataset, matching image-state access. It returns WorkflowReasonEntry[]
projected from one authoritative event-log snapshot. Each entry identifies the
image, event sequence/ID, actor, timestamp, action, optional workflow and object
identities, optional text and structured exclusion category. Optional
reviewDecision preserves the recorded decision for review and revised-review
comments; absent values remain neutral comments rather than inferred rejections.
The existing reason fields remain at the top level. Each entry additionally has
an optional author with githubLogin and githubUserId, resolved from current
server account data for that event's actor. Missing accounts produce null;
local accounts can have null GitHub fields. No account timestamps, display names,
roles, or unrelated accounts are included. Older responses without author
remain readable. This read-only transport metadata changes no persisted event. currentRound,
superseded and currentExclusion describe that snapshot's relevance. Historical
entries remain ordered by source event, with copied exclusion sources and the
legacy correction's identical review comment deduplicated. Blank text and known
internal annotation markers are omitted. The route does not claim assignments,
mutate history, or expose annotation geometry. Missing images return 404; role
and authentication failures follow the normal image-route rules.
The browser loads this resource under the assignment request's existing stale response gate. A failed request fails the image load and remains retryable; it does not masquerade as an empty reason history. No persisted event or state shape changes. Correction explanations for several objects use explicit object labels inside the existing optional submission reason and retain its 2000-byte limit.
| Method and path | Access | Input → output |
|---|---|---|
POST /datasets/{dataset_id}/work-items/claim |
Annotator or reviewer for requested kind |
ClaimWorkflowRequest { selection: { taskId, kind, variant }, prefetch, excluded } → Assignment?; variant is objects or overview
|
GET /datasets/{dataset_id}/work-items/availability |
Requested kind authorized by storage |
kind query → WorkflowAvailability[], including each variant's split flag and blocking reason |
POST /datasets/{dataset_id}/work-items/display |
Exact active item owner |
AssignmentActionRequest → ImageState; records durable seen state and renews the lease |
POST /datasets/{dataset_id}/work-items/draft |
Exact active displayed item owner |
SaveWorkflowDraftRequest { assignment, geometry, expectedSequence }, or { assignment, edits: { changes, reason }, expectedSequence } → ImageState; geometry is an annotation Objects draft, edits are annotation Overview or reviewer proposals |
GET /datasets/{dataset_id}/work-items/history |
Requested kind authorized by storage |
taskId, kind, variant query → newest-first WorkflowHistoryEntry[] within the configured window |
POST /datasets/{dataset_id}/work-items/reopen |
Owner of an eligible history visit |
AssignmentActionRequest → renewed or reacquired Assignment
|
POST /datasets/{dataset_id}/work-items/leave |
Requested kind authorized by storage |
WorkflowSelection → success; releases the caller's matching reservations |
GET /datasets/{dataset_id}/assignments/availability |
Session; requested kind authorized by storage |
AssignmentAvailabilityRequest query → AssignmentAvailability
|
POST /datasets/{dataset_id}/images/next |
Session; requested kind authorized by storage |
AssignNextRequest → Assignment?
|
POST /datasets/{dataset_id}/images/{image_id}/assignments/revalidate |
Owner of exact active assignment |
AssignmentActionRequest → AssignmentRevalidation?
|
POST /datasets/{dataset_id}/assignments/release |
Assigned actor |
AssignmentActionRequest → Assignment
|
POST /datasets/{dataset_id}/assignments/complete |
Assigned annotator |
AssignmentActionRequest → Assignment
|
POST /datasets/{dataset_id}/assignments/reopen |
Owner of exact prior annotation or eligible review assignment |
AssignmentActionRequest → Assignment
|
GET /datasets/{dataset_id}/images/{image_id} |
Any role | No input → ImageState
|
GET /datasets/{dataset_id}/images/{image_id}/review-submitters |
Any role | No input → ReviewSubmitter[]: image/task/user IDs and optional public GitHub login/account ID for current review-round or migration-confirmation authors; no private account fields or history mutation |
GET /datasets/{dataset_id}/images/{image_id}/reasons |
Any role | No input → WorkflowReasonEntry[]; see saved reasons
|
POST /datasets/{dataset_id}/images/{image_id}/return-to-review |
Reviewer or data admin |
ReturnToReviewRequest → ImageState; exact sequence, atomic selected workflows |
GET /datasets/{dataset_id}/images/{image_id}/record |
Any role | No input → ImageRecord
|
GET /datasets/{dataset_id}/images/{image_id}/file |
Any role | No input → original image bytes and stored media type |
GET /datasets/{dataset_id}/images/{image_id}/preview |
Any role |
max query clamped to 256–4096 → raw RGBA bytes (application/octet-stream) plus x-image-width and x-image-height; bounded legacy fallback |
GET /datasets/{dataset_id}/images/{image_id}/encoded-preview |
Any role |
profile=standard_v1 (default), data_saver_v1 or thumbnail_v1 → bounded image/webp, x-image-width, x-image-height, x-original-width, x-original-height, x-preview-profile
|
GET /datasets/{dataset_id}/images/{image_id}/detail |
Any role | Explicit bounded original-detail display; original encoded bytes and decoder-format MIME, private, no-store
|
POST /datasets/{dataset_id}/images/{image_id}/events |
Assigned annotator; role also derived from allowed payload |
AssignmentActionRequest query plus AppendEventRequest → EventLogEntry
|
POST /datasets/{dataset_id}/images/{image_id}/annotation-batch |
Assigned annotator |
AssignmentActionRequest query plus AnnotationBatchRequest → ImageState
|
POST /datasets/{dataset_id}/images/{image_id}/admin/events |
Data admin |
AppendEventRequest with permitted repair payload → EventLogEntry
|
POST /datasets/{dataset_id}/images/{image_id}/rebuild |
Any role | No body → replayed ImageState
|
POST /datasets/{dataset_id}/images/{image_id}/reviews |
Assigned reviewer |
AssignmentActionRequest query plus ReviewRecord → ImageState
|
POST /datasets/{dataset_id}/images/{image_id}/missing-object-rejections |
Reviewer; retired command | Returns 400; historical evidence remains readable |
POST /datasets/{dataset_id}/images/{image_id}/review-revisions |
Owner of active decision-revision lease; reviewer role |
AssignmentActionRequest query plus ReviewRevisionCommit → ImageState
|
POST /datasets/{dataset_id}/images/{image_id}/review-corrections |
Assigned reviewer, including revision owner |
AssignmentActionRequest query plus ReviewCorrectionSubmission → ImageState
|
POST /datasets/{dataset_id}/images/{image_id}/corrections |
Assigned reviewer |
AssignmentActionRequest query plus CorrectionRequest → EventLogEntry
|
GET /datasets/{dataset_id}/offline-bundle |
Annotator |
OfflineBundleRequest query → OfflineBundle
|
POST /datasets/{dataset_id}/offline-sync |
Annotator; same authenticated user and dataset | versioned OfflineSyncRequest → OfflineSyncResult
|
Work-item geometry and edit drafts are provisional; neither confirms work nor awards points. Edit proposals contain at most 10000 typed changes and 2000 bytes of reason. The sequence is compare-and-swap across reassignments; an exact retry is idempotent. Item-aware annotation batches and review/correction commands enforce the captured object or Overview scope. Raw event ingress cannot manufacture workflow events.
Per-user inference, retained-hint discovery, generation polling, browser-result certification and model-byte routes have been removed. Their paths return 404. Administrator configuration and generation routes remain available. New/changed task bindings permit at most one prelabel configuration; unchanged legacy lists remain readable. Preparation is server-owned and includes both boxes and poses.
The assignment ID, image ID, task ID, actor, kind, current sequence, and live state are validated at the transaction boundary. Possessing an ID is not authorization.
Availability and direct claims apply the current-count completion-balance
decision. AssignNextRequest.prefetch, default false, adds prospective balance
admission for upcoming work. It cannot be combined with assignmentId reclaim.
excludedImageIds accepts at most 202 distinct validated IDs, covering the
maximum queue, current image, and one skipped image. An imbalance-limited
prefetch returns {"reason":"imbalance_limit"} with status 200 before the client
fetches image resources. Other unavailable work still returns null; a successful
claim retains the assignment-object response. The reason comes from the claim's
admission decision, without another balance request.
Availability responses include queue.size and queue.eligibleAssignments.
The latter contains the caller's live, eligible reservation IDs for the requested
kind, filtered by projected balance when enforcement is enabled. Clients also
accept null from older or demonstration adapters as an omitted reservation filter.
It is advisory and never renews leases. Clients must not apply an older response
to reservations created after that request. The complete count, denominator, disabled-peer,
zero-count, and exact-boundary contract is maintained in
Assignment.
Review reopening and replacement follow the strict
queue history contract.
ReviewRevisionCommit contains reviews, from 1 to 10001 ReviewRecord values.
Each record must belong to the caller, have a unique ID and captured exact target,
and contain at most 2000 bytes of comment. The final record is the task or
migration-confirmation decision. Approval must include every captured target
without a rejected object. Foreign actors or missing reviewer authority return
401. Invalid syntax or assignment-kind input returns 400; changed context,
expired ownership, malformed replacement targets, and conflicting retries return
409. Ordinary review commands cannot mutate a task held by an exclusive revision
lease. Its owner may submit corrections through review-corrections.
Workflow, ReviewAssignmentOpened, ReviewAssignmentFinished, and
ReviewRevisionCommitted, ReviewCorrectionSubmitted, and MissingObjectEvidenceRecorded are server-owned events. Raw event, annotation batch,
admin repair, and offline sync ingress cannot publish them. Clients submit
commands to the dedicated endpoints and never choose superseded review IDs.
ReviewCorrectionSubmission contains correctionId, captured round,
targetFingerprint, 1–10000 changes, and optional reason (at most 2000 bytes).
Changes are tagged edit, add, remove, or migration_object and use snake-case
variant fields: exact annotation_id/expected_version, new class_id and
geometry, or object_group_id/expected_disposition_version with a replacement
(skeleton or exclude). The server supplies reviewer attribution and provenance.
It rejects empty or unchanged corrections, invalid geometry and identifiers,
stale targets, foreign ownership, and conflicting retries. The full batch,
rejection of the old round, assignment completion and fresh submitted round are
atomic. An identical retry does not append events. Saving does not finalize work.
corrections is a compatibility adapter for one geometry edit; it now returns
the ReviewCorrectionSubmitted receipt event and has the same fresh-round semantics.
The allowReviewerCorrections flag no longer disables corrections. Ordinary,
migration and revision commands reject decision-only rejection. Final approval
requires approval of every current object by this reviewer.
missing-object-rejections is retired and rejects writes. Nonempty
ReviewRevisionCommit.missingObjects is rejected as well. Historical evidence
remains in ImageState, snapshots and event replay. Raw event and admin repair
commands cannot bypass substantive-correction requirements with ReviewRecorded
rejections.
The workflows entries cover the requested kind and every authorized related kind,
so cached view changes retain Objects/Overview eligibility. Each entry includes
its kind in selection.
AssignmentAvailability and each authorized related entry retain their
tasks map of task IDs to booleans. An additive reasons map contains a bounded
reason code for each unavailable task and no entries for available tasks. Older
responses without this map remain readable; missing or unknown codes display a
generic unavailable explanation. The codes are balance_limit, review_disabled,
empty_dataset, annotation_finished, nothing_awaiting_review,
claimed_by_others, review_revision, import_excluded, review_finalized,
and unavailable.
Task-level configuration and balance restrictions take precedence. Otherwise,
any eligible image makes the task available. A specific image-level reason is
reported only when all images have the same first restriction under the claim
policy; mixed results use unavailable. Submitted and completed work needs no
further annotation. Completed review work uses review_finalized; pending,
in-progress and correction work uses nothing_awaiting_review. Availability is
advisory and does not reserve work. Reasons share the same role checks, bounded
scan, cache lifetime, and mutation invalidation as the boolean result. No actor,
image, annotation, or import-source details are exposed by these codes.
All dataset members may list indexed images and read their previews and current annotations, including completed images outside their assignment queue. Search, workflow, class and workflow-status predicates apply before pagination. With no workflow/class/status filter, only the requested page loads annotation state; search and ordering use the image index. State-dependent filters evaluate rebuildable, process-local per-image summaries before selecting the page. A cold summary loads authoritative image state once; subsequent queries reuse it until that image changes. Configuration defaults and image records come from the current request, not cached workflow definitions. A workflow filter identifies configured workflow status, including Pending on unannotated images; selecting a workflow alone need not reduce the image count. With All workflows, Completed requires every currently configured workflow (including disabled workflows) to be Completed, and a dataset without workflows matches no completed images. Historical workflow IDs do not affect this check. With a specific workflow selected, Completed checks only that workflow. Other status filters match any workflow when no workflow is selected. Browsing and changing overlay visibility require no assignment and append no events.
POST /datasets/{dataset_id}/images/{image_id}/return-to-review requires a
reviewer or data-admin role and the normal CSRF checks. Its JSON body is
ReturnToReviewRequest: requestId, expectedSequence, taskIds and reason.
Select 1–100 distinct configured workflows and supply a nonblank reason of at
most 2000 UTF-8 bytes. The server requires an exact image sequence and enabled,
completed approval workflows without an unexpired assignment. It rejects the
entire selection if any workflow is ineligible. Active leases are not stolen.
Success returns the replayed ImageState with the selected workflows submitted
for fresh review rounds. Geometry and previous decisions remain unchanged.
Stale state, active work, ineligible workflows and conflicting request-ID reuse
return 409. An identical request by the same actor is idempotent across restart
and later work; it returns current state without appending another event.
Request identity is scoped to the dataset image. Other roles cannot mutate
through this route, and ordinary event ingress cannot forge its server event.
Standard v1 resizes to at most 1600 pixels on the longest edge and encodes lossless WebP. Data Saver v1 uses at most 1280 pixels and lossy WebP quality 80 (on libwebp's 0–100 scale). Thumbnail v1 uses a 256-pixel maximum edge and the same lossy quality for gallery proxies. No profile upscales. All preserve the existing Triangle resize at the decoder's native channel depth followed by RGBA8 conversion, first-frame behavior, and no EXIF orientation or ICC conversion. Standard decoded RGBA, including transparent RGB, is identical to the legacy 1600 preview. Original record dimensions remain authoritative for geometry.
Both routes authenticate and authorize each request, including cache hits, and
recheck session, roles, and image index after worker completion. Source content
is verified against its authoritative BLAKE3 hash before every cache read.
Responses use Cache-Control: private, no-store; encoded dimension and profile
headers are exposed by credentialed CORS. Encoded responses are at most 16 MiB.
Limits are configured under previews.
Oversized source/pixel/decoder requests return 413; full preview admission queues,
cache ownership conflicts, exhausted cache quota, or stale source identity return
409; unsupported/unavailable sources and decoding failures return 422;
unavailable cache/encoder failures return 500.
Errors never include source paths or decoder text.
ImageApi::get_encoded_image_preview returns EncodedImagePreview, separate
from ImagePreview::rgba. HTTP clients bound streaming response bytes and
validate MIME/profile/metadata and check encoded WebP dimensions before pixel
allocation. Native clients use the bounded Rust WebP decoder. The browser UI
uses asynchronous createImageBitmap decoding and an offscreen canvas, checks
the decoded dimensions and RGBA length, and closes the bitmap after copying its
pixels. This keeps prefetch decoding from blocking browser input. Browser decode
errors use bounded messages without decoder details. The UI always requests
Data Saver v1 for working-image loads, reloads, retries and prefetch. Generation, transfer and decode errors propagate without
requesting Standard, legacy RGBA or original bytes. The encoded route retains
its Standard v1 default for API callers that omit the profile. Image assignment,
annotation geometry and draft state are independent of the representation.
Existing request/auth/workspace epochs reject stale image replies and clear account-scoped texture/prefetch state.
ImageApi::get_original_detail and GET .../detail remain explicit API
capabilities; the working UI does not call them or expose an original-detail
action. Ordinary original-file download keeps its existing separate contract.
Detail reads share preview source-byte, pixel, decoder-header/allocation
and worker limits, secure source opening/hash verification, and final
session/role/index checks. They return original source bytes without a derived
cache entry. The client bounds streaming bytes to 64 MiB and decoding to
32 million original pixels / 256 MiB decoder allocation, checks declared MIME
and authoritative original dimensions, and applies no EXIF/ICC transform. Larger
server configuration limits do not raise these browser detail limits.
Every working-image load and prefetch uses the encoded Data Saver v1 route. Saved browser quality preferences are ignored; preview failure never chooses original detail. Aborting superseded image transfers does not abort assignment claims: their replies must still be received so obsolete reservations can be released. Server workers already started retain their configured bounds through completion/cleanup.
Every route requires a valid session, exact owned assignment, the role shown,
and exactly one idempotency-key containing 1–200 visible ASCII characters
other than comma or semicolon. Responses are
ManualMigrationCommandResult.
| Method and path | Role | Request |
|---|---|---|
POST /datasets/{dataset_id}/images/{image_id}/migration/skeleton |
Annotator | SaveMigrationSkeletonRequest |
POST /datasets/{dataset_id}/images/{image_id}/migration/skeletons |
Annotator | AddMigrationSkeletonRequest |
POST /datasets/{dataset_id}/images/{image_id}/migration/skeletons/edit |
Annotator | EditMigrationSkeletonRequest |
POST /datasets/{dataset_id}/images/{image_id}/migration/skeletons/delete |
Annotator | DeleteMigrationSkeletonRequest |
POST /datasets/{dataset_id}/images/{image_id}/migration/skeletons/reconcile |
Annotator | ReconcileMigrationCompanionRequest |
POST /datasets/{dataset_id}/images/{image_id}/migration/exclude |
Annotator | ExcludeMigrationTargetRequest |
POST /datasets/{dataset_id}/images/{image_id}/migration/reopen |
Annotator | ReopenMigrationTargetRequest |
POST /datasets/{dataset_id}/images/{image_id}/migration/revisit |
Annotator | RevisitMigrationTargetRequest |
POST /datasets/{dataset_id}/images/{image_id}/migration/passes |
Annotator | StartMigrationPassRequest |
POST /datasets/{dataset_id}/images/{image_id}/migration/keep |
Annotator | KeepMigrationTargetRequest |
POST /datasets/{dataset_id}/images/{image_id}/migration/confirm |
Annotator | ConfirmMigrationRequest |
POST /datasets/{dataset_id}/images/{image_id}/migration/review |
Reviewer | ReviewMigrationRequest |
Adding, editing, or removing a missing-object skeleton is allowed only at the full-image cursor. Edit and delete requests identify an active, native, group-less skeleton for the selected migration task and supply its exact expected version; stale or non-migration annotations are rejected.
New save, add, and edit requests must contain at least one visible or hidden
keypoint with coordinates. A skeleton containing only absent keypoints returns
HTTP 400 with the safe invalid-assignment message
manual migration skeleton requires at least one positioned keypoint.
Idempotent replay is checked first, so an identical retry of a successfully
recorded historical all-absent command still replays without appending events.
Each newly discovered skeleton is saved with one ordinary bounding box in the
configured guide task, in the same per-image event transaction. The box bounds
include positioned visible and hidden keypoints and span at least 15% of each
image dimension, retaining a one-original-pixel floor per axis for tiny images.
Expansion is centered on the keypoint bounds and shifted inside the image at
edges and corners. This derivation rule also applies to automatic regeneration
and explicit reconciliation, not to independent human box edits. Existing
persisted geometry is not resized when loaded or replayed. The server chooses its task, class and stable
identity;
clients cannot supply another guide task. The guide task enters needs_correction
with its terminal outcome cleared. Existing history and review records remain.
Still-derived companions follow skeleton edits and deletion. An independent box
edit or review, active guide-task assignment, stale version, or changed guide
configuration rejects the whole operation. Explicit reconciliation requires the
same active migration annotation assignment and full-image cursor. Its request
contains assignmentId, passId, taskId, annotationId, expectedVersion,
and nullable expectedBoxVersion. A missing box uses null; regenerating an
existing box requires its exact current version. Reconciliation creates or
regenerates one box from the saved skeleton and returns the ordinary migration
command result. Identical command retries do not append or duplicate objects.
Historical reconciliation requires an active group-less native skeleton with a recorded full-image discovery creation event. Ordinary skeletons without that provenance, coordinate-less skeletons, inconsistent links and competing work remain unresolved with an actionable conflict. Each committed link is a durable unit of progress: after interruption, inspect current links and reconcile the remaining objects. Reads never perform reconciliation.
Migration review defaults to canonical dispositions, then every active discovered
skeleton in annotation-ID order, then the full-image confirmation. Individual
items may be approved in any order under the exact active review assignment,
binding their current disposition or skeleton version. This permits approval of
a later unchanged item while an earlier correction remains local until overview
submission. Final confirmation still requires this reviewer to have approved
every current item in the current submission round. A discovered
review target is { "targetType": "discovered", "annotationId": "…", "version": 1 };
it binds the exact current skeleton version. A rejected discovery must receive
a new version or be removed before submission. Companion boxes retain the
ordinary guide-task review flow and are never approved by migration review.
All import routes require a bootstrap-administrator session. Job routes also
require ownership of the import. A missing/disabled import service returns
409. Mutating routes marked "Idempotent" require exactly one
idempotency-key with the same syntax as migration routes. Reusing a key for a
different operation or request is a conflict; a completed identical request
replays its response.
| Method and path | Additional contract | Input → output |
|---|---|---|
GET /import-capabilities |
Authorized roots are filtered per actor | No input → ImportCapabilities
|
POST /import-roots/{root_id}/browse |
Root must be configured for actor |
BrowseServerImportRootRequest → ImportBrowsePage
|
GET /imports |
Owned jobs only | No input → ImportJob[]
|
POST /imports |
Idempotent |
CreateImportRequest → ImportJob
|
GET /imports/{import_id} |
Owned job | No input → ImportJob
|
POST /imports/{import_id}/files/register |
Idempotent; 8 MiB body |
RegisterImportFilesRequest → RegisterImportFilesResult
|
POST /imports/{import_id}/files/{file_id}/chunks |
Idempotent; advertised chunk limit | Raw bytes plus upload-offset, upload-length, and full BLAKE3 digest headers → ImportChunkResult
|
POST /imports/{import_id}/source/browse |
Owned job |
BrowseImportSourceRequest → ImportBrowsePage
|
POST /imports/{import_id}/yolo-descriptor/inspect |
Owned job |
InspectYoloDescriptorRequest → YoloDescriptorInspection
|
POST /imports/{import_id}/seal |
Idempotent |
SealImportRequest → SealImportResult
|
POST /imports/{import_id}/preflight |
Idempotent |
StartImportPreflightRequest → ImportJob
|
GET /imports/{import_id}/plan |
Current accepted/preflight plan required | No input → ImportPlan
|
PUT /imports/{import_id}/plan |
Idempotent |
UpdateImportPlanRequest → ImportPlan
|
GET /imports/{import_id}/diagnostics |
Limit bounded by capabilities |
ImportDiagnosticsQuery → ImportDiagnosticsPage
|
POST /imports/{import_id}/commit |
Idempotent; reauthorizes every attempt |
CommitImportRequest → CommitImportResult
|
POST /imports/{import_id}/cancel |
Idempotent; not committing/succeeded |
CancelImportRequest → CancelImportResult
|
Every route requires a current authenticated dataset DataAdmin. POST
requests retain the session CSRF requirement. Control JSON is limited to 1 MiB.
Capabilities report availability and limits; unavailable operations return 409.
Jobs are private to their dataset, and current role checks apply to retained
jobs too. Transport DTOs live in labello-client/src/export.rs.
| Route | Request and response |
|---|---|
GET /datasets/{dataset_id}/exports/capabilities |
ExportCapabilities |
GET /datasets/{dataset_id}/exports |
Retained ExportJob[]
|
POST /datasets/{dataset_id}/exports |
ExportOptions → capturing ExportJob
|
GET /datasets/{dataset_id}/exports/{job_id} |
Current ExportJob
|
POST /datasets/{dataset_id}/exports/{job_id}/start |
Ready job → building job |
POST /datasets/{dataset_id}/exports/{job_id}/cancel |
Cancellation state |
GET /datasets/{dataset_id}/exports/{job_id}/download |
Verified streamed ZIP attachment |
HEAD /datasets/{dataset_id}/exports/{job_id}/download |
Same authorization and integrity checks, without a body |
Options include profile, task/class identities, a nonempty splits filter, a
required fallback split, and optional per-image split choices. Omitting splits
preserves the previous all-splits behavior. Capabilities advertise absent size
quotas as null; explicitly configured numeric limits remain supported. Completed downloads send application/zip,
a safe generated filename, content length, private, no-store, and nosniff.
Download checks authorization again after checksum I/O. The browser client
uses HEAD before returning the attachment URL and does not buffer the archive.
Missing jobs return 404; busy workers, source changes, and invalid lifecycle operations return 409; limits return 413; incompatible selections and invalid source geometry return 422; storage and verification failures return a safe 500 category. Error messages exclude source paths and geometry. Preflight blockers and omissions are recorded in the job summary; see export.
SessionInfo from GET /me and POST /auth/local-admin includes
prelabelAvailable, true only when the server has a configured prelabel service.
This capability applies to server and browser execution. The UI reads it before
showing prelabel controls or making hint requests; false is a normal disabled
state. Clients default an absent field to false. Session responses remain
no-store; reload the web app after changing server configuration. Dataset-role
and CSRF checks still apply to prelabel operations.
All unsafe prelabel requests use the normal session/CSRF/origin checks.
PrelabelResponse includes generation, raw validated candidates, execution mode,
prepared-result status, and an optional signed browser grant. The shared client
filters candidates against its current annotation draft before display.
AnnotationBatchRequest.prelabelAcceptances maps new annotation IDs to exact
signed evidence. The server verifies it, constructs immutable prelabel origin,
and writes a human accepted/edited revision through the existing assignment
transaction. Raw client provenance cannot substitute for evidence. Historical
prelabel revision sources remain readable. See Model prelabels
for binding, reset, trust, resource limits, and failure behavior.
Dataset metadata and UpdateDatasetConfigRequest include
boundingBoxVisibility: { "iouThreshold": 0.9 }, defaulting to 0.9 when omitted.
The existing data-admin configuration route owns updates. Values must be finite
and within [0, 1]; invalid JSON values fail extraction. Image state responses
retain all annotation records and carry the current visibility policy so shared
clients can filter consistently. The policy does not authorize mutation of
hidden records or bypass assignment ownership. See
configuration and workflow policy.
Edit documentation in the Labello repository. The wiki is generated from the current documentation; archived plans stay in the repository.
- Labello documentation
- Labello
- Getting started
- Product glossary
- Annotation and review
- Stylus input
- Dataset administration
- Model prelabels
- Dataset import
- Guided box-to-skeleton migration
- Dataset export
- Assignment
- Contribution scoring
- Current limitations