> ## 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 unified rule surface — one CRUD collection over every operation, the SDK bundle, and the single evaluation call.

The unified rule surface is mounted under `/rules/*`, with the single evaluation call at `/evaluate`. The base URL is `https://api.visiqlabs.com`.

One rules collection spans every operation. Each rule declares an `operations[]` array (its stored `applies_to`) drawn from `action`, `retrieval`, and `delegation`, and the engine evaluates one event against the operations it carries.

<Note>
  The per-facet management endpoints — `/allow/rules` (action) and `/recall/rules` (retrieval) — are **additive, permanent compatibility aliases** over the same `rules` table, not deprecated surfaces. A rule created here appears there and vice versa; issued keys keep their existing grants. Use whichever surface fits your integration.
</Note>

## 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. On this surface a harness key covers exactly `GET /rules/bundle` and `POST /evaluate`; calling the rule-management endpoints (`GET/POST /rules`, `GET/PUT/DELETE /rules/:id`) with one 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. Management keys are **launching soon**: they are visible in the dashboard under **Settings → API Keys**, but creating one is not yet enabled. Until then, drive the management endpoints from the dashboard, which authenticates with your session.

The management CRUD reuses the action-facet permissions (`allow_rules:view` / `:create` / `:update` / `:delete`), so a key or role that already manages action rules manages unified rules unchanged. Evaluation is scope-gated: `POST /evaluate` accepts the operation-native `rules:evaluate` **or** the legacy `allow:write` / `recall:write`; `full_access` satisfies everything.

### 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

The list endpoint returns the standard 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

***

### POST /evaluate

The single SDK-facing evaluation call for every governed event. The request declares which operations the event performs, and the response carries the union decision vocabulary — so one contract spans every facet. This is a pure projection onto the same plane handlers the per-facet evaluate endpoints use: there is exactly one decision implementation.

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

**Request body (operation-native form):**

```json theme={null}
{
  "operations": ["action"],
  "agent_id": "billing-agent",
  "target_app": "stripe.com",
  "action": "POST /v1/charges",
  "context": { "amount": 5000 }
}
```

| Field               | Type                                         | Required                                        | Description                                                                                  |
| ------------------- | -------------------------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `operations`        | `string[]`                                   | Yes                                             | 1–3 of `action`, `retrieval`, `delegation`                                                   |
| `agent_id`          | `string`                                     | Yes                                             | The agent making the request (1–255 chars)                                                   |
| `target_app`        | `string`                                     | When `operations` include `action`/`delegation` | Hostname or app identifier (1–255 chars)                                                     |
| `action`            | `string`                                     | When `operations` include `action`/`delegation` | The action string — HTTP method + path or tool name (1–255 chars)                            |
| `context`           | `object`                                     | No                                              | Key-value context for action/delegation rule matching. Default `{}`                          |
| `operation`         | `retrieve` \| `tool_call` \| `prompt_render` | No                                              | Retrieval hook that produced the content. Default `retrieve`                                 |
| `resource_type`     | `string`                                     | When `operations` include `retrieval`           | Resource type being accessed (1–255 chars)                                                   |
| `resource_metadata` | `object`                                     | No                                              | Metadata for retrieval rule matching. Default `{}`                                           |
| `trust_tier`        | `string` \| `null`                           | No                                              | Fallback trust tier for retrieval (an assigned tier overrides it) (max 64 chars)             |
| `surface`           | `string` \| `null`                           | No                                              | Delivery surface for retrieval, matched exactly against surface-scoped rules (max 128 chars) |
| `query`             | `string` \| `null`                           | No                                              | The retrieval query string (max 4000 chars)                                                  |
| `telemetry`         | `object`                                     | No                                              | Opaque client telemetry attached to the decision's record envelope                           |

<Note>
  A `delegation` operation projects onto the action facet (it therefore requires `target_app` and `action`), which runs the delegation-facet policy check — the `orchestrate.delegate` authority transfer. This is the stateless policy leg; the grant/token lifecycle lives on the [delegation endpoints](/rules/delegation/api-reference).
</Note>

<Note>
  A legacy `kind`-discriminated form is still accepted and behaves identically: `{ "kind": "action", ... }` or `{ "kind": "retrieval", ... }`. The `operations[]` form is canonical.
</Note>

**Response (single-operation):**

```json theme={null}
{
  "operations": ["action"],
  "decision": "permit",
  "plane_decision": "permit",
  "decision_id": "550e8400-e29b-41d4-a716-446655440000",
  "reason": "Matched rule: Allow Stripe reads",
  "reason_code": null,
  "rule_code": "R-1042",
  "enforced": true,
  "agent_mode": "enforce"
}
```

| Field                                  | Type                      | Description                                                                              |
| -------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------- |
| `decision`                             | `string`                  | The union outcome — see the table below                                                  |
| `plane_decision`                       | `string`                  | The facet handler's original word (e.g. a retrieval `allow`, before it maps to `permit`) |
| `decision_id`                          | `string` (UUID) \| `null` | The underlying decision's id, for audit and polling                                      |
| `reason` / `reason_code` / `rule_code` | —                         | Explanation, machine-readable reason, and the matched rule code                          |
| `enforced` / `agent_mode`              | —                         | Whether the decision was enforced, and the agent's server-authoritative mode             |
| `redaction_rules`                      | `array`                   | Present when the outcome carries masking directives                                      |
| `hitl_fallback`                        | `deny` \| `mask`          | Present on an outcome that can fall back after a human timeout                           |

The union decision vocabulary:

| Outcome             | Facet     | Meaning                                                                              |
| ------------------- | --------- | ------------------------------------------------------------------------------------ |
| `permit`            | any       | The event proceeds unchanged                                                         |
| `deny`              | any       | Blocked or suppressed                                                                |
| `approval_required` | action    | Pauses for a human decision                                                          |
| `mask`              | action    | Proceeds with named arguments redacted                                               |
| `redact`            | retrieval | Proceeds with fields/patterns masked                                                 |
| `escalate`          | retrieval | Recorded for review; passes through (masked if the rule chooses) rather than pausing |

**Hybrid events.** When `operations` includes both `action` and `retrieval`, both facets are evaluated and the response carries a per-operation `results[]` plus an overall `decision` equal to the **most restrictive** facet outcome (fail-closed combine):

```json theme={null}
{
  "operations": ["retrieval", "action"],
  "decision": "deny",
  "reason": "...",
  "reason_code": null,
  "rule_code": "R-1099",
  "results": [
    { "operation": "retrieval", "decision": "redact", "plane_decision": "redact", "...": "..." },
    { "operation": "action", "decision": "deny", "plane_decision": "deny", "...": "..." }
  ]
}
```

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

***

### GET /rules/bundle

Fetch the compiled unified rule bundle for local evaluation — every enabled rule whose `applies_to` overlaps the action ∪ retrieval facets, plus the agent's mode and attributes. This is the bundle the tagged SDK runtime pulls; it caches it and revalidates with `If-None-Match` so decisions on the hot path never wait on the network.

**Permission:** `allow_rules:view` (a harness key passes the audience gate here)

**Query parameters:**

| Parameter  | Required | Description                                                                                                                              |
| ---------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_id` | Yes      | The agent the bundle is compiled for — carries that agent's mode and attributes. An unknown id auto-provisions the agent in monitor mode |

**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...",
  "dialect_version": 1,
  "min_dialect": 1,
  "agent_mode": "enforce",
  "cognition_capture": false,
  "agent_attributes": {
    "trust_tier": "tier2",
    "categories": ["transactional"],
    "business_functions": ["finance_accounting"],
    "business_function": "finance_accounting",
    "blast_radius_tier": null,
    "no_coverage": null
  },
  "rules": [ { "id": "rule-uuid", "applies_to": ["action"], "...": "..." } ],
  "no_coverage": {
    "no_coverage_defaults": { "read": "approve", "write": "approve", "delete": "approve", "admin": "approve" },
    "autopilot_enabled": false,
    "enduser_hitl_enabled": true,
    "hitl_timeout_seconds": 120
  }
}
```

| Field                             | Description                                                                                                                                                |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `version`                         | SHA-256 over the serialized payload — changes when any rule, the agent's mode/attributes, or the no-coverage settings change                               |
| `dialect_version` / `min_dialect` | The wire-contract dialect this bundle is compiled for, and the minimum an SDK must speak. A client below `min_dialect` refuses the bundle and fails closed |
| `agent_mode`                      | The agent's resolved mode (`enforce` \| `monitor` \| `off`)                                                                                                |
| `agent_attributes`                | Server-authoritative attributes hydrated into `input.agent.*` for local evaluation                                                                         |
| `rules`                           | Enabled effective rules overlapping the action ∪ retrieval facets, priority-descending, parsed by the SDK with the same parser the server uses             |
| `no_coverage`                     | Your organization's no-coverage policy, so the SDK resolves uncovered events locally                                                                       |

<Note>
  A shut-down agent (operator kill-switch) receives a **terminal bundle** — enforce mode, zero rules, all-deny no-coverage — so an SDK that honours the `shutdown` flag stops immediately, and one that ignores it still denies everything.
</Note>

**Response headers:** `ETag` (quoted SHA-256), `Cache-Control: private, max-age=60`

**Status codes:** `200 OK`, `304 Not Modified`, `400 Bad Request` (missing/invalid `agent_id`), `401 Unauthorized`, `404 Not Found` (unknown agent after provisioning), `500 Internal Server Error`

***

## Rules

Management endpoints — a harness key receives `403 harness_key_not_permitted` here. One collection over every operation; the `applies_to` array on each rule decides which facets it governs.

***

### GET /rules

List rules, priority-descending, optionally filtered to a facet.

**Permission:** `allow_rules:view`

**Query parameters:** `page` (default `1`), `limit` (default `50`, max `100`), `operations` (optional CSV of `action`,`retrieval`,`delegation` — returns only rules whose `applies_to` overlaps the set)

**Response:**

```json theme={null}
{
  "data": [
    {
      "id": "rule-uuid",
      "rule_code": "R-1042",
      "name": "Allow Stripe reads",
      "description": "Permit read-only Stripe API calls",
      "natural_language": "Allow my billing agent to read from Stripe",
      "priority": 10,
      "enabled": true,
      "applies_to": ["action"],
      "target_app": "stripe.com",
      "action_pattern": "GET *",
      "trust_tier": null,
      "surface": null,
      "principal_exclusions": null,
      "created_at": "2026-07-03T10:00:00Z",
      "updated_at": "2026-07-03T10:00:00Z"
    }
  ],
  "total": 12,
  "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 /rules

Create a rule. The `operations[]` array is stored as the rule's `applies_to`.

**Permission:** `allow_rules:create`

**Request body:**

```json theme={null}
{
  "operations": ["action"],
  "name": "Allow Stripe reads",
  "description": "Permit read-only Stripe API calls",
  "rego_source": "package rules\n\ndefault decision = \"deny\"\n...",
  "natural_language": "Allow my billing agent to read from Stripe",
  "priority": 10,
  "enabled": true,
  "target_app": "stripe.com",
  "action_pattern": "GET *"
}
```

| Field                  | Type                 | Required | Description                                                                                    |
| ---------------------- | -------------------- | -------- | ---------------------------------------------------------------------------------------------- |
| `operations`           | `string[]`           | Yes      | 1–3 of `action`, `retrieval`, `delegation` — stored (deduped, canonical order) as `applies_to` |
| `name`                 | `string`             | Yes      | Rule name, unique per organization (max 255 chars)                                             |
| `description`          | `string`             | No       | Description (max 1000 chars)                                                                   |
| `rego_source`          | `string`             | Yes      | Policy source (1–100000 chars) in the platform's Rego-subset condition language                |
| `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`                                                     |
| `target_app`           | `string` \| `null`   | No       | Action-facet: hostname/app the rule scopes to (max 255 chars)                                  |
| `action_pattern`       | `string` \| `null`   | No       | Action-facet: action glob the rule pre-filters on (max 255 chars)                              |
| `trust_tier`           | `string` \| `null`   | No       | Retrieval-facet: minimum trust tier the rule requires (max 64 chars)                           |
| `surface`              | `string` \| `null`   | No       | Retrieval-facet: exact-match delivery surface restriction (max 128 chars)                      |
| `principal_exclusions` | `string[]` \| `null` | No       | Retrieval-facet: agent ids the rule excludes                                                   |

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

**Status codes:** `201 Created`, `400 Bad Request` (invalid JSON or body), `401 Unauthorized`, `409 Conflict` (a rule with this name already exists), `500 Internal Server Error`

***

### GET /rules/:id

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

**Permission:** `allow_rules:view`

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

***

### PUT /rules/:id

Update a rule. Every field from `POST /rules` is accepted and optional; at least one is required (an empty body returns `400 No fields to update`). Only provided fields change; supplying `operations` rewrites `applies_to`.

**Permission:** `allow_rules:update`

**Response:** the updated rule object.

**Status codes:** `200 OK`, `400 Bad Request`, `401 Unauthorized`, `404 Not Found`, `409 Conflict` (duplicate name), `500 Internal Server Error`

***

### DELETE /rules/:id

Delete a rule permanently.

**Permission:** `allow_rules:delete`

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

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

***

## Errors & conventions

Every endpoint on this page follows the platform-wide REST conventions — the validation-error body shape, the `/v1/` vs unversioned split, and the API stability policy. See [REST API conventions](/reference/rest-conventions).
