7 Annotations
8 operations.
7.0.1 listAnnotationCategories
GET /api/annotation-categories
Lists the whole vocabulary of annotation categories, so a reviewer can pick one when the AI’s wording matches nothing in it.
Why it exists. ‡ Offer the vocabulary of problem classes a reviewer picks from, so a mark on a photograph is labelled from an agreed list rather than free text.
Called by — components/workspaces/category-editor.tsx · components/workspaces/condition-class-picker.tsx · modules/gallery/core/hooks/use-suggestion-review.ts · modules/gallery/overlays/components/image-viewer.tsx …and 1 more
Reached from — /datasets/<slug> · /w/data-explorer/anomalies/map · /w/data-explorer/assets · /w/data-explorer/categories · /w/data-explorer/insights · /w/integrity/assets …and 2 more
Every annotation category, for the reviewer choosing one when a suggestion’s label maps to none. Distinct from listDatasetAnnotationCategories, which summarizes the categories a dataset’s existing annotations already use, with counts and thumbnails. The vocabulary is per-organization (20260818160000_annotation_categories_organization_scope.sql): the read runs under the caller’s RLS and adds no filter of its own, so two callers in different organizations get different vocabularies from this request. This is one of the two vocabularies createEquipmentDamageMechanism grounds a finding in — the other is listDamageMechanisms’s API-571 catalog.
Authentication: CookieAuth, SupabaseAuth
Returns — limit · truncated · includeRetired · categories
Field names as the handler returns them; the source states no types for them.
Effect — reads annotation_categories
Parameters
| Name | In | Required | Type | Notes |
|---|---|---|---|---|
limit |
query | no | integer | |
includeRetired |
query | no | boolean |
Responses
| Status | Meaning | Body |
|---|---|---|
200 |
The annotation vocabulary. | |
401 |
Not authenticated. | Error |
500 |
Server error. | Error |
7.0.2 createAnnotationCategory
POST /api/annotation-categories
Adds a condition class the shared vocabulary does not yet contain, so an inspection can name what it actually found rather than picking the nearest wrong label.
Why it exists. ‡ Let an engineer name a condition the catalog does not yet contain, so the vocabulary can grow to fit what inspections actually find.
Called by — components/workspaces/category-editor.tsx
Reached from — /w/data-explorer/categories
Creates an annotation category in one organization’s vocabulary (P4 of the free-findings proposal, under D2: create, rename, retire — no delete, no merge). Sets no coco_category_id — nullable since 20260825160000 precisely so this operation would not have to invent one. A name already used in that organization is a 409, case-insensitively. Authorized by annotation_categories_insert_policy, not by this handler (CONTRACT-API-001): is_org_editor_secure admits an organization’s admins and members and excludes viewers.
Authentication: CookieAuth, SupabaseAuth
Returns — category
Field names as the handler returns them; the source states no types for them.
Effect — writes (insert) annotation_categories
Request body
| Field | Type | Required |
|---|---|---|
organization_id |
string (uuid) | no |
name |
string | no |
parent_category |
string | no |
subcategory |
string | no |
Responses
| Status | Meaning | Body |
|---|---|---|
201 |
The class was created. | |
400 |
Missing or invalid organization_id or name. | Error |
401 |
Not authenticated. | Error |
403 |
Forbidden — caller lacks the member/admin role in that organization. | Error |
409 |
A class with that name already exists in the organization. | Error |
500 |
Server error. | Error |
7.0.3 amendAnnotationCategory
PATCH /api/annotation-categories/{id}
Renames a condition class, or retires one that should no longer be offered — keeping every annotation and finding already recorded in its terms.
Why it exists. ‡ Rename a condition class, or retire one that should no longer be offered, so a shared vocabulary can be corrected and pruned without deleting the history recorded in its terms.
Called by — components/workspaces/category-editor.tsx
Reached from — /w/data-explorer/categories
Renames or retires an annotation category (P4 of the free-findings proposal, under D2: create, rename, retire — no delete, no merge). THERE IS NO DELETE, deliberately: edm_observation_category_id_fkey is ON DELETE CASCADE, so deleting a class would take every finding grounded in it; retirement is reversible and destroys nothing. A RENAME does more than it looks — handle_annotation_category_update() rewrites images.tags across the class’s images. A class in another organization is absent rather than forbidden — 404. Authorized by annotation_categories_update_policy (is_org_editor_secure), not by this handler.
Authentication: CookieAuth, SupabaseAuth
Returns — category · changed
Field names as the handler returns them; the source states no types for them.
Effect — writes (update) annotation_categories
Parameters
| Name | In | Required | Type | Notes |
|---|---|---|---|---|
id |
path | yes | string (uuid) |
Request body
| Field | Type | Required |
|---|---|---|
name |
string | no |
parent_category |
string | no |
subcategory |
string | no |
retired |
boolean | no |
Responses
| Status | Meaning | Body |
|---|---|---|
200 |
The class as it now stands. changed is false when nothing differed. |
|
400 |
No amendable field given, or an invalid one. | Error |
401 |
Not authenticated. | Error |
403 |
Forbidden — caller can read the class but lacks the member/admin role. | Error |
404 |
No such class, or it belongs to an organization the caller is not a member of. | Error |
409 |
Another class in the organization already has that name. | Error |
500 |
Server error. | Error |
7.0.4 appendAnnotationReviewDecisions
POST /api/annotation-review-decisions
Records what a reviewer decided about one or more AI proposals — accept, reject, or recategorize — and, for the ones they accepted, turns them into real annotations in the same step. Decisions are added, never overwritten: a rejection later changed to an acceptance leaves both, with who did it and when. A proposal whose wording matches no category cannot be accepted until someone picks one.
Why it exists. ‡ Record what a reviewer decided about a machine suggestion — accept, reclassify or reject — and promote an accepted one into a real mark, so the decision is auditable and the detector can be measured against it.
Called by — modules/gallery/core/hooks/use-suggestion-review.ts
Reached from — /datasets/<slug> · /w/data-explorer/anomalies/map · /w/data-explorer/assets · /w/integrity/assets · /w/integrity/evidence-explorer
Appends what a reviewer decided about one or more annotation suggestions and, for an accept or a reclassify, promotes the suggestion to an annotation in the same transaction (the Supabase client cannot express a transaction itself, so the work happens in review_annotation_suggestions, a SECURITY INVOKER function — a transaction boundary, not a privilege boundary; the editor-tier insert policies on both tables still decide). Decisions are appended, never edited: a reject later changed to an accept leaves both rows with their actors and times. Refusals are per item and arrive with a 200 — a reviewer who accepted nine good boxes does not lose all nine because the tenth carried a label that maps to no category. Read recorded, not the status code.
Authentication: CookieAuth, SupabaseAuth
Returns — recorded · refused · results
Field names as the handler returns them; the source states no types for them.
Responses
| Status | Meaning | Body |
|---|---|---|
200 |
What happened to each decision. Includes the case where every one of them was refused: the request itself was understood. | |
400 |
Malformed body, an unknown decision word, or more than 200 decisions in one request. | Error |
401 |
Not authenticated. | Error |
500 |
The decisions could not be recorded. | Error |
7.0.5 listAnnotations
GET /api/annotations
Lists the annotations drawn on a specific photo.
Why it exists. ‡ List the marks recorded on photographs, with their classes resolved, for the panels that show what was found.
Called by — components/bounding-box-visualizer.tsx · modules/gallery/overlays/components/image-viewer.tsx · modules/gallery/overlays/components/pairs-viewer.tsx
Reached from — /datasets/<slug> · /w/data-explorer/anomalies/map · /w/data-explorer/assets · /w/integrity/assets · /w/integrity/evidence-explorer
Retrieves all annotations for a specific image, with each annotation’s category resolved to a name. imageId accepts a UUID or, for a caller that only has one, a filename — the route resolves either to the underlying image before reading its annotations. Runs as the caller: images is readable only from the caller’s organizations’ datasets, and annotations only from those images.
Authentication: CookieAuth, SupabaseAuth
Returns — annotations · count
Field names as the handler returns them; the source states no types for them.
Effect — reads annotation_categories · annotations · images
Parameters
| Name | In | Required | Type | Notes |
|---|---|---|---|---|
imageId |
query | yes | string |
Responses
| Status | Meaning | Body |
|---|---|---|
200 |
A list of annotations for the image. | Annotation |
400 |
Bad request, imageId is required. | Error |
401 |
Not authenticated. | Error |
500 |
Server error. | Error |
7.0.6 reclassifyAnnotation
PATCH /api/annotations/{id}
Reclassifies one stored annotation to another category. Only an editor of the owning organization may do it; the act is written to an append-only edit log.
Why it exists. ‡ Re-label a mark on a photograph when the class it carries is wrong, keeping who changed it and when.
Called by — modules/gallery/overlays/components/stored-annotations-panel.tsx
Reached from — /datasets/<slug> · /w/data-explorer/anomalies/map · /w/data-explorer/assets · /w/integrity/assets · /w/integrity/evidence-explorer
Changes the category of one stored annotation (user-editable anomalies, Step A). Authorization is the database’s: the editor-scoped RLS UPDATE policy on public.annotations governs the write, so an annotation outside the caller’s organizations is a 404 and a non-editor who can read it is a 403. The act is recorded in the append-only annotation_edits log.
Authentication: CookieAuth, SupabaseAuth
Returns — id · category_id
Field names as the handler returns them; the source states no types for them.
Effect — writes (insert, update) annotation_categories · annotation_edits · annotations
Parameters
| Name | In | Required | Type | Notes |
|---|---|---|---|---|
id |
path | yes | string |
Request body
| Field | Type | Required |
|---|---|---|
category_id |
string (uuid) | no |
Responses
| Status | Meaning | Body |
|---|---|---|
200 |
The annotation was reclassified. | |
400 |
category_id missing, or names no category. | Error |
401 |
Not authenticated. | Error |
403 |
The caller may read the annotation but is not an editor. | Error |
404 |
No such annotation the caller may read. | Error |
500 |
Server error. | Error |
7.0.7 deleteAnnotation
DELETE /api/annotations/{id}
Removes one stored annotation. The removal is recorded in the append-only edit log first — keeping the category, box and photo — so the audit survives the annotation. Editors only.
Why it exists. ‡ Remove a mark from a photograph, whether a person or the assistant put it there.
Called by — modules/gallery/overlays/components/stored-annotations-panel.tsx
Reached from — /datasets/<slug> · /w/data-explorer/anomalies/map · /w/data-explorer/assets · /w/integrity/assets · /w/integrity/evidence-explorer
Removes one stored annotation (user-editable anomalies, Step A). A remove record is written to the append-only annotation_edits log first, so the audit preserves the removed annotation’s category, bbox and image even after the row is gone. Authorization is the database’s: the editor-scoped RLS DELETE policy governs it, so an annotation outside the caller’s organizations is a 404 and a non-editor who can read it is a 403.
Authentication: CookieAuth, SupabaseAuth
Returns — id · deleted
Field names as the handler returns them; the source states no types for them.
Effect — writes (delete, insert) annotation_edits · annotations
Parameters
| Name | In | Required | Type | Notes |
|---|---|---|---|---|
id |
path | yes | string |
Responses
| Status | Meaning | Body |
|---|---|---|
200 |
The annotation was removed. | |
401 |
Not authenticated. | Error |
403 |
The caller may read the annotation but is not an editor. | Error |
404 |
No such annotation the caller may read. | Error |
500 |
Server error. | Error |
7.0.8 createAnnotation
POST /api/images/{id}/annotations
Adds a new human-drawn anomaly to a photo from a rectangle and a category, recorded as a human annotation. Only an editor of the owning organization may do it; the act is written to an append-only edit log.
Why it exists. ‡ Let an inspector draw a problem on a photograph by hand, rather than only accepting what the assistant proposed.
Called by — modules/gallery/overlays/components/image-viewer.tsx
Reached from — /datasets/<slug> · /w/data-explorer/anomalies/map · /w/data-explorer/assets · /w/integrity/assets · /w/integrity/evidence-explorer
Creates a human-drawn annotation on the image from a normalized bounding box and a category (user-editable anomalies, Step B). The row is stamped source: 'human' and created_by. Authorization is the database’s: the editor-scoped RLS INSERT policy on public.annotations governs the write, so a caller who can see the image but is not an editor of its organization is a 403. The act is recorded in the append-only annotation_edits log.
Authentication: CookieAuth, SupabaseAuth
Returns — id · category_id · bbox
Field names as the handler returns them; the source states no types for them.
Effect — writes (insert) annotation_categories · annotation_edits · annotations
Parameters
| Name | In | Required | Type | Notes |
|---|---|---|---|---|
id |
path | yes | string |
Request body
| Field | Type | Required |
|---|---|---|
bbox |
array of number | no |
category_id |
string (uuid) | no |
area |
number | no |
Responses
| Status | Meaning | Body |
|---|---|---|
200 |
The annotation was created. | |
400 |
bbox invalid, or category_id missing / names no category. | Error |
401 |
Not authenticated. | Error |
403 |
The caller may view the image but is not an editor. | Error |
500 |
Server error. | Error |