From 492934126d72fde6a80f1d286bfebfc9f8e030ee Mon Sep 17 00:00:00 2001 From: Oleh Martsokha Date: Sun, 20 Sep 2026 13:16:20 +0200 Subject: [PATCH] Regenerate the specification for SDK 0.57 The workspace detection list moves from /workspaces/{id}/pipelines/detections to /workspaces/{id}/detections, resolving the odd nesting the cross-pipeline list had under /pipelines/, and a workspace-wide redaction list appears at /workspaces/{id}/redactions. Path count is unchanged at 91; schemas go from 392 to 395 with WorkspaceRedactionsQuery, ExifPolicy and ComponentSelection. No prose referenced the moved route, so the rewrite is limited to the new surface: - Document redactions.listRedactions, which lists a workspace's redactions across detections and narrows by detectionId or documentId, next to the existing getReview. - Document codec.exifPolicy (strip, strip_sensitive, retain) in supported formats, beside rasterMode. Co-Authored-By: Claude Opus 5 (1M context) --- api-reference/openapi.json | 534 +++++++++++++++++++++--------- features/supported-formats.mdx | 13 + sdks/typescript/api-reference.mdx | 10 +- 3 files changed, 394 insertions(+), 163 deletions(-) diff --git a/api-reference/openapi.json b/api-reference/openapi.json index 1527e95..b6125ed 100644 --- a/api-reference/openapi.json +++ b/api-reference/openapi.json @@ -7061,163 +7061,6 @@ ] } }, - "/workspaces/{workspaceId}/pipelines/detections": { - "get": { - "tags": [ - "Detections" - ], - "summary": "List workspace detections", - "description": "Returns all detections across the workspace, most recent first, with optional status, document, pipeline, trigger-account, and trigger-type filters.", - "parameters": [ - { - "in": "path", - "name": "workspaceId", - "description": "Workspace identifier.", - "required": true, - "schema": { - "description": "Workspace identifier.", - "type": "string", - "format": "uuid" - }, - "style": "simple" - }, - { - "in": "query", - "name": "after", - "description": "Cursor pointing to the last item of the previous page.\nObtain this from the `nextCursor` field in the response.", - "schema": { - "description": "Cursor pointing to the last item of the previous page.\nObtain this from the `nextCursor` field in the response.", - "type": "string" - }, - "style": "form" - }, - { - "in": "query", - "name": "includeCount", - "description": "Whether to include the total item count in the response's `total` field.\nDefaults to `false`, since counting is an extra query; set it to `true`\nonly when the count is actually needed.", - "schema": { - "description": "Whether to include the total item count in the response's `total` field.\nDefaults to `false`, since counting is an extra query; set it to `true`\nonly when the count is actually needed.", - "type": "boolean", - "default": false - }, - "style": "form" - }, - { - "in": "query", - "name": "limit", - "description": "The maximum number of records to return (1-100, default: 20).", - "schema": { - "description": "The maximum number of records to return (1-100, default: 20).", - "type": "integer", - "format": "uint32", - "maximum": 100, - "minimum": 1 - }, - "style": "form" - }, - { - "in": "query", - "name": "documentId", - "description": "Filter by the source document the detection analyzes.", - "schema": { - "description": "Filter by the source document the detection analyzes.", - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "pipelineId", - "description": "Filter by the owning pipeline.", - "schema": { - "description": "Filter by the owning pipeline.", - "type": "string", - "format": "uuid" - }, - "style": "form" - }, - { - "in": "query", - "name": "status", - "description": "Filter by detection status.", - "schema": { - "description": "Filter by detection status.", - "$ref": "#/components/schemas/DetectionStatus" - }, - "style": "form" - }, - { - "in": "query", - "name": "triggerType", - "description": "Filter by how the detection was initiated (user vs system).", - "schema": { - "description": "Filter by how the detection was initiated (user vs system).", - "$ref": "#/components/schemas/PipelineTriggerType" - }, - "style": "form" - }, - { - "in": "query", - "name": "triggeredBy", - "description": "Filter by the account that triggered the detection.", - "schema": { - "description": "Filter by the account that triggered the detection.", - "type": "string", - "format": "uuid" - }, - "style": "form" - } - ], - "responses": { - "200": { - "description": "Generic paginated response wrapper.\n\nProvides a consistent structure for all paginated API responses with\ncursor-based pagination support. When `next_cursor` is present, there\nare more items to fetch.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorkspaceDetectionPage" - } - } - } - }, - "401": { - "description": "The serialized shape of an HTTP error: the inert wire/OpenAPI-schema view\nthat [`Error`] renders to at the response boundary.\n\nIt carries no builder logic \u2014 [`Error`] is the type handlers construct and\nthread through `Result`, and it builds an `ErrorResponse` directly in its\n`IntoResponse` impl. `context` and `status` are not part of the JSON body\n(`context` is logged, `status` sets the HTTP status line).\n\n[`Error`]: crate::response::Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "403": { - "description": "The serialized shape of an HTTP error: the inert wire/OpenAPI-schema view\nthat [`Error`] renders to at the response boundary.\n\nIt carries no builder logic \u2014 [`Error`] is the type handlers construct and\nthread through `Result`, and it builds an `ErrorResponse` directly in its\n`IntoResponse` impl. `context` and `status` are not part of the JSON body\n(`context` is logged, `status` sets the HTTP status line).\n\n[`Error`]: crate::response::Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "The serialized shape of an HTTP error: the inert wire/OpenAPI-schema view\nthat [`Error`] renders to at the response boundary.\n\nIt carries no builder logic \u2014 [`Error`] is the type handlers construct and\nthread through `Result`, and it builds an `ErrorResponse` directly in its\n`IntoResponse` impl. `context` and `status` are not part of the JSON body\n(`context` is logged, `status` sets the HTTP status line).\n\n[`Error`]: crate::response::Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [ - { - "BearerAuth": [] - } - ] - } - }, "/workspaces/{workspaceId}/pipelines/{pipelineId}/detections": { "get": { "tags": [ @@ -7518,6 +7361,161 @@ } }, "/workspaces/{workspaceId}/detections": { + "get": { + "tags": [ + "Detections" + ], + "summary": "List workspace detections", + "description": "Returns all detections across the workspace, most recent first, with optional status, document, pipeline, trigger-account, and trigger-type filters.", + "parameters": [ + { + "in": "path", + "name": "workspaceId", + "description": "Workspace identifier.", + "required": true, + "schema": { + "description": "Workspace identifier.", + "type": "string", + "format": "uuid" + }, + "style": "simple" + }, + { + "in": "query", + "name": "after", + "description": "Cursor pointing to the last item of the previous page.\nObtain this from the `nextCursor` field in the response.", + "schema": { + "description": "Cursor pointing to the last item of the previous page.\nObtain this from the `nextCursor` field in the response.", + "type": "string" + }, + "style": "form" + }, + { + "in": "query", + "name": "includeCount", + "description": "Whether to include the total item count in the response's `total` field.\nDefaults to `false`, since counting is an extra query; set it to `true`\nonly when the count is actually needed.", + "schema": { + "description": "Whether to include the total item count in the response's `total` field.\nDefaults to `false`, since counting is an extra query; set it to `true`\nonly when the count is actually needed.", + "type": "boolean", + "default": false + }, + "style": "form" + }, + { + "in": "query", + "name": "limit", + "description": "The maximum number of records to return (1-100, default: 20).", + "schema": { + "description": "The maximum number of records to return (1-100, default: 20).", + "type": "integer", + "format": "uint32", + "maximum": 100, + "minimum": 1 + }, + "style": "form" + }, + { + "in": "query", + "name": "documentId", + "description": "Filter by the source document the detection analyzes.", + "schema": { + "description": "Filter by the source document the detection analyzes.", + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "pipelineId", + "description": "Filter by the owning pipeline.", + "schema": { + "description": "Filter by the owning pipeline.", + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "status", + "description": "Filter by detection status.", + "schema": { + "description": "Filter by detection status.", + "$ref": "#/components/schemas/DetectionStatus" + }, + "style": "form" + }, + { + "in": "query", + "name": "triggerType", + "description": "Filter by how the detection was initiated (user vs system).", + "schema": { + "description": "Filter by how the detection was initiated (user vs system).", + "$ref": "#/components/schemas/PipelineTriggerType" + }, + "style": "form" + }, + { + "in": "query", + "name": "triggeredBy", + "description": "Filter by the account that triggered the detection.", + "schema": { + "description": "Filter by the account that triggered the detection.", + "type": "string", + "format": "uuid" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "Generic paginated response wrapper.\n\nProvides a consistent structure for all paginated API responses with\ncursor-based pagination support. When `next_cursor` is present, there\nare more items to fetch.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkspaceDetectionPage" + } + } + } + }, + "401": { + "description": "The serialized shape of an HTTP error: the inert wire/OpenAPI-schema view\nthat [`Error`] renders to at the response boundary.\n\nIt carries no builder logic \u2014 [`Error`] is the type handlers construct and\nthread through `Result`, and it builds an `ErrorResponse` directly in its\n`IntoResponse` impl. `context` and `status` are not part of the JSON body\n(`context` is logged, `status` sets the HTTP status line).\n\n[`Error`]: crate::response::Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The serialized shape of an HTTP error: the inert wire/OpenAPI-schema view\nthat [`Error`] renders to at the response boundary.\n\nIt carries no builder logic \u2014 [`Error`] is the type handlers construct and\nthread through `Result`, and it builds an `ErrorResponse` directly in its\n`IntoResponse` impl. `context` and `status` are not part of the JSON body\n(`context` is logged, `status` sets the HTTP status line).\n\n[`Error`]: crate::response::Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The serialized shape of an HTTP error: the inert wire/OpenAPI-schema view\nthat [`Error`] renders to at the response boundary.\n\nIt carries no builder logic \u2014 [`Error`] is the type handlers construct and\nthread through `Result`, and it builds an `ErrorResponse` directly in its\n`IntoResponse` impl. `context` and `status` are not part of the JSON body\n(`context` is logged, `status` sets the HTTP status line).\n\n[`Error`]: crate::response::Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "BearerAuth": [] + } + ] + }, "post": { "tags": [ "Detections" @@ -8315,6 +8313,132 @@ ] } }, + "/workspaces/{workspaceId}/redactions": { + "get": { + "tags": [ + "Redactions" + ], + "summary": "List workspace redactions", + "description": "Returns all redactions across the workspace, most recent first, cursor-paginated, with optional detection and document filters. Each redaction is one redact pass with its own reviewer edits, output document, and review audit.", + "parameters": [ + { + "in": "path", + "name": "workspaceId", + "description": "Workspace identifier.", + "required": true, + "schema": { + "description": "Workspace identifier.", + "type": "string", + "format": "uuid" + }, + "style": "simple" + }, + { + "in": "query", + "name": "after", + "description": "Cursor pointing to the last item of the previous page.\nObtain this from the `nextCursor` field in the response.", + "schema": { + "description": "Cursor pointing to the last item of the previous page.\nObtain this from the `nextCursor` field in the response.", + "type": "string" + }, + "style": "form" + }, + { + "in": "query", + "name": "includeCount", + "description": "Whether to include the total item count in the response's `total` field.\nDefaults to `false`, since counting is an extra query; set it to `true`\nonly when the count is actually needed.", + "schema": { + "description": "Whether to include the total item count in the response's `total` field.\nDefaults to `false`, since counting is an extra query; set it to `true`\nonly when the count is actually needed.", + "type": "boolean", + "default": false + }, + "style": "form" + }, + { + "in": "query", + "name": "limit", + "description": "The maximum number of records to return (1-100, default: 20).", + "schema": { + "description": "The maximum number of records to return (1-100, default: 20).", + "type": "integer", + "format": "uint32", + "maximum": 100, + "minimum": 1 + }, + "style": "form" + }, + { + "in": "query", + "name": "detectionId", + "description": "Filter to the redactions produced from a specific detection.", + "schema": { + "description": "Filter to the redactions produced from a specific detection.", + "type": "string", + "format": "uuid" + }, + "style": "form" + }, + { + "in": "query", + "name": "documentId", + "description": "Filter to the redactions of detections analyzing a specific document.", + "schema": { + "description": "Filter to the redactions of detections analyzing a specific document.", + "type": "string", + "format": "uuid" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "Generic paginated response wrapper.\n\nProvides a consistent structure for all paginated API responses with\ncursor-based pagination support. When `next_cursor` is present, there\nare more items to fetch.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkspaceRedactionResultPage" + } + } + } + }, + "401": { + "description": "The serialized shape of an HTTP error: the inert wire/OpenAPI-schema view\nthat [`Error`] renders to at the response boundary.\n\nIt carries no builder logic \u2014 [`Error`] is the type handlers construct and\nthread through `Result`, and it builds an `ErrorResponse` directly in its\n`IntoResponse` impl. `context` and `status` are not part of the JSON body\n(`context` is logged, `status` sets the HTTP status line).\n\n[`Error`]: crate::response::Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "The serialized shape of an HTTP error: the inert wire/OpenAPI-schema view\nthat [`Error`] renders to at the response boundary.\n\nIt carries no builder logic \u2014 [`Error`] is the type handlers construct and\nthread through `Result`, and it builds an `ErrorResponse` directly in its\n`IntoResponse` impl. `context` and `status` are not part of the JSON body\n(`context` is logged, `status` sets the HTTP status line).\n\n[`Error`]: crate::response::Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "The serialized shape of an HTTP error: the inert wire/OpenAPI-schema view\nthat [`Error`] renders to at the response boundary.\n\nIt carries no builder logic \u2014 [`Error`] is the type handlers construct and\nthread through `Result`, and it builds an `ErrorResponse` directly in its\n`IntoResponse` impl. `context` and `status` are not part of the JSON body\n(`context` is logged, `status` sets the HTTP status line).\n\n[`Error`]: crate::response::Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "BearerAuth": [] + } + ] + } + }, "/workspaces/{workspaceId}/redactions/{redactionId}/review": { "get": { "tags": [ @@ -14097,6 +14221,14 @@ } ] }, + "selection": { + "description": "Which recognizers this request selected.\n\nCarried back for the same reason [`codec`] is. Anonymize\ncompiles its analyzers afresh, and a lineup narrower than\nthe one analyze detected with would leave an entity found\nand then silently not redacted.\n\n[`codec`]: Audit::codec", + "allOf": [ + { + "$ref": "#/components/schemas/ComponentSelection" + } + ] + }, "usage": { "description": "What the analyze pass cost: one entry per recognizer and\nenricher that ran, each self-identifying by the name the\ndeployment configured it under.\n\nCarried here rather than read off the report: elide derives\nusage during analysis and drops it when a report is rebuilt\nfrom the wire, so a host that bills on model spend would\nlose it on the round trip.", "allOf": [ @@ -14302,6 +14434,27 @@ "description": "How the codec decodes this document.\n\nDefaults to the codec's own behaviour, so a caller with no\nopinion passes [`CodecParams::default`].", "type": "object", "properties": { + "csvDelimiter": { + "description": "The CSV field separator, or [`None`] to auto-detect it.\n\nDefaults to [`None`], the codec's own behaviour. Detection\nfalls back to a comma when nothing stands out, which can\nmisread a TSV or semicolon-delimited file, so pass the byte\nwhen the format is known.", + "type": "integer", + "format": "uint8", + "maximum": 255, + "minimum": 0 + }, + "csvHasHeaders": { + "description": "Whether a CSV's first row is its header.\n\nDefaults to `true`, the codec's own behaviour. A header row\ngains column-name context hints for the data below it, but\nis protected from a row-drop redaction \u2014 so a *headerless*\nfile needs `false`, or its first row of real data cannot be\ndropped.", + "type": "boolean", + "default": true + }, + "exifPolicy": { + "description": "What happens to an image's EXIF metadata on re-encode.\n\nDefaults to [`ExifPolicy::default`], which the codec also\nregisters \u2014 [`Strip`](ExifPolicy::Strip) today: metadata\ngoes unless a request asks to keep it. Deferring to the\nenum rather than naming a variant keeps this from drifting\nif the codec's own bias changes again.\n\nThis governs the output only when no metadata pipeline ran:\na wired EXIF recognizer and anonymizer strip through the\n`#exif` sub-part and always win. It is the knob for\nstripping \u2014 or deliberately preserving \u2014 *without* wiring\none.", + "allOf": [ + { + "$ref": "#/components/schemas/ExifPolicy" + } + ], + "default": "strip" + }, "rasterMode": { "description": "How container formats carrying both a text layer and page\nimages treat OCR.\n\nDefaults to [`RasterMode::Auto`], the codec's own behaviour.", "allOf": [ @@ -14395,6 +14548,26 @@ "status" ] }, + "ComponentSelection": { + "description": "Which of the available components one request wants to run.\n\nEmpty by default, meaning every component the caller has\navailable. A request that expresses no opinion gets full\ndetection \u2014 the opposite would let an omitted field quietly\ndisable redaction.", + "type": "object", + "properties": { + "only": { + "description": "Names or tags to run, or [`None`] for all available.\n\nAn empty `Vec` is not the same as [`None`]: it selects\nnothing and is refused, because a request that can detect\nnothing is a mistake rather than a request for an empty\nreport.", + "type": "array", + "items": { + "type": "string" + } + }, + "skip": { + "description": "Names or tags to skip, applied after `only`.\n\nLets a caller run everything but one component without\nenumerating the rest.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, "Confidence": { "description": "Confidence score in the closed range `0.0..=1.0`.\n\nCarried by every provenance [`AuditEvent`] (the `before`/`after` of a\nrecognition, fusion, or calibration) and by the effective confidence\nof an [`Entity`]. The newtype enforces the range at construction so\nno downstream code has to defend against values outside `[0, 1]`.\n\nDistinct from [`ConfidenceThreshold`] so the two cannot be confused\nat a call site: a score is *produced* by detection, a threshold is a\n*cutoff* configured to filter scores. Compare the two with\n[`ConfidenceThreshold::passes`].\n\n[`AuditEvent`]: crate::entity::audit::AuditEvent\n[`Entity`]: crate::entity::Entity", "type": "number", @@ -15537,6 +15710,26 @@ "message" ] }, + "ExifPolicy": { + "description": "What to do with an image's EXIF metadata when re-encoding it.\n\nEXIF mixes privacy-sensitive fields (GPS, device serial, capture timestamp)\nwith benign ones (orientation, colour profile) that a viewer needs to render\nthe image correctly, so the choice is a policy, not a fixed behaviour.", + "oneOf": [ + { + "description": "Drop the metadata, keeping only the tags an image structurally requires\nto stay a valid, renderable image. The safe default: nothing personal\nsurvives, though benign hints (a viewer's orientation tag) go too.", + "type": "string", + "const": "strip" + }, + { + "description": "Drop only the privacy-sensitive fields (GPS, device, timestamps), keeping\nthe rest so the image still renders as intended.", + "type": "string", + "const": "strip_sensitive" + }, + { + "description": "Leave the metadata untouched.", + "type": "string", + "const": "retain" + } + ] + }, "ExportFormat": { "description": "The file format an export is rendered as.", "oneOf": [ @@ -18787,10 +18980,6 @@ "description": "Public view of one recognizer in the engine's NER or LLM\nlineup.\n\nCarries the name a request's allowlist picks by, an optional\nhuman-readable description, and a provider slug identifying\nthe backend kind. Connection details and (future)\ncredentials stay in the private `NerConfig` / `LlmConfig`.\n\nOwned rather than borrowing from the engine so callers can\ncarry the value past the borrow that produced it. Cloning is\ncheap: [`HipStr`] shares the backing string via an `Arc`\nheader.", "type": "object", "properties": { - "description": { - "description": "Optional human-readable description.", - "type": "string" - }, "name": { "description": "Recognizer name: the identifier a request's allowlist\npicks by.", "type": "string" @@ -18798,6 +18987,13 @@ "provider": { "description": "Provider slug. NER: `\"bento\"`, `\"mock\"`. LLM: `\"openai\"`,\n`\"anthropic\"`, `\"gemini\"`, `\"ollama\"`, `\"mock\"`.\n\nOwned so the type deserializes from a runtime buffer: a\n`&'static str` field would make the derive emit\n`Deserialize<'static>` only, which compiles against string\nliterals but not against an owned `String` or a reader -\nthe shapes a host actually decodes from. Borrowing a\n`&'static str` into a [`HipStr`] does not allocate, so\nengine-side construction stays free.", "type": "string" + }, + "tags": { + "description": "The groupings this component belongs to, as the deployment\ntagged it.\n\nWhat a request's selection and the engine's availability\nmatch on besides the name, so a caller listing components\ncan see which families it may ask for.", + "type": "array", + "items": { + "type": "string" + } } }, "required": [ @@ -24264,6 +24460,22 @@ "items" ] }, + "WorkspaceRedactionsQuery": { + "description": "Query parameters for listing a workspace's redactions.\n\nEvery field is an optional filter; unset fields impose no constraint. A\nredaction has no status of its own, so it narrows by its owning detection and\nthe document that detection analyzed.", + "type": "object", + "properties": { + "detectionId": { + "description": "Filter to the redactions produced from a specific detection.", + "type": "string", + "format": "uuid" + }, + "documentId": { + "description": "Filter to the redactions of detections analyzing a specific document.", + "type": "string", + "format": "uuid" + } + } + }, "WorkspaceReview": { "description": "Response type for a review: a named discussion on a document with a manual\nsign-off lifecycle (0..N per document), opened explicitly. It carries a\n`reviewStatus`, a `displayName`, and its `assignees` (0..N reviewers); its\nstream is a [`WorkspaceReviewEntry`] timeline.", "type": "object", diff --git a/features/supported-formats.mdx b/features/supported-formats.mdx index d6ab982..7b9408b 100644 --- a/features/supported-formats.mdx +++ b/features/supported-formats.mdx @@ -104,6 +104,19 @@ as `codec.rasterMode`: text path today; per-page rendering of scanned pages lands with OCR. +## Image Metadata + +EXIF mixes privacy-sensitive fields (GPS, device serial, capture timestamp) +with benign ones a viewer needs to render the image correctly, so what happens +to it on re-encode is a policy. It is carried on the audit as +`codec.exifPolicy`: + +| Policy | Behavior | +|--------|----------| +| `strip` | Drop the metadata, keeping only the tags the image structurally requires to stay renderable. The default | +| `strip_sensitive` | Drop only the privacy-sensitive fields (GPS, device, timestamps), keeping the rest | +| `retain` | Leave the metadata untouched | + ## Filtering Documents List endpoints filter by format, modality, name, or content hash: diff --git a/sdks/typescript/api-reference.mdx b/sdks/typescript/api-reference.mdx index 79eb2b2..8b4a866 100644 --- a/sdks/typescript/api-reference.mdx +++ b/sdks/typescript/api-reference.mdx @@ -281,11 +281,17 @@ whose analysis ran no enricher has none and responds 404. ## Redactions ```typescript +listRedactions(workspaceId: string, query?: CursorPagination & WorkspaceRedactionsQuery): Promise getReview(workspaceId: string, redactionId: string): Promise ``` -Returns the redaction's review audit — what was redacted and why — independent -of the detection it came from. +`listRedactions` lists a workspace's redactions across every detection; +`WorkspaceRedactionsQuery` narrows them by `detectionId` or `documentId`. To +list the redactions of one detection, `detections.listRedactions` takes the +detection id directly. + +`getReview` returns the redaction's review audit — what was redacted and why — +independent of the detection it came from. ## Reviews