> ## Documentation Index
> Fetch the complete documentation index at: https://docs.visiqlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API Reference

> Complete REST API reference for the retrieval-governance endpoints.

Retrieval-governance endpoints are mounted under `/recall/*`, with the versioned audit-log read at `/v1/recall/audit-log`. The base URL is `https://api.visiqlabs.com`.

Action governance and retrieval governance share a single rule engine — each rule declares which facets it applies to. The endpoints on this page are the retrieval facet: they govern what your agents can **see** (retrieved documents, tool results, rendered prompts), with decisions `allow`, `deny`, `redact`, and `escalate`.

## Authentication

All endpoints require a Bearer credential: `Authorization: Bearer <key>`. Requests without a valid credential receive `401 Unauthorized`.

Two credential audiences exist:

* **Harness keys** — the operational credential your SDK or harness runs with. Either a `vq_prod_...` / `vq_test_...` key minted in the dashboard under **Settings → Harness Keys**, or the `allow_...` key returned once by `POST /allow/agents`. On the retrieval surface the operational allowlist covers exactly `POST /recall/evaluate` and `GET /recall/rules/bundle` — calling any other endpoint on this page with a harness key returns `403 {"error": "harness_key_not_permitted"}`.
* **Management keys** — general automation credentials governed by explicit permission grants. They can call every endpoint on this page. Mint one in the dashboard under **Settings → API Keys**, scoped to the exact permission grants it needs; or drive the management endpoints from the dashboard, which authenticates with your session.

Scoped keys are additionally checked against their granted scopes. Evaluation requires `rules:evaluate` (or the legacy `recall:write`); reads require `rules:read` (or the legacy `recall:read`); rule mutations require `rules:write` (or the legacy `recall:write`); `full_access` satisfies everything. A key without the required scope receives `403 {"error": "insufficient_scope"}` listing the required and granted scopes.

### Rate limiting

Every API-key request passes a per-key sliding-window rate limit (default 600 requests per 60 seconds). Responses carry `X-RateLimit-Limit` and `X-RateLimit-Remaining` headers; exceeding the window returns `429` with a `Retry-After` header and body `{"error": "rate_limited", "detail": "API key rate limit exceeded.", "retryAfter": <seconds>}`.

### List responses

Every list endpoint on this page returns the same envelope:

```json theme={null}
{
  "data": [ ... ],
  "total": 128,
  "page": 1,
  "pageSize": 50
}
```

Pagination is controlled by `page` (default `1`) and `limit` (default `50`, max `100`) query parameters.

***

## Evaluation

The endpoints your SDK or harness calls at runtime. Both accept a harness key. The SDK evaluates most retrievals locally from the cached rule bundle — the server and the SDK run the same policy interpreter over the same rule source, so local and remote decisions agree by construction.

***

### POST /recall/evaluate

Evaluate a retrieval against your suppression rules and return a decision.

**Scope:** `rules:evaluate` or `recall:write`

**Request body:**

```json theme={null}
{
  "agent_id": "research-bot",
  "operation": "retrieve",
  "resource_type": "document",
  "resource_metadata": {
    "classification": "confidential",
    "department": "finance"
  },
  "surface": "public_channel",
  "query": "quarterly revenue figures"
}
```

| Field               | Type                                         | Required | Description                                                                                   |
| ------------------- | -------------------------------------------- | -------- | --------------------------------------------------------------------------------------------- |
| `agent_id`          | `string`                                     | Yes      | Identifier of the agent making the retrieval (1–255 chars)                                    |
| `operation`         | `retrieve` \| `tool_call` \| `prompt_render` | Yes      | Which hook produced the content: a retriever, a tool result, or prompt assembly               |
| `resource_type`     | `string`                                     | No       | Resource type being accessed. Default `document` (1–255 chars)                                |
| `resource_metadata` | `object`                                     | No       | Metadata about the resource (classification, department, …). Default `{}`                     |
| `trust_tier`        | `string`                                     | No       | Fallback trust tier — see the note below (max 50 chars)                                       |
| `surface`           | `string`                                     | No       | Delivery surface for the output, matched exactly against surface-scoped rules (max 100 chars) |
| `query`             | `string`                                     | No       | The retrieval query string (max 2000 chars)                                                   |
| `telemetry`         | `object`                                     | No       | Opaque client telemetry, attached to the decision's signed record envelope                    |

<Note>
  Agent attributes are **server-authoritative**. The platform looks up `agent_id` in the agent registry and hydrates its assigned trust tier, categories, and business function into rule input (`input.agent.*`). An assigned trust tier **overrides** the request's `trust_tier` — the body value is only a fallback for agents with no assigned tier, so varying it in a request cannot spoof a tier-gated rule. Unlike the action facet's evaluate endpoint, this endpoint does not auto-provision unknown agents.
</Note>

<Note>
  For an agent in **monitor mode**, a non-allow decision is normalized to `allow` before it is recorded and returned, with `reason_code: "MONITOR_MODE"` and the engine's original reason preserved behind a `Monitor: ` prefix.
</Note>

**Response:**

```json theme={null}
{
  "decision_id": "550e8400-e29b-41d4-a716-446655440000",
  "decision": "redact",
  "reason_code": "POLICY_DENY",
  "reason": "Matched rule: Mask account numbers for support agents",
  "redaction_rules": [
    { "field": "ssn", "mode": "full", "replacement": "[REDACTED]" },
    { "field": "account_number", "mode": "partial", "keepLast": 4, "maskChar": "*" }
  ]
}
```

| Field             | Type                                        | Description                                                                                |
| ----------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `decision_id`     | `string` (UUID)                             | Unique ID for this decision — used for audit and the retained-original reveal              |
| `decision`        | `allow` \| `deny` \| `redact` \| `escalate` | The suppression outcome                                                                    |
| `reason_code`     | `string`                                    | Machine-readable reason — see the table below                                              |
| `reason`          | `string`                                    | Human-readable explanation                                                                 |
| `redaction_rules` | `array`                                     | Present on a `redact` decision (and on an `escalate` whose fallback is `mask`) — see below |
| `hitl_fallback`   | `deny` \| `mask`                            | Present on an `escalate` decision — see below                                              |

**Decision semantics:**

* `allow` — the content passes to the agent unchanged.
* `deny` — the content is suppressed; the agent never sees it.
* `redact` — the content proceeds with the accompanying `redaction_rules` applied.
* `escalate` — hold the content for human review; if no human approves in time, fall back per `hitl_fallback`.

**Redaction directives.** Each entry in `redaction_rules` may include `field`, `pattern`, `replacement`, `mode` (`full` | `partial` | `email` | `custom`), `keepFirst`, `keepLast`, `maskChar`, and `keepPattern`. `full` replaces the whole match (the default); `partial` keeps the first/last characters; `email` keeps the first character of the local part plus the domain; `custom` keeps every `keepPattern` match and masks the rest. A `redact` decision from a rule whose directives resolve to nothing passes content through unchanged — unless the rule is flagged fail-closed, in which case the decision is downgraded to `deny` server-side.

**The `escalate` decision** tells your harness to hold the content for human review — this endpoint does not itself notify approvers; register the approval through the human-in-the-loop flow (see [Human-in-the-Loop](/rules/action/hitl)). `hitl_fallback` tells the harness what to do when no human responds in time: `deny` (the default, fail-closed) or `mask` (proceed with the accompanying `redaction_rules` applied).

**Reason codes:**

| Code                  | Meaning                                                                                                 |
| --------------------- | ------------------------------------------------------------------------------------------------------- |
| `POLICY_ALLOW`        | A matched rule's conditions evaluated to allow                                                          |
| `POLICY_DENY`         | A matched rule's conditions produced a non-allow outcome (deny, redact, or escalate)                    |
| `TIER_MISMATCH`       | The agent's trust tier is below the minimum required by the rule or resource                            |
| `PRINCIPAL_EXCLUDED`  | The agent is on the rule's (or resource's) principal exclusion list                                     |
| `SURFACE_RESTRICTION` | A surface-scoped rule applied to this delivery surface and did not allow it                             |
| `AUDIENCE_EXPANSION`  | Reserved — defined in the decision vocabulary but not currently emitted by the engine                   |
| `DEFAULT_DENY`        | No rule matched — the retrieval facet fails closed                                                      |
| `EMERGENCY_BYPASS`    | An active [emergency bypass](/rules/retrieval/emergency-bypass) on the matched rule allowed the request |
| `MONITOR_MODE`        | The agent runs in monitor mode — a non-allow decision was recorded but returned as `allow`              |

<Note>
  The response carries no receipt ID — tamper evidence is asynchronous. For organizations with artifact signing enabled, each decision emits a record envelope that receives an Ed25519 receipt off the request path, is Merkle-batched under a KMS-signed root with an RFC 3161 timestamp, and lands in a hash-chained checkpoint log. Verify a decision's record envelope server-side via `GET /record/envelopes/:id/verify` (`:id` is the record envelope's UUID, not the `decision_id` returned here).
</Note>

<Note>
  **Minimum-necessary at rest:** the decision is evaluated against the real values, but the persisted copies of `resource_metadata` and `query` are masked — first by the matched rule's redaction directives, then by an always-on value-shape redaction floor. The raw original is discarded by default; organizations can opt in to retaining it encrypted, revealable only via the [retained-original endpoint](#get-recalldecisionsidunmask).
</Note>

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `403 Forbidden` (insufficient scope), `429 Too Many Requests`, `500 Internal Server Error`

**Example:**

```bash theme={null}
curl -X POST https://api.visiqlabs.com/recall/evaluate \
  -H "Authorization: Bearer $VISIQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "research-bot",
    "operation": "retrieve",
    "resource_type": "document",
    "resource_metadata": { "classification": "confidential" }
  }'
```

***

### GET /recall/rules/bundle

Fetch your enabled retrieval rules as one bundle for local evaluation. Clients can cache this bundle and revalidate it with `If-None-Match`, so retrieval decisions on the hot path never wait on the network. (The VisIQ SDK consumes the unified `GET /rules/bundle`, which carries both facets.)

**Request headers:**

| Header          | Description                                                                                     |
| --------------- | ----------------------------------------------------------------------------------------------- |
| `If-None-Match` | ETag from a previous response. The server returns `304 Not Modified` if the bundle is unchanged |

**Response:**

```json theme={null}
{
  "version": "a3b4c5d6e7f8...",
  "rules": [
    {
      "id": "rule-uuid",
      "name": "Deny tier3 from confidential",
      "rego_source": "package recall.rules.tier3_guard\n\ndefault decision = \"deny\"\n...",
      "trust_tier": "tier3",
      "surface": null,
      "principal_exclusions": [],
      "priority": 50
    }
  ]
}
```

| Field     | Description                                                                                                                                                                                               |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `version` | SHA-256 hash of the serialized rules array — changes whenever any included rule changes                                                                                                                   |
| `rules`   | Enabled retrieval rules, sorted by priority descending. Each carries its full `rego_source` for local evaluation. Rules without policy source are excluded from the bundle (they are not yet enforceable) |

**Response headers:**

| Header          | Value                                       |
| --------------- | ------------------------------------------- |
| `ETag`          | `"<sha256-version>"` (quoted, per RFC 7232) |
| `Cache-Control` | `private, max-age=60`                       |

**Status codes:** `200 OK`, `304 Not Modified`, `401 Unauthorized`, `403 Forbidden` (insufficient scope), `500 Internal Server Error`

***

## Rules

Rule management. These are management endpoints — a harness key receives `403 harness_key_not_permitted` here. The recommended authoring path is the dashboard's rule editor (natural-language compile, visual condition builder, and a Simulate panel that checks a new rule against your recent traffic — rules projected to inhibit more than 5% of it are blocked). These endpoints are the programmatic equivalent.

***

### GET /recall/rules

List retrieval rules, sorted by priority descending — the same order the engine evaluates them in (first match wins).

**Permission:** `recall_rules:view`

**Query parameters:** `page` (default `1`), `limit` (default `50`, max `100`)

**Response:**

```json theme={null}
{
  "data": [
    {
      "id": "rule-uuid",
      "name": "Deny tier3 from confidential",
      "description": "Prevent low-trust agents from accessing sensitive data",
      "natural_language": "Deny tier3 agents from internal, confidential, or restricted data",
      "priority": 50,
      "enabled": true,
      "trust_tier": "tier3",
      "surface": null,
      "principal_exclusions": null,
      "bypass_active": false,
      "bypass_reason": null,
      "bypass_activated_by": null,
      "bypass_activated_at": null,
      "bypass_expires_at": null,
      "created_at": "2026-07-03T10:00:00Z",
      "updated_at": "2026-07-03T10:00:00Z"
    }
  ],
  "total": 8,
  "page": 1,
  "pageSize": 50
}
```

The list omits `rego_source`; fetch a single rule to read the policy source.

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `500 Internal Server Error`

***

### POST /recall/rules

Create a retrieval rule.

**Permission:** `recall_rules:create`

**Request body:**

```json theme={null}
{
  "name": "Deny tier3 from confidential",
  "description": "Prevent low-trust agents from accessing sensitive data",
  "rego_source": "package recall.rules.tier3_guard\n\ndefault decision = \"deny\"\n\ndecision = \"deny\" if {\n  input.resource_metadata.classification in [\"internal\", \"confidential\", \"restricted\"]\n}",
  "natural_language": "Deny tier3 agents from accessing internal, confidential, or restricted data",
  "priority": 50,
  "enabled": true,
  "trust_tier": "tier3"
}
```

| Field                  | Type       | Required | Description                                                                                                                                               |
| ---------------------- | ---------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                 | `string`   | Yes      | Rule name (max 255 chars)                                                                                                                                 |
| `description`          | `string`   | No       | Description (max 1000 chars)                                                                                                                              |
| `rego_source`          | `string`   | Yes      | Policy source (see note below)                                                                                                                            |
| `natural_language`     | `string`   | No       | The plain-English intent the policy was compiled from (max 2000 chars)                                                                                    |
| `priority`             | `number`   | No       | Evaluation priority — higher runs first. Default `0`                                                                                                      |
| `enabled`              | `boolean`  | No       | Whether the rule is active. Default `true`                                                                                                                |
| `trust_tier`           | `string`   | No       | Minimum trust tier this rule requires (`tier1` is highest trust, `tier3` lowest). Agents below the minimum are denied with `TIER_MISMATCH` (max 50 chars) |
| `surface`              | `string`   | No       | Exact-match surface restriction — the rule applies only to retrievals delivered to this surface (max 100 chars)                                           |
| `principal_exclusions` | `string[]` | No       | Agent IDs this rule excludes — an excluded agent is denied with `PRINCIPAL_EXCLUDED`                                                                      |

<Note>
  `rego_source` holds the rule's policy source in the platform's Rego-subset condition language (equality, inequality, set membership, `startswith`/`endswith`/`contains`, regex, counts, and numeric comparisons). The decision (`allow`, `deny`, `redact`, `escalate`) is derived from the policy body. Prefer `POST /recall/rules/compile` — the compiler drafts and validates the policy against the live evaluation engine.
</Note>

**Response (201):** the created rule object, including `rego_source`.

**Status codes:** `201 Created`, `400 Bad Request`, `401 Unauthorized`, `500 Internal Server Error`

***

### POST /recall/rules/compile

Compile a natural-language description into a retrieval rule. The compiler reads your existing rules for context, drafts the policy source, and validates it against the live evaluation engine.

**Permission:** `recall_rules:create`

**Rate limit:** 10 compile requests per minute per organization — exceeding it returns `429`.

**Query parameters:** `stream=true` — stream the compile over Server-Sent Events instead of a single JSON response (also triggered by `Accept: text/event-stream`).

**Request body:**

```json theme={null}
{
  "prompt": "Deny tier3 agents from accessing any document classified as internal, confidential, or restricted"
}
```

| Field     | Type     | Required | Description                                                  |
| --------- | -------- | -------- | ------------------------------------------------------------ |
| `prompt`  | `string` | Yes      | Plain-English rule description (max 4000 chars)              |
| `nodeRef` | `string` | No       | Rule-graph node reference for editor context (max 255 chars) |

**Response:**

```json theme={null}
{
  "rego_source": "package recall.rules.tier3_guard\n\ndefault decision = \"deny\"\n...",
  "natural_language": "This rule denies tier3 agents access to internal, confidential, or restricted documents...",
  "name": "Compiled retrieval rule",
  "description": "AI-compiled suppression rule",
  "suggested_priority": 50
}
```

**Streaming response** (`?stream=true`): a `text/event-stream` of JSON events —

```
data: {"type":"text","content":"Drafting the policy..."}

data: {"type":"done","result":{"rego_source":"...","natural_language":"...","name":"...","description":"...","suggested_priority":50}}
```

On failure the stream emits `{"type":"error","message":"..."}` and closes.

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `422 Unprocessable Entity` (the compiler could not produce a valid policy — refine the prompt), `429 Too Many Requests`, `500 Internal Server Error`, `503 Service Unavailable` (rule compilation is not configured in this environment)

***

### GET /recall/rules/:id

Get a single rule by UUID, including `rego_source`.

**Permission:** `recall_rules:view`

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `404 Not Found`, `500 Internal Server Error`

***

### PUT /recall/rules/:id

Update a rule. Same fields as `POST /recall/rules`, all optional; at least one is required. Only provided fields change. `trust_tier`, `surface`, and `principal_exclusions` accept `null` to clear.

**Permission:** `recall_rules:update`

**Response:** the updated rule object.

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `404 Not Found`, `500 Internal Server Error`

***

### DELETE /recall/rules/:id

Delete a rule permanently.

**Permission:** `recall_rules:delete`

**Response:** `{ "deleted": true }`

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `404 Not Found`, `500 Internal Server Error`

***

## Emergency Bypass

Temporarily suspend one rule's enforcement during an incident — see [Emergency Bypass](/rules/retrieval/emergency-bypass) for when and why. Bypasses auto-expire; expiry is checked at evaluation time, so there is no revocation lag. Both activation and deactivation are written to the audit log with reason code `EMERGENCY_BYPASS`.

***

### POST /recall/rules/:id/bypass

Activate an emergency bypass on a rule. While active, any request that reaches the bypassed rule in the priority cascade returns `allow` with reason code `EMERGENCY_BYPASS` — even requests the rule's conditions would not have matched — and lower-priority rules never run for it. Higher-priority rules keep enforcing.

**Permission:** `recall_rules:bypass`

**Request body:**

```json theme={null}
{
  "reason": "Incident INC-2041: unblocking on-call diagnosis",
  "duration_minutes": 30
}
```

| Field              | Type     | Required | Description                                                         |
| ------------------ | -------- | -------- | ------------------------------------------------------------------- |
| `reason`           | `string` | Yes      | Why the bypass is needed (min 10 chars) — recorded in the audit log |
| `duration_minutes` | `number` | Yes      | One of `15`, `30`, `60`, `120`. The bypass expires automatically    |

**Response:** the full rule object with the bypass fields populated:

```json theme={null}
{
  "id": "rule-uuid",
  "name": "Deny tier3 from confidential",
  "bypass_active": true,
  "bypass_reason": "Incident INC-2041: unblocking on-call diagnosis",
  "bypass_activated_by": "user-uuid",
  "bypass_activated_at": "2026-07-03T10:30:00Z",
  "bypass_expires_at": "2026-07-03T11:00:00Z",
  "...": "..."
}
```

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `404 Not Found`, `409 Conflict` (a bypass is already active on this rule), `500 Internal Server Error`

***

### DELETE /recall/rules/:id/bypass

Deactivate an active bypass before it expires.

**Permission:** `recall_rules:bypass`

**Response:** the full rule object with the bypass fields cleared.

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `404 Not Found`, `409 Conflict` (no active bypass on this rule), `500 Internal Server Error`

***

## Audit Log

***

### GET /v1/recall/audit-log

Query the retrieval decision audit log. Every decision made through this endpoint — allow and deny, including monitor-mode observations and bypass activations — is recorded. (Locally evaluated SDK decisions surface through the runtime telemetry pipeline instead.) The log is append-only; no writes are accepted via this API.

**Permission:** `recall_audit_log:view`

<Note>
  Tamper evidence comes from the signed record pipeline, not from the query API: for organizations with artifact signing enabled, each decision emits a record envelope that receives an asynchronous Ed25519 receipt, is Merkle-batched under a KMS-signed root with an RFC 3161 timestamp, and lands in a hash-chained checkpoint log. Verify a decision's record envelope server-side via `GET /record/envelopes/:id/verify`.
</Note>

**Scope:** `rules:read` or `recall:read` (management keys / dashboard session — harness keys cannot query the log)

**Query parameters:**

| Parameter     | Type              | Description                                                                                                                                                         |
| ------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_id`    | `string`          | Filter by agent logical ID                                                                                                                                          |
| `operation`   | `string`          | `retrieve` \| `tool_call` \| `prompt_render` \| `emergency_bypass_activate` \| `emergency_bypass_deactivate`                                                        |
| `decision`    | `string`          | `allow` \| `deny` \| `redact` \| `escalate`                                                                                                                         |
| `reason_code` | `string`          | `TIER_MISMATCH` \| `PRINCIPAL_EXCLUDED` \| `POLICY_DENY` \| `AUDIENCE_EXPANSION` \| `SURFACE_RESTRICTION` \| `POLICY_ALLOW` \| `DEFAULT_DENY` \| `EMERGENCY_BYPASS` |
| `start_date`  | ISO 8601 datetime | Lower bound on `created_at`                                                                                                                                         |
| `end_date`    | ISO 8601 datetime | Upper bound on `created_at`                                                                                                                                         |
| `page`        | `number`          | Page index (default `1`)                                                                                                                                            |
| `limit`       | `number`          | Records per page (default `50`, max `100`)                                                                                                                          |

Rows recorded under monitor mode carry `reason_code: "MONITOR_MODE"`; it is not yet accepted as a filter value.

**Response:**

```json theme={null}
{
  "data": [
    {
      "id": "audit-entry-uuid",
      "agent_id": "research-bot",
      "operation": "retrieve",
      "resource_type": "document",
      "decision": "deny",
      "reason_code": "TIER_MISMATCH",
      "reason": "agent trust tier 'tier3' (level 1) is below the required minimum 'tier2' (level 2)",
      "rule_id": "rule-uuid",
      "receipt_id": null,
      "metadata": {
        "surface": "public_channel",
        "trust_tier": "tier3",
        "query": "quarterly revenue",
        "resource_metadata": { "classification": "confidential" },
        "floor_detectors": [],
        "decision_id": "decision-uuid"
      },
      "created_at": "2026-07-03T10:30:00Z"
    }
  ],
  "total": 1247,
  "page": 1,
  "pageSize": 50
}
```

`metadata.query` and `metadata.resource_metadata` are the **masked** persisted copies (rule directives plus the always-on redaction floor); `metadata.floor_detectors` names any floor detectors that fired, and `metadata.decision_id` is the underlying decision's ID for the retained-original reveal below. `receipt_id` is a legacy column and is `null` on new rows — receipts flow through the record pipeline instead.

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `403 Forbidden` (insufficient scope), `429 Too Many Requests`, `500 Internal Server Error`

***

## Retained Originals

***

### GET /recall/decisions/:id/unmask

Reveal the retained, encrypted original payload for one retrieval decision. Persisted copies are masked at rest; when your organization has enabled retained-original storage (off by default), the raw `resource_metadata` and `query` are additionally stored AES-256-GCM-encrypted, and this endpoint decrypts them.

**Permission:** `payloads:unmask`

This is a management endpoint gated on the `payloads:unmask` permission — only explicitly-authorized roles can ever see the original values, and every reveal is logged with who requested it. Harness keys receive `403`.

**Path parameter:** `:id` — the decision UUID (`decision_id` from `POST /recall/evaluate`, or `metadata.decision_id` on an audit-log row)

**Response:**

```json theme={null}
{
  "decision_id": "decision-uuid",
  "original": {
    "resource_metadata": { "classification": "confidential", "account_number": "4111111111111111" },
    "query": "quarterly revenue figures"
  }
}
```

**Status codes:** `200 OK`, `400 Bad Request` (invalid UUID), `401 Unauthorized`, `403 Forbidden`, `404 Not Found` (unknown decision, or retention is disabled for your organization), `500 Internal Server Error`
