19  Images

23 operations.

19.0.1 listImages

GET /api/images

Searches images across datasets by tag, free text, thermal type or a geographic radius — the general-purpose image query behind search and the AI tools.

Why it exists. ‡ Search images across datasets by tag, free text, thermal type or a geographic radius. The general-purpose image query behind search and the AI tools, as opposed to the dataset-scoped gallery listing.

Called by — lib/workspaces/queries/candidates.ts

Reached from — /progress · /w/data-explorer/anomalies · /w/integrity/campaigns · /w/integrity/evidence-explorer · /w/integrity/insights · /w/integrity/investigate

Search/filter/paginate images with dataset, tag, filename, and thermal-type filters, optionally inlining base64 thumbnails; results are RLS-scoped to the caller’s datasets. lat/lng/radius parameters are accepted but not applied.

Authentication: CookieAuth, SupabaseAuth

Uses a service-role client (row-level security bypassed).

Returns — not readable from the source: the handler returns a value assembled at run time rather than a literal.

Effect — reads datasets · images

Parameters

Name In Required Type Notes
dataset_id query no string (uuid)
dataset_slug query no string
thermal_type query no string — one of thermal, rgb
tags query no string
search query no string
limit query no integer
offset query no integer
base64 query no string — one of true
lat query no number
lng query no number
radius query no number

Responses

Status Meaning Body
200 Matching images, paginated.
400 Malformed lat/lng/radius. Error
401 Not authenticated. Error
500 Server error. Error
503 Geographic search is not available on this deployment. Error

19.0.2 resolveImageReferences

POST /api/images

Fetches up to fifty images by id in one request, so a chat answer that references several images can resolve them all at once.

Why it exists. ‡ Fetch up to fifty images by id in one request, so a chat answer that references several images can resolve them all at once instead of issuing a request per image.

Called by — app/api/test-image-visualization/route.ts · modules/gallery/services/resolve-image-refs.ts

Batch-fetches metadata for up to 50 images by id — filename, thumbnail_url, dataset_name, dataset_slug — optionally attaching base64 payloads (RLS-scoped — images outside the caller’s datasets are silently omitted); ready_for_ai counts images whose base64 fetch succeeded. Also the metadata resolver for MARKDOWN_REPORT image references (AG-UI contract §7.4.1 form D), which uses the per-image dataset_slug for click-to-viewer URLs. It is a frontend resolver, not a skill input: preparing media for an image skill is POST /api/media/resolve (ADR-006), which carries no slug, path, or URL. The ready_for_ai counter reports base64 fetch success and does not mean an analysis ran.

Authentication: SupabaseAuth

Uses a service-role client (row-level security bypassed).

Returns — batch_id · images · analysis_type · total_processed · ready_for_ai · message

Field names as the handler returns them; the source states no types for them.

Effect — reads images

Request body

Field Type Required
image_ids array of string yes
analysis_type string no
batch_request boolean no
include_base64 boolean no

Responses

Status Meaning Body
200 Resolved images.
400 image_ids array is required, and at most 50 entries. Error
401 Not authenticated. Error
500 Server error. Error

19.0.3 listImageCandidates

GET /api/images/candidates

Lists a campaign’s review candidates — tagged images awaiting engineering attention — with their review state and asset attribution.

Scans up to limit images in a dataset (default 200) and returns every one carrying at least one tag, each with its priority (crit/mod/low, from its own tags), review state (new/confirmed/dismissed, from decided equipment_damage_mechanisms citing it), and asset attribution (nearest active image_asset_link, explicit only — never image_asset_proximity geo-guessing). Backed by public.v_image_review_candidates so this and Kawa’s equivalent tool compute the same answer from the same definition. scanned is the total images considered (tagged and untagged), matching kap_list_candidates’ “N of M images scanned” framing.

Authentication: CookieAuth, SupabaseAuth

Returns — datasetSlug · scanned · candidates

Field names as the handler returns them; the source states no types for them.

Effect — reads v_image_review_candidates

Parameters

Name In Required Type Notes
dataset_slug query yes string
limit query no integer

Responses

Status Meaning Body
200 Review candidates for the dataset, plus the total images scanned.
400 dataset_slug is required. Error
401 Not authenticated. Error
500 Server error. Error

19.0.4 getImageExif

GET /api/images/exif

Reads the EXIF block out of one stored image — camera, GPS, timestamp — for inspecting what the camera actually recorded.

Why it exists. † Read the EXIF block out of one stored image and return it, for inspecting what the camera actually recorded when a position or timestamp looks wrong.

Extracts EXIF metadata for a given image by ID (RLS-scoped — an image outside the caller’s datasets 404s). Not an agent tool yet. The response echoes the stored filename, which the UUID-only boundary excludes: filenames in this domain encode capture structure (_T. / _V., mission and path fragments), and a model handed one starts reasoning about it and repeating it in chat. Promoting this operation needs the field dropped from the handler first — a behaviour change, not a spec edit.

Authentication: CookieAuth, SupabaseAuth

Returns — success · imageId · filename · exif

Field names as the handler returns them; the source states no types for them.

Effect — reads datasets · images

Parameters

Name In Required Type Notes
imageId query yes string

Responses

Status Meaning Body
200 EXIF data (possibly empty if none was found).
400 Missing imageId, invalid storage path, or unsupported provider. Error
401 Not authenticated. Error
404 Image or dataset not found. Error
500 Server error. Error
501 Not implemented for SharePoint-backed datasets. Error

19.0.5 extractExifFromUpload

POST /api/images/exif

Reads the EXIF block out of image bytes posted directly, so a file can be inspected before it is ingested into a dataset.

Why it exists. † Read the EXIF block out of image bytes posted directly, so a file can be inspected before it is ingested into a dataset.

Extracts EXIF metadata from a binary image upload (buffer in body).

Authentication: CookieAuth, SupabaseAuth

Returns — success · exif

Field names as the handler returns them; the source states no types for them.

Responses

Status Meaning Body
200 EXIF data (possibly empty if none was found).
400 Missing or empty request body. Error
401 Not authenticated. Error
500 Server error. Error

19.0.6 extractImageMetadata

GET /api/images/extract-metadata

Extracts and stores one image’s capture metadata — position, camera, timestamp — from the image itself.

Why it exists. ‡ Extract and store one image’s capture metadata — position, camera, timestamp — from the image itself, for repairing a record whose metadata is missing or wrong.

Called by — 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

Extracts and stores metadata for a given image by ID. Access is checked under RLS (404 when out of scope); the metadata write is service-role.

Authentication: CookieAuth, SupabaseAuth

Returns — success · image

Field names as the handler returns them; the source states no types for them.

Effect — writes (update) datasets · images

Parameters

Name In Required Type Notes
imageId query yes string

Responses

Status Meaning Body
200 Metadata extracted and stored.
400 Missing imageId, invalid storage path, or unsupported provider. Error
401 Not authenticated. Error
404 Image or dataset not found. Error
500 Server error. Error

19.0.7 extractDatasetImageMetadata

POST /api/images/extract-metadata

Extracts capture metadata for a batch of a dataset’s images in one pass, to backfill a dataset indexed without EXIF.

Why it exists. ‡ Extract capture metadata for a batch of a dataset’s images in one pass, so a dataset indexed without EXIF can be backfilled without a request per image.

Called by — app/(authenticated)/datasets/[slug]/components/extract-metadata-button.tsx

Reached from — /datasets/<slug>

Extracts and stores metadata for all images in a dataset, optionally limited. Access is checked under RLS (404 when out of scope); the metadata writes are service-role.

Authentication: CookieAuth, SupabaseAuth

Returns — success · message · processed · totalImages · successCount · failedCount · foundXMPCount · totalImagesInBatch

Field names as the handler returns them; the source states no types for them.

Effect — writes (update) datasets · images

Request body

Field Type Required
datasetId string yes
limit integer no

Responses

Status Meaning Body
200 Processed (possibly zero images, if none needed it).
400 Missing datasetId. Error
401 Not authenticated. Error
404 Dataset not found. Error
500 Server error. Error

19.0.8 searchDatasetImages

GET /api/images/search

Searches a dataset’s images by filename, tag or description, with pagination and sorting, optionally restricted to GPS-tagged images.

Why it exists. ‡ Search a dataset’s images by filename or description with pagination and sorting, optionally restricted to images that carry GPS. The gallery’s search box.

Called by — components/image-search.tsx · modules/gallery/core/hooks/use-gallery-search-controller.ts · modules/gallery/ui/components/image-gallery-renderer.tsx

Reached from — /datasets/<slug> · /w/data-explorer/anomalies/map · /w/data-explorer/assets · /w/integrity/assets · /w/integrity/evidence-explorer

Searches images in a dataset by tags or metadata; RLS-scoped, so an inaccessible dataset behaves like a missing one (404 via slug, empty via id).

Authentication: CookieAuth, SupabaseAuth

Returns — success · images · totalImages · currentPage · totalPages · pageSize · filterOptions · searchQuery · searchStats

Field names as the handler returns them; the source states no types for them.

Effect — reads datasets · images

Parameters

Name In Required Type Notes
datasetId query no string
datasetSlug query no string
query query no string
exact query no string — one of true, false
gpsOnly query no string — one of true, false
sort query no string — one of filename_asc, filename_desc
page query no integer
pageSize query no integer

Responses

Status Meaning Body
200 Matching images, paginated.
400 Missing datasetId or datasetSlug. Error
401 Not authenticated. Error
404 Dataset not found for the given slug. Error
500 Server error. Error

19.0.9 listImageSuggestions

GET /api/images/suggestions

Suggests tag completions from the tags actually present in a dataset, so the search box offers terms that will return results.

Why it exists. ‡ Suggest tag completions from the tags actually present in a dataset, so the search box offers terms that will return results rather than a fixed vocabulary.

Called by — modules/gallery/core/hooks/use-gallery-search-controller.ts

Reached from — /datasets/<slug> · /w/data-explorer/anomalies/map · /w/data-explorer/assets · /w/integrity/assets · /w/integrity/evidence-explorer

Suggests tags or metadata values for images in a dataset based on a query string; RLS-scoped, so an inaccessible dataset yields empty suggestions.

Authentication: CookieAuth, SupabaseAuth

Returns — success · suggestions

Field names as the handler returns them; the source states no types for them.

Effect — reads annotation_categories · images

Parameters

Name In Required Type Notes
datasetId query yes string
query query no string
limit query no integer
all query no string — one of true, false

Responses

Status Meaning Body
200 Suggested completions.
400 Missing datasetId. Error
401 Not authenticated. Error
500 Server error. Error

19.0.10 listDatasetThumbnails

GET /api/images/thumbnails

Returns a dataset’s thumbnails for preview strips.

Why it exists. † Return a dataset’s thumbnails for preview strips. Accepts either a browser session or a bearer token, having been built for tool callers as well as the interface.

Thumbnail list for a dataset identified by slug, with optional thermal (_T.) / rgb (_V.) filename filtering. Accepts a bearer token (forwarded to Supabase, RLS applies) or the session cookie.

Authentication: SupabaseAuth, CookieAuth

Returns — images · dataset · count

Field names as the handler returns them; the source states no types for them.

Effect — reads datasets · images

Parameters

Name In Required Type Notes
dataset_slug query yes string
limit query no integer
type query no string — one of thermal, rgb

Responses

Status Meaning Body
200 Thumbnails.
400 dataset_slug is required. Error
401 Not authenticated. Error
404 Dataset not found. Error

19.0.11 getImage

GET /api/images/{id}

Fetches one image’s stored record — metadata, tags, and any analysis attached to it — optionally with the bytes inlined.

Why it exists. ‡ Fetch one image’s stored record — its metadata, tags and any analysis attached to it — optionally with the image bytes inlined for a caller that cannot follow a storage URL.

Called by — app/(authenticated)/w/integrity/investigate/investigate-client.tsx · components/main-panel-renderer.tsx · components/workspaces/widgets/image-viewer-dialog.tsx · lib/mcp/manifest.ts …and 2 more

Reached from — /datasets/<slug> · /w/data-explorer/anomalies/map · /w/data-explorer/assets · /w/data-explorer/categories · /w/data-explorer/new-dataset · /w/data-explorer/storage …and 9 more

Single image metadata (RLS-scoped — an image outside the caller’s datasets 404s), optionally with base64 fields (base64=true); format=base64 (combined with base64=true) returns the raw base64 body with the image’s content type instead of JSON.

Authentication: CookieAuth, SupabaseAuth

Uses a service-role client (row-level security bypassed).

Returns — not readable from the source: the handler returns a value assembled at run time rather than a literal.

Effect — reads images

Parameters

Name In Required Type Notes
id path yes string

Responses

Status Meaning Body
200 Image details.
401 Not authenticated. Error
404 Image not found. Error
500 Server error. Error

19.0.12 prepareImageForAnalysis

POST /api/images/{id}

Deprecated. A placeholder for single-photo AI analysis that never performed any — it echoes the photo back. Preparing a photo for analysis is POST /media/resolve; the analysis itself runs in the AI backend.

Why it exists. † Return one image packaged for AI analysis. A placeholder: it prepares the payload and says so rather than performing an analysis, which has not been wired up.

Deprecated. Stub analysis entry point — re-fetches its own GET with base64 (forwarding the caller’s credentials) and echoes the payload marked ready_for_ai. No analysis is performed, and none ever was. It predates ADR-006 and claims a job two other surfaces now do properly: POST /api/media/resolve prepares media for a skill under the UUID-only boundary, and image analysis itself runs through the gateway’s skill-execution endpoint (ADR-007). Kept only until its callers are confirmed gone; a new caller should use neither this nor a base64 echo.

Authentication: CookieAuth, SupabaseAuth

Uses a service-role client (row-level security bypassed).

Returns — not readable from the source: the handler returns a value assembled at run time rather than a literal.

Parameters

Name In Required Type Notes
id path yes string

Request body

Field Type Required
analysis_type string no
request_base64 boolean no

Responses

Status Meaning Body
200 Packaged.
400 Base64 image data required for AI processing. Error
401 Not authenticated. Error
500 Server error. Error

19.0.13 analyzeImage

POST /api/images/{id}/analyze

Dispatches one image to the AI gateway’s analysis skill and records the attempt with its suggestions.

Why it exists. ‡ Analyse one photograph on request from the viewer, so an engineer can ask for a reading directly instead of hoping an assistant chooses to look.

Called by — modules/gallery/core/hooks/use-batch-analysis.ts · 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

Runs an image skill over a single image and records what it found. This route dispatches; it does not analyze. The skill executes in the AI gateway (ADR-007), which resolves the image to prepared bytes as the caller (ADR-006) and returns the canonical ImageAnalysisResult. The caller’s own JWT is what travels — never a platform credential, or media resolution would authorize the platform instead of the person. The image is authorized under the caller’s scope before anything is dispatched, so an image outside their organization is a 404 and costs no model invocation. The caller’s tier is checked before dispatch too. Recording an attempt is editor-tier work, so a viewer’s analysis would otherwise run, cost a model invocation, and be discarded at the point of storage — reported as a failure to record, which is not what happened. A viewer gets 403: they can see the image, so a 404 would only mislead. The attempt and its suggestions are written in one transaction. A gateway refusal records nothing at all: an attempt row marks an image analyzed, and the “already analyzed” test reads attempts. Findings without geometry are not stored as suggestions — a reviewer cannot draw or accept a box that has no region.

Authentication: SupabaseAuth

Returns — attempt · suggestions

Field names as the handler returns them; the source states no types for them.

Effect — reads annotation_suggestions · images

Parameters

Name In Required Type Notes
id path yes string

Request body

Field Type Required
skill_id string no
skill_version string no

Responses

Status Meaning Body
200 Analysis attempt recorded.
401 Not authenticated, or no access token available to authorize media resolution. Error
403 Analysis requires member or admin access to this dataset. Error
404 Image not found, or the analysis skill is unknown. Error
500 Analysis succeeded but could not be recorded. Error
502 Analysis service rejected the request, or returned no result. Error
503 Analysis service unreachable. Error

19.0.14 addImageAssetTag

POST /api/images/{id}/asset-tag

Confirms that a photograph shows a particular piece of equipment, promoting an OCR-read suggestion to a confirmed tag.

Why it exists. ‡ Confirm that a photograph shows a particular piece of equipment, moving it from ‘taken nearby’ to evidence about that asset.

Confirms (approves) that an image shows an equipment asset, promoting an OCR-read suggestion to a confirmed tag. The tag is added to the image’s tags set (the confirmed store), and the approval — who confirmed it and when — is appended to metadata.tag_approvals for the audit trail. The call is idempotent: a tag already confirmed returns 200 with the current tags and adds no duplicate. The tag must name a real asset in the register; an arbitrary string is refused (400) so only known equipment can enter tags. Authorized by the database, not by this handler (CONTRACT-API-001): the editor-scoped RLS policy on public.images (“Users can update images in their datasets”, via is_org_editor_secure) governs the write. An image outside the caller’s organizations is a 404, indistinguishable from one that does not exist; a caller who can read the image but is not an editor gets a 403 that names the role required. Pair propagation: a visual/thermal pair shares asset tags. When the image is a _V/_T member, the same confirmation is applied to its counterpart, identified by filename convention (not data_group_id, which is unpopulated). The counterpart id is returned as paired_image_id (null when there is none). Propagation is best-effort and RLS-scoped: a counterpart write RLS declines is silently skipped and never changes this response.

Authentication: CookieAuth, SupabaseAuth

Returns — image_id · tag · tags · paired_image_id

Field names as the handler returns them; the source states no types for them.

Effect — writes (update) equipment · images

Parameters

Name In Required Type Notes
id path yes string

Request body

Field Type Required
tag string yes

Responses

Status Meaning Body
200 Confirmed (idempotent — an already-confirmed tag returns the current state).
400 tag is required, or names no known asset. Error
401 Not authenticated. Error
403 You have read access to this image but not permission to edit it. Error
404 Image not found. Error
500 Server error. Error

19.0.15 removeImageAssetTag

DELETE /api/images/{id}/asset-tag

Withdraws a confirmed asset tag, for a photograph attributed to the wrong asset.

Why it exists. ‡ Withdraw that confirmation, for a photograph attributed to the wrong asset.

Removes (rejects) an asset tag from an image — the “reject” half of the suggest-then-approve model, the counterpart to the POST. The tag is dropped from the image’s tags set (the confirmed store), and the rejection — who rejected it and when — is appended to metadata.tag_rejections for the audit trail. A recorded rejection also suppresses the suggestion: getEquipment will not re-offer a tag this image was rejected as. The call is idempotent: rejecting a tag the image never carried still records the rejection and returns 200. Authorized by the database, not by this handler (CONTRACT-API-001): the editor-scoped RLS policy on public.images governs the write. An image outside the caller’s organizations is a 404; a caller who can read the image but is not an editor gets a 403 that names the role required. Pair propagation: a visual/thermal pair shares asset tags. When the image is a _V/_T member, the same rejection is applied to its counterpart, identified by filename convention (not data_group_id, which is unpopulated). The counterpart id is returned as paired_image_id (null when there is none). Propagation is best-effort and RLS-scoped: a counterpart write RLS declines is silently skipped and never changes this response.

Authentication: CookieAuth, SupabaseAuth

Returns — image_id · tag · tags · paired_image_id

Field names as the handler returns them; the source states no types for them.

Effect — writes (update) images

Parameters

Name In Required Type Notes
id path yes string

Request body

Field Type Required
tag string yes

Responses

Status Meaning Body
200 Rejected (idempotent — rejecting a tag the image never carried still records the rejection).
400 tag is required. Error
401 Not authenticated. Error
403 You have read access to this image but not permission to edit it. Error
404 Image not found. Error
500 Server error. Error

19.0.16 updateImageMetadata

PATCH /api/images/{id}/metadata

Shallow-merges a partial object into one image’s stored metadata, without resupplying the whole object.

Why it exists. † Amend one image’s stored metadata without resupplying the whole object, so a correction to a single field cannot silently discard the rest of the record.

Called by — scripts/phash-backfill.ts

Shallow-merges a partial object into an image’s metadata (jsonb). The merge is top-level only: a key in the patch replaces that key wholesale, and any top-level key absent from the patch is preserved. Authorized by the database, not by this handler (CONTRACT-API-001): the editor-scoped RLS policy on public.images (“Users can update images in their datasets”, via is_org_editor_secure) governs the write. An image outside the caller’s organizations is a 404, indistinguishable from one that does not exist; a caller who can read the image but is not an editor gets a 403 that names the role required.

Authentication: CookieAuth, SupabaseAuth

Returns — image

Field names as the handler returns them; the source states no types for them.

Effect — writes (update) images

Parameters

Name In Required Type Notes
id path yes string

Request body

Field Type Required
metadata object yes

Responses

Status Meaning Body
200 Updated.
400 metadata must be a JSON object. Error
401 Not authenticated. Error
403 You have read access to this image but not permission to edit it. Error
404 Image not found. Error
500 Server error. Error

19.0.17 getImageOcr

GET /api/images/{id}/ocr

Returns the OCR text a prior run stored for one image — a cheap read, no model invoked.

Why it exists. † Return what was previously read from a photograph’s placard, so a proposal can be reviewed without running the read again.

Returns the OCR text a prior run stored in the image’s metadata (images.metadata.ocr). A cheap read — no model is invoked and the image bytes are not downloaded. RLS-scoped — an image outside the caller’s datasets 404s. ocr is null when the image was never OCR’d; an ocr object with an empty texts array means it was OCR’d and no text was found.

Authentication: CookieAuth, SupabaseAuth

Returns — image_id · ocr

Field names as the handler returns them; the source states no types for them.

Effect — reads images

Parameters

Name In Required Type Notes
id path yes string

Responses

Status Meaning Body
200 ocr is null when the image was never OCR’d.
401 Not authenticated. Error
404 Image not found. Error
500 Server error. Error

19.0.18 extractImageText

POST /api/images/{id}/ocr

Dispatches one image to the AI gateway’s text-reading skill and persists the result onto the image’s metadata.

Why it exists. † Read the equipment ID placard in a photograph, so the platform can propose which asset the frame shows rather than leaving an engineer to identify it by eye.

Runs the open-vocabulary image.read-text skill over a single image and stores what it read. Like analyzeImage this route dispatches; it does not read text itself. The skill executes in the AI gateway (ADR-007), which resolves the image to prepared bytes as the caller (ADR-006). The caller’s own JWT is what travels — never a platform credential, or media resolution would authorize the platform instead of the person. The image is authorized under the caller’s scope before anything is dispatched, so an image outside their organization is a 404 and costs no model invocation. The caller’s tier is checked before dispatch too: running OCR and persisting its result is editor-tier work, so a viewer gets 403 (they can see the image, so a 404 would only mislead). Every stored read carries text, kind, confidence and region: the skill reads verbatim and returns a normalised box, and the route classifies each string as asset_tag (a plant equipment tag by the tag grammar) or text, because the image→asset link writers select on kind = 'asset_tag'. The read result is persisted onto images.metadata.ocr with a shallow top-level merge — the same merge updateImageMetadata performs — so the read is durable and the sibling GET /images/{id}/ocr reflects it.

Authentication: CookieAuth, SupabaseAuth

Returns — image_id · status · ocr

Field names as the handler returns them; the source states no types for them.

Effect — writes (update) images

Parameters

Name In Required Type Notes
id path yes string

Responses

Status Meaning Body
200 Text extracted and stored.
401 Not authenticated, or no access token available to authorize media resolution. Error
403 OCR requires member or admin access to this dataset. Error
404 Image not found, or the OCR skill is unknown. Error
500 OCR ran but its result could not be stored. Error
502 OCR service rejected the request, or returned no result. Error
503 OCR service unreachable. Error

19.0.19 getImagePair

GET /api/images/{id}/pair

Finds the visual or thermal counterpart of one image, so the two frames of a capture can be viewed together.

Why it exists. ‡ Find the visual or thermal counterpart of one image, so the two frames of a capture can be viewed together instead of being hunted for by filename.

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

The visual/thermal counterpart of one image, so the viewer can offer a side-by-side or overlay of the pair. A field capture produces two frames of the same scene — a visual (RGB) and a thermal — paired by filename convention only (not data_group_id, which is unpopulated). RLS-scoped: an image outside the caller’s datasets is a 404, and a counterpart the caller cannot see is simply absent. When there is no counterpart the response is { pair: null }; otherwise pair carries the counterpart’s id, filename, thumbnail URL, dataset slug (pairs are same-dataset), and kind — ‘rgb’ or ‘thermal’.

Authentication: CookieAuth, SupabaseAuth

Returns — pair

Field names as the handler returns them; the source states no types for them.

Effect — reads images

Parameters

Name In Required Type Notes
id path yes string

Responses

Status Meaning Body
200 The counterpart, or { pair: null } when there is none.
401 Not authenticated. Error
404 Image not found. Error
500 Server error. Error

19.0.20 listImageAnnotationSuggestions

GET /api/images/{id}/suggestions

Returns the machine’s proposed marks for one photograph, so a reviewer can see what was suggested without having been the one who asked.

Why it exists. ‡ Return the machine’s proposed marks for one photograph, so a reviewer can open the queue later and see what was suggested rather than only in the tab that asked for 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

What has been proposed for this image and where each proposal stands (WP-3). analyzeImage returns the suggestions it just produced, but only to the caller that asked; a reviewer who reloads, or who arrives at an image someone else analyzed, reads them back from here. Each suggestion carries its latest decision, which is a projection. The append-only log is the record; this operation answers “where does this stand now?”, not “what happened to it”. The attempts are here too, including the failed ones: an image that could not be analyzed otherwise looks exactly like an image with nothing wrong in it. The image is authorized under the caller’s own scope first, so one outside their organization 404s indistinguishably from a missing one.

Authentication: CookieAuth, SupabaseAuth

Returns — limit · truncated · attempts · suggestions

Field names as the handler returns them; the source states no types for them.

Effect — reads analysis_attempts · annotation_categories · annotation_review_decisions · annotation_suggestions · images

Parameters

Name In Required Type Notes
id path yes string
limit query no integer

Responses

Status Meaning Body
200 Suggestions with their latest review decision, if any.
401 Not authenticated. Error
404 Image not found. Error
500 Server error. Error

19.0.21 updateImageTags

PATCH /api/images/{id}/tags

Replaces the tags on one image, so a reviewer can correct or add to what a detector labelled it with.

Why it exists. ‡ Replace the tags on one image, so a reviewer can correct or add to what a detector labelled it with.

Called by — modules/gallery/overlays/components/image-metadata-panel.tsx

Reached from — /datasets/<slug> · /w/data-explorer/anomalies/map · /w/data-explorer/assets · /w/integrity/assets · /w/integrity/evidence-explorer

Updates the tags for a specific image. Access is checked under RLS (404 for an image outside the caller’s datasets); the write itself is service-role since images has no UPDATE policy.

Authentication: CookieAuth, SupabaseAuth

Uses a service-role client (row-level security bypassed).

Returns — success

Field names as the handler returns them; the source states no types for them.

Effect — writes (update) images

Parameters

Name In Required Type Notes
id path yes string

Request body

Field Type Required
tags array of string yes

Responses

Status Meaning Body
200 Updated.
400 Invalid request. Error
401 Not authenticated. Error
404 Image not found. Error
500 Server error. Error

19.0.22 getImageThumbnail

GET /api/images/{id}/thumbnail

Redirects to a small preview of one photograph, generating it on demand when none exists yet.

Why it exists. ‡ Return a small preview of one photograph, generating it on demand when none exists, so a gallery never renders a permanent blank for imagery that was simply never processed.

Redirects to the image’s 300×300 thumbnail. If the thumbnail does not exist yet it is generated on demand — resized from the original, stored in the thumbnails bucket, and recorded on images.thumbnail_url — and the redirect then points at the fresh URL. This is the single path every surface uses as its <img> source, so a missing thumbnail self-heals on first view regardless of which surface asked for it. RLS-scoped: an image outside the caller’s datasets 404s. Generation reads the image as the caller but writes the derived thumbnail with a platform credential, so a viewer who can see the image still receives a generated thumbnail rather than a blank. On a generation error the response redirects to a neutral placeholder instead of yielding a broken image.

Authentication: CookieAuth, SupabaseAuth

Uses a service-role client (row-level security bypassed).

Returns — not readable from the source: the handler returns a value assembled at run time rather than a literal.

Effect — writes (update) datasets · images

Parameters

Name In Required Type Notes
id path yes string

Responses

Status Meaning Body
302 Redirects to the thumbnail, or to a placeholder image if generation fails.
401 Not authenticated. Error
404 Image not found. Error

19.0.23 resolveMedia

POST /api/media/resolve

Host-facing: turns a batch of image ids into ready-to-analyze image bytes for the AI backends — each id is checked against the caller’s own access, and failures come back per-image.

Why it exists. † Hand an agent the prepared bytes of images it names by id, so it can look at them without ever holding a storage path, SAS URL, or credential.

The transport for the Agent System hosts’ request-scoped resolve_media capability (ADR-006). Resolves a batch of image UUIDs under the caller’s RLS scope into prepared media — validated, orientation-normalized bytes as base64 — or a per-UUID typed failure. An image outside the caller’s scope resolves not_found, indistinguishable from a missing one. The response never contains a SAS URL, storage path, or credential. Limits: 25 UUIDs per batch, 64MB per image, 150MP decode.

Authentication: SupabaseAuth

Returns — results

Field names as the handler returns them; the source states no types for them.

Request body

Field Type Required
imageIds array of string yes

Responses

Status Meaning Body
200 One entry per requested UUID.
400 Missing/empty imageIds, or batch over the limit. Error
401 Not authenticated. Error