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

# Governance REST API

> Hono-mounted REST/JSON API at /api/governance/* with an auto-generated OpenAPI spec consumed by the TypeScript and Python SDKs. Every governance feature exposes the same CRUD verb set, sharing one service-layer source of truth with the dashboard tRPC procedures, no duplicated business logic, identical RBAC enforcement.

<Info>**Pairs with:** [Governance CLI](/docs/ai-governance/cli) (the CLI is a thin shell over this API) and [Governance MCP server](/docs/ai-governance/mcp) (the MCP tools are a thin shell over this API). All three surfaces dispatch through the same service-layer functions; the dashboard tRPC procedures call into the same services. There is exactly one place each governance verb is implemented.</Info>

<Note>**Service-layer-shared, repository-pattern-backed.** Per `specs/ai-gateway/governance/governance-api-cli-mcp-coverage.feature`, every Hono route delegates to a shared service-layer function (`IngestionTemplateService.updateOttlRules`, `IngestionKeyService.install`, etc.). Services use repositories for persistence; services never import `prisma` directly. The umbrella spec also locks the no-bypass invariant, no UI page, route handler, CLI command, or MCP tool may call `prisma.<governanceModel>.*` directly. Sergey's Lane B-5 commit at `8fffad4ad` locked the invariant for the governance resources shipped in v1: see `langwatch/ee/governance/repositories/ingestionTemplate.repository.ts` and `governanceAudit.repository.ts` (the latter wraps every `AuditLog` write so audit emission also goes through the repository boundary). Repo methods accept `Prisma.TransactionClient | PrismaClient` so they work inside service `$transaction` blocks and against the top-level client transparently.</Note>

## Why a separate REST API alongside tRPC

LangWatch's dashboard already uses tRPC, type-safe, but coupled to the Next.js, React render path. The Hono-mounted REST API exists for the surfaces that *aren't* the dashboard:

* **Agentic workflows**: Claude Code, Codex, Cursor running `langwatch governance …` commands need a stable, OpenAPI-described surface they can call directly.
* **CI, scripting**: the same OpenAPI spec is consumed by the TypeScript and Python SDKs (auto-regen on build), so any pipeline language with a generated client gets the full governance feature set.
* **Future integrations**: partner platforms (SIEMs, ticketing, ops automations) want a documented API contract, not a tRPC client.

Both paths call into the same service-layer functions, there is no business-logic duplication. The umbrella coverage spec locks that invariant.

## Where it lives

| Property    | Value                                                                                                                                                                                                                                                                              |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Mount point | `/api/governance/<resource>`                                                                                                                                                                                                                                                       |
| Spec        | Emitted into the canonical `openapiLangWatch.json` on the next `sdk:regen`, every governance route registers via the same `describeRoute` + `hono-openapi` pipeline as the rest of the public REST API                                                                             |
| Auth        | Project API key, `Authorization: Bearer <projectApiKey>` or `X-Auth-Token`, same as `/api/dataset/*` and `/api/trace/*`. The org for the call derives from the project's team                                                                                                      |
| RBAC        | Per-route ceiling permission for PAT-authed callers (`aiTools:view` on reads, `aiTools:manage` on writes for ingestion-templates; analogous `<resource>:view`, `<resource>:manage` on other resources). Legacy project tokens bypass the ceiling, same model as `gateway-platform` |
| Tenancy     | `organizationId` is derived from `project → team → organization`. Cross-org probes collapse to 404 at the service layer (no enumeration vector)                                                                                                                                    |
| Wire shape  | Paths kebab-case, JSON fields snake\_case (e.g. `ottl_rules`, `source_type`, `display_name`) per public-REST convention                                                                                                                                                            |

## Resource × verb matrix

Every governance resource exposes the full CRUD triple, `list`, `get`, `create`, `update`, `delete`, over Hono, the CLI, and the MCP server. The umbrella spec lists the resource set; each resource gets its own OpenAPI path group.

| Resource              | OpenAPI path group                                                                                    | Detail page                                                                           |
| --------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| Anomaly rules         | `/api/governance/anomaly-rules`                                                                       | [Anomaly rules](/docs/ai-governance/anomaly-rules)                                         |
| Audit log (read-only) | `/api/governance/audit-log`                                                                           | [Audit log](/docs/ai-governance/audit-log)                                                 |
| Gateway budgets       | `/api/governance/gateway-budgets`                                                                     | [Control plane](/docs/ai-governance/control-plane)                                         |
| Ingestion sources     | `/api/governance/ingestion-sources`                                                                   | [Ingestion sources](/docs/ai-governance/ingestion-sources)                                 |
| Ingestion templates   | `/api/governance/ingestion-templates`                                                                 | [Ingestion templates](/docs/ai-governance/ingestion-templates)                             |
| Ingestion keys        | `/api/auth/cli/governance/ingestion-key` (mint) + tRPC `ingestionKey` (`list` / `install` / `rotate`) | [Ingestion templates](/docs/ai-governance/ingestion-templates)                             |
| Members               | `/api/governance/members`                                                                             | [Members and invites](/docs/ai-governance/members-and-invites)                             |
| Invites               | `/api/governance/invites`                                                                             | [Members and invites](/docs/ai-governance/members-and-invites)                             |
| Role bindings         | `/api/governance/role-bindings`                                                                       | [Roles and permissions](/docs/ai-governance/roles-and-permissions)                         |
| AI tool entries       | `/api/governance/ai-tool-entries`                                                                     | [Personas](/docs/ai-governance/personas)                                                   |
| Sessions              | `/api/governance/sessions`                                                                            | [No-spy mode](/docs/ai-governance/no-spy-mode) (sessions live alongside in `/me/sessions`) |
| Virtual keys          | `/api/governance/virtual-keys`                                                                        | [Control plane](/docs/ai-governance/control-plane)                                         |

`audit-log` is intentionally read-only (no create, update, delete), audit rows are emitted by other state-changing routes and are immutable by design.

## Verb shape: worked example

The first surface to ship is `ingestion-templates` (Sergey's Lane B-1 commit). The verb set:

| Verb                  | Method + path                                              | Returns                                                                    | RBAC             |
| --------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------- |
| `list`                | `GET /api/governance/ingestion-templates`                  | `{ data: IngestionTemplateDto[] }` (end-user shape, `ottl_rules` redacted) | `aiTools:view`   |
| `admin-list`          | `GET /api/governance/ingestion-templates/admin`            | `{ data: IngestionTemplateDto[] }` (admin shape, includes `ottl_rules`)    | `aiTools:manage` |
| `get`                 | `GET /api/governance/ingestion-templates/:id`              | `{ ingestion_template: IngestionTemplateDto }`                             | `aiTools:view`   |
| `create`              | `POST /api/governance/ingestion-templates`                 | `{ ingestion_template: IngestionTemplateDto }` (201)                       | `aiTools:manage` |
| `update-ottl-rules`   | `PATCH /api/governance/ingestion-templates/:id/ottl-rules` | `{ ingestion_template: IngestionTemplateDto }`                             | `aiTools:manage` |
| `archive`             | `DELETE /api/governance/ingestion-templates/:id`           | `{ archived: true }` (soft-archive)                                        | `aiTools:manage` |
| `clone-from-platform` | `POST /api/governance/ingestion-templates/clone`           | `{ ingestion_template: IngestionTemplateDto }`                             | `aiTools:manage` |

A few resource-shape notes that generalise across the namespace:

* **End-user vs admin shape**: list endpoints return a redacted shape by default and a separate `/admin` sub-route for the canonical shape. Avoids accidentally leaking the OTTL source to non-admins via list iteration.
* **Resource-specific verbs**: `update-ottl-rules` (instead of generic `update`) and `clone-from-platform` (instead of generic `create`) reflect the domain, admins don't generically PATCH every field; they specifically replace OTTL or clone a platform row. Other resources will introduce their own resource-specific verbs (`rotate` on ingestion sources, `assign-to-user` on role bindings, `revoke` on sessions) the same way.
* **Error mapping**: `403 PlatformTemplateImmutable` (admin tried to PATCH a platform-published row), `404 TemplateNotFound` (cross-org probe), `400 InvalidSourceType`, validation. All map to a common `errorSchema` with `{ type, code, message }`.

The second resource to ship is `ingestion-keys` (Sergey's Lane B-2 commit at `5275e7e11`). An ingestion key is just an `ApiKey` (prefix `sk-lw-`) scoped to one project with an ingest-only role, there is no separate binding model. The CLI mints one through a dedicated auth route; the dashboard manages the rest through the `ingestionKey` tRPC router (`list` / `install` / `rotate`):

| Verb      | Method + path                                                          | Returns                                                                                                   | RBAC             |
| --------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------- |
| `mint`    | `POST /api/auth/cli/governance/ingestion-key` (body `{ source_type }`) | `{ token: "sk-lw-…", prefix, endpoint }` (token returned exactly once)                                    | `aiTools:manage` |
| `list`    | tRPC `ingestionKey.list`                                               | `{ data: IngestionKeyDto[] }` (project-scoped; only the `sk-lw-` prefix is visible, never the full token) | `aiTools:manage` |
| `install` | tRPC `ingestionKey.install`                                            | `{ key: ..., token: "sk-lw-…" }` (token returned exactly once)                                            | `aiTools:manage` |
| `rotate`  | tRPC `ingestionKey.rotate`                                             | `{ key: ..., token: "sk-lw-…" }` (hard-cut rotation per spec, old token is invalid immediately)           | `aiTools:manage` |

Two consequences worth noting:

* **Ingest-only role, single project**: the minted `ApiKey` carries an ingest-only role scoped to exactly one project, so the token can write traces into that project but cannot read or mutate any other governance resource. The full `sk-lw-…` secret is shown exactly once at mint/install/rotate; thereafter only the prefix is returned.
* **`source_type` selects the shaping**: the mint body's `source_type` records which upstream tool the key ingests for, so the gateway can apply the matching ingestion-template OTTL transform when traces arrive on that key. A key minted for an org-authored template carries that template's id.

The Zod schemas backing each request and response body are the same schemas the corresponding service-layer function consumes. The OpenAPI generator (`describeRoute` + `hono-openapi`) reads them directly, so the spec and the runtime are guaranteed in sync.

## OpenAPI spec generation + SDK regen

Governance routes register through the same `describeRoute` + `hono-openapi` pipeline as the rest of the public REST API. The canonical OpenAPI spec lives at `langwatch/openapiLangWatch.json`, and both SDKs regenerate from it on the standard build target.

```bash theme={null}
pnpm openapi:emit                  # regenerate openapiLangWatch.json from Hono routes
pnpm sdk:regen --target typescript # regenerate the TS SDK
pnpm sdk:regen --target python     # regenerate the Python SDK
```

<Note>**TBD-IMPL, exact script names**: Sergey's first route SHA at `0bb951160` mounts ingestion-templates into `api-router` + `generateOpenAPISpec` so the OpenAPI shape lands in `openapiLangWatch.json` on the next regen. The exact `package.json` script names fold in once the SDK regen target lands.</Note>

The TS SDK exposes `langwatch.governance.<resource>.<verb>(...)`, the Python SDK exposes `langwatch.governance.<resource>.<verb>(...)`. Type signatures match the Zod-derived schemas exactly.

## Audit emission

State-changing calls always emit an audit row from the service layer (the same row a dashboard tRPC mutation would emit, there is one service-layer audit emitter, not three). Every audit row carries a **surface attribution** tag stamped into `AuditLog.metadata.surface`:

| Surface        | `metadata.surface` value |
| -------------- | ------------------------ |
| Dashboard tRPC | `"trpc"` (default)       |
| Hono REST      | `"hono"`                 |
| CLI            | `"cli"`                  |
| MCP            | `"mcp"`                  |

The shared type lives at `@ee/governance/services/auditSurface.ts` (`GovernanceCallSurface`); every mutating service method on the governance services takes an optional `surface` parameter (default `"trpc"`) which is stamped into `metadata` at audit emission. Forensic readers query `metadata->>'surface'` to filter by surface.

```sql theme={null}
-- "What did automation change in the last 24h?"
SELECT * FROM "AuditLog"
 WHERE "createdAt" > now() - interval '24 hours'
   AND metadata->>'surface' IN ('cli', 'mcp')
 ORDER BY "createdAt" DESC;
```

All four rows have **identical** payload shapes apart from the `metadata.surface` field, same event kind, same target, same actor. Surface attribution helps incident response (which automation made this change?) without changing the audit-row event-kind taxonomy.

The mutating verbs across the two shipped resources (`createOrgTemplate`, `updateOttlRules`, `archiveOrgTemplate`, `cloneFromPlatform` on templates; `install`, `rotate` on ingestion keys) all carry surface attribution as of Sergey's Lane B-3 commit at `fc6d54100`. Minting or rotating an ingestion key emits a `gateway.ingestion_key.minted` audit row, and revoking one emits `gateway.ingestion_key.revoked`.

## Verifying the contract

The wire shape, request bodies, response envelopes, status-code mapping, and surface attribution, is locked by two integration tests that run against real Postgres + the real Hono pipeline (no service mocks):

**Ingestion-templates wire shape**: `langwatch/src/app/api/governance/__tests__/governance-rest-api.integration.test.ts`. 14 scenarios (Lane B-4 at `1839d9f54`) covering the full ingestion-templates verb set, 401, 400, 403, 404 envelopes, and a wire-level audit-uniform assertion that `metadata.surface === "hono"` for state-changing calls.

**Ingestion-key wire shape**: `langwatch/src/app/api/governance/__tests__/governance-ingestion-keys.integration.test.ts`. 9 scenarios (Lane B-6 at `60f769498`) covering the ingestion-key flows. Specifically locks:

* the mint route returns a one-time `sk-lw-…` token and thereafter exposes only the prefix
* `install` and `rotate` audit rows both stamp `metadata.surface === "hono"` end-to-end (cross-validates the B-3 surface threading at `fc6d54100`)
* `rotate` emits `gateway.ingestion_key.minted` and revoking a key emits `gateway.ingestion_key.revoked` at the wire level, previously unit-only

The setup pattern in this file (project + ingest-only RoleBinding + `aiTools:manage` caller) is the canonical reference for any downstream lane (CLI, MCP) that needs an ingestion-key test path.

**No-bypass invariant (CI-enforced)**: `langwatch/ee/governance/repositories/__tests__/no-bypass.unit.test.ts`. 3 tests (Lane B-7 at `94c219035`) statically reject any future PR that adds a direct `prisma.ingestionTemplate.*` call outside the allowlist. Locks the umbrella spec's `@no-bypass` invariant in CI so the repository-pattern boundary won't silently drift.

**MCP audit-uniform regression**: `langwatch/src/mcp/__tests__/governance-tools.audit-uniform.integration.test.ts`. 3 cases (Lane B-MCP audit at `66fd35162`) prove the Path B contract end-to-end: `governance_ingestion_templates_create` and `governance_ingestion_keys_mint` both stamp `metadata.surface === "mcp"` against real Postgres, and the `AUTH_REQUIRED:` negative case stays closed (write tool with no `callerUserId` fails before any audit row is written).

**Cross-surface audit-uniformity regression**: `langwatch/ee/governance/services/__tests__/auditSurface.crossSurface.integration.test.ts`. Invokes `createOrgTemplate` via all four surfaces, tRPC service-direct, Hono REST, CLI REST (with `X-LangWatch-Surface: cli`), and MCP service-direct, in one test (Lane B-8 at `d96cd4300`, extended to 4 surfaces at `cb4c8224c`); asserts the four audit rows have **identical** payload shape, same `action`, same `targetKind`, same `organizationId`, same metadata-key-set, with only `metadata.surface` varying (`"trpc"`, `"hono"`, `"cli"`, `"mcp"`). Slug regex format identical, no default-fallback leakage.

**Surface-spoof rejection regression**: same file as above (Lane B-8 extension at `cb4c8224c`). Fires three Hono POSTs with `X-LangWatch-Surface` set to `trpc`, `mcp`, and `evil`; asserts all three audit rows fall back to `metadata.surface === "hono"`. Locks Alexis's `GovernanceCallSurface` enum filter at `resolveSurfaceFromRequest` as the defense against external HTTP callers forging in-process surface tags, the only legal value the Hono surface accepts on inbound is `cli`; everything else falls through to the route's default `"hono"`. Together with the cross-surface uniformity test this exhaustively pins the umbrella spec's `@audit-uniform` invariant for v1.

To run locally:

```bash theme={null}
cd langwatch
pnpm test:integration governance-rest-api.integration.test.ts
pnpm test:integration governance-ingestion-keys.integration.test.ts
pnpm test:integration governance-tools.audit-uniform.integration.test.ts
pnpm test:integration auditSurface.crossSurface.integration.test.ts
pnpm test:unit no-bypass.unit.test.ts
```

Use these files as the authoritative reference when implementing a CLI command or MCP tool against a governance route, the request/response shape and audit-stamping invariants that live there are exactly what each route accepts, returns, and emits, and the no-bypass test will reject any direct-prisma shortcut at PR time.

## Cross-references

* [Governance CLI](/docs/ai-governance/cli): `langwatch governance <resource> <verb>` thin shell
* [Governance MCP server](/docs/ai-governance/mcp): same surface as MCP tools for agent use
* [Roles and permissions](/docs/ai-governance/roles-and-permissions): RBAC scopes that gate each verb
* [Audit log](/docs/ai-governance/audit-log): where state-change rows land
* [Compliance architecture](/docs/ai-governance/compliance-architecture): how the OCSF v1.1 export consumes the same audit stream
* Spec, `specs/ai-gateway/governance/governance-api-cli-mcp-coverage.feature`
* v1 scope-fence audit, `specs/ai-gateway/governance/agentic-first-parity-v1-status.md` (12-resource × 4-surface matrix on shipped SHAs; scoring 2/12 full parity, 8/12 tRPC-only deferred to follow-on)
* Wire-shape lock, `langwatch/src/app/api/governance/__tests__/governance-rest-api.integration.test.ts`
