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

> ## Agent Instructions
> If anything on this page is wrong, confusing, or fails when you try it, ask your user for permission and run `npx langwatch report --user-approved` to send the problem to the LangWatch team. No login is needed and secrets are redacted locally. `npx langwatch report --help` has the details.

# Governance REST API

> Hono-mounted REST/JSON API at /api/v1/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.

> [!NOTE]
>
> **Pairs with:** [Governance CLI](https://langwatch.ai/docs/ai-governance/cli.md) (the CLI is a thin shell over this API) and
> [Governance MCP server](https://langwatch.ai/docs/ai-governance/mcp.md) (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.

> [!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. The governance repositories hold that boundary: see `enterprise/modules/governance/process/src/repositories/prisma/prisma.ingestion-template.repository.ts` and `modules/audit-log/process/src/repositories/prisma/prisma.audit-log.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.

## Why a separate REST API alongside tRPC

LangWatch's dashboard already uses tRPC, type-safe, but coupled to the Vite React app that renders it. The Hono-mounted REST API exists for the surfaces outside 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/v1/governance/<resource>` |
| Spec | Emitted into the canonical `openapiLangWatch.json` on the next spec regeneration, 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/v1/dataset/*` and `/api/v1/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

Governance resources expose the full CRUD set, `list`, `get`, `create`, `update`, `delete`, over Hono, the CLI, and the MCP server, except where the table below names a narrower verb set (`audit-log` is read-only, ingestion keys mint through their own route). The umbrella spec lists the resource set; each resource gets its own OpenAPI path group.

Anomaly rules are not in the table: they have no REST surface and no authoring page, see [Anomaly rules](https://langwatch.ai/docs/ai-governance/anomaly-rules.md).

| Resource | OpenAPI path group | Detail page |
| - | - | - |
| Audit log (read-only) | `/api/governance/audit-log` | [Audit log](https://langwatch.ai/docs/platform/audit-log.md) |
| Gateway budgets | `/api/governance/gateway-budgets` | [Control plane](https://langwatch.ai/docs/ai-governance/control-plane.md) |
| Ingestion sources | `/api/governance/ingestion-sources` | [Ingestion sources](https://langwatch.ai/docs/ai-governance/ingestion-sources.md) |
| Ingestion templates | `/api/governance/ingestion-templates` | [Ingestion templates](https://langwatch.ai/docs/ai-governance/ingestion-templates.md) |
| Ingestion keys | `/api/auth/cli/governance/ingestion-key` (mint) + tRPC `ingestionKey` (`list` / `install` / `rotate` / `revoke`) | [Ingestion templates](https://langwatch.ai/docs/ai-governance/ingestion-templates.md) |
| Members | `/api/governance/members` | [Members and Teams](https://langwatch.ai/docs/platform/members-and-teams.md) |
| Invites | `/api/governance/invites` | [Members and Teams](https://langwatch.ai/docs/platform/members-and-teams.md) |
| Role bindings | `/api/governance/role-bindings` | [Access Control (RBAC)](https://langwatch.ai/docs/platform/rbac.md) |
| AI tool entries | `/api/governance/ai-tool-entries` | [Personas](https://langwatch.ai/docs/ai-governance/personas.md) |
| Sessions | `/api/governance/sessions` | [No-spy mode](https://langwatch.ai/docs/ai-governance/no-spy-mode.md) (sessions live alongside in `/me/devices`) |
| Virtual keys | `/api/governance/virtual-keys` | [Control plane](https://langwatch.ai/docs/ai-governance/control-plane.md) |

`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` (Lane B-1 commit). The verb set:

| Verb | Method + path | Returns | RBAC |
| - | - | - | - |
| `list` | `GET /api/v1/governance/ingestion-templates` | `{ data: IngestionTemplateDto[] }` (end-user shape, `ottl_rules` redacted) | `aiTools:view` |
| `admin-list` | `GET /api/v1/governance/ingestion-templates/admin` | `{ data: IngestionTemplateDto[] }` (admin shape, includes `ottl_rules`) | `aiTools:manage` |
| `get` | `GET /api/v1/governance/ingestion-templates/:id` | `{ ingestion_template: IngestionTemplateDto }` | `aiTools:view` |
| `create` | `POST /api/v1/governance/ingestion-templates` | `{ ingestion_template: IngestionTemplateDto }` (201) | `aiTools:manage` |
| `update-ottl-rules` | `PATCH /api/v1/governance/ingestion-templates/:id/ottl-rules` | `{ ingestion_template: IngestionTemplateDto }` | `aiTools:manage` |
| `archive` | `DELETE /api/v1/governance/ingestion-templates/:id` | `{ archived: true }` (soft-archive) | `aiTools:manage` |
| `clone-from-platform` | `POST /api/v1/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`. An ingestion key is just an `ApiKey` (prefix `ik-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` / `revoke`):

| Verb | Method + path | Returns | RBAC |
| - | - | - | - |
| `mint` | `POST /api/auth/cli/governance/ingestion-key` (body `{ source_type }`) | `{ token: "ik-lw-…", prefix, endpoint }` (token returned exactly once) | device session |
| `list` | tRPC `ingestionKey.list` | one row per live key: `apiKeyId`, `sourceType`, `deviceLabel`, `parentApiKeyId`, `createdAtMs`, `lastUsedAtMs` (never the token) | `organization:view` |
| `install` | tRPC `ingestionKey.install` | `{ token: "ik-lw-…", prefix, apiKeyId }` (token returned exactly once) | `organization:view` |
| `rotate` | tRPC `ingestionKey.rotate` | the same plus `revokedCount` and `revokedDeviceLabels` (every previous key for the source is refused immediately) | `organization:view` |
| `revoke` | tRPC `ingestionKey.revoke` (`{ apiKeyId }`) | `{ success: true }`; idempotent, another person's key answers `ingestion_key_not_found` | `organization:view` |

Three consequences:

* **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 `ik-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.
* **A personal CLI key follows its session**: a personal key the CLI mints stores the session's login key as `parentApiKeyId`. Logout, a devices-tab revoke, a re-login from the same device and session expiry all revoke that login key, and every key parented to it goes with it. A mint from a revoked session answers 401.
* **Each side records its own cause**: the login key carries why it died (`user` for a logout or a devices-tab revoke, `rotation` for a re-login, `expired` for a session that ran out, `offboarded` for a membership that ended). The keys under it carry the same cause, except `user`, which reaches them as `session`. A re-login therefore leaves `rotation` on the keys, which is what tells the CLI it may re-mint under the session that replaced theirs.
* **A project CLI key does not**: a key minted into a named team project carries no parent and outlives the session that asked for it.
* **Only the CLI mints for a tool it wraps**: `install` and the MCP mint accept only source types a published template names, and a wrapped tool answers `ingestion_key_source_not_allowed`.

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 OpenAPI document is generated from the route declarations and is not committed. The api builds it at boot and serves it at `/api/openapi.json`. The SDK clients and the copy published alongside these docs, `docs/api-reference/openapiLangWatch.json`, are generated from it and committed, and CI fails when they no longer match the routes.

```bash theme={null}
# from the repo root: write the document to specs/api-reference/openapi-document.json (gitignored)
pnpm --filter @langwatch/platform-api run openapi:generate

# from the repo root: regenerate the TypeScript, Python and Go clients and the docs copy
make sync-all-openapi
```

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`, `revoke` on ingestion keys) all carry surface attribution. Minting an ingestion key emits an `ingestionKey.mint` audit row, rotating a source emits `ingestionKey.rotate` with the count of keys it revoked, and revoking one emits `ingestionKey.revoke`.

## Verifying the contract

The wire shape, request bodies, response envelopes, status-code mapping, and surface attribution, is locked by the governance transport and application suites, which drive the real Hono pipeline and the real application over stubbed services:

**Ingestion-templates wire shape**: `enterprise/modules/governance/process/src/app/__tests__/governance.transport.unit.test.ts`. 20 cases covering the full ingestion-templates verb set, the 401, 400, 403 and 404 envelopes in this family's nested `{ error: { type, code, message } }` body, and the audit-uniform assertion that a state-changing call is attributed with `surface: "hono"`. It is the successor to the monolith's `governance-rest-api.integration.test.ts`: what that suite proved about the DOOR moved here, and what it proved about the DOMAIN moved to the service tests beside it.

**Surface-spoof rejection**: same file. Fires Hono POSTs with `X-LangWatch-Surface` set to `cli` and to in-process values, and asserts only `cli` is honoured while everything else falls back to `"hono"`. `resolveSurfaceFromRequest` in `enterprise/modules/governance/process/src/transport/governance.rest.ts` filters the header against the `GovernanceCallSurface` vocabulary, which is what stops an external HTTP caller forging an in-process surface tag: `cli` is the only value the Hono surface accepts on inbound, and `trpc` and `mcp` are reachable only in-process.

**Cross-surface audit uniformity**: `enterprise/modules/governance/process/src/app/__tests__/governance.app.unit.test.ts`, scenario "audit rows are identical apart from metadata.surface". Creates the same organization template over all four surfaces, tRPC, Hono REST, CLI and MCP, and asserts the four writes are field-for-field identical apart from `surface`. Surface attribution therefore cannot drift into a second opinion about what a write means, and there is no separate MCP-transport regression because the MCP tool reaches this same application call.

**No-bypass invariant (CI-enforced)**: the `prisma-containment` policy in `packages/architecture-enforcer/src/prisma-boundaries.ts`, run by `pnpm lint:architecture`. It rejects any file importing generated Prisma outside a repository under `server/src/repositories/prisma/` or the Postgres composition adapter `server/src/adapters/postgres.<subject>.adapter.ts`. That is the umbrella spec's `@no-bypass` invariant enforced repo-wide rather than for governance alone, so the repository-pattern boundary cannot silently drift.

**Ingestion-key flows**: `enterprise/modules/governance/process/src/services/__tests__/ingestion-key.service.unit.test.ts` pins the service-level ordering, that a replacement key is minted only after the prior one is hard-revoked, and that an additive issue leaves another machine's key alone. The wire-level guarantees below are contract rather than covered ground, they carry no test of their own today:

* the mint route answers 201 with an `ik-lw-…` token, its prefix and the OTLP endpoint, and a second mint for the same source returns a different token rather than replacing the first
* a mint from a CLI session stores that session's login key as `parentApiKeyId` and the session's device label, and a mint from a session whose login key was revoked answers 401
* the describe route reports a key's status and its `revocation_cause`, which is what the CLI reads before it re-mints

Surface attribution is not asserted there. `metadata.surface` on the ingestion-key audit rows is locked by `platform/app/src/mcp/__tests__/governance-tools.audit-uniform.integration.test.ts` and `platform/app/ee/governance/services/__tests__/auditSurface.crossSurface.integration.test.ts`.

The `organization:view` caller plus a project-scoped ingest-only RoleBinding is the setup pattern any downstream lane (CLI, MCP) needs for an ingestion-key test path. The cascade itself is locked by `auth-cli-logout.integration.test.ts` and `auth-cli-refresh-expiry.integration.test.ts`, and the service rules by `platform/app/ee/governance/services/__tests__/ingestionKey.lifecycle.unit.test.ts`.

**No-bypass invariant (CI-enforced)**: `platform/app/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**: `platform/app/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**: `platform/app/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`, and asserts all three audit rows fall back to `metadata.surface === "hono"`.

That locks the `GovernanceCallSurface` enum filter at `resolveSurfaceFromRequest` as the defense against external HTTP callers forging in-process surface tags. The only value the Hono surface accepts on inbound is `cli`; everything else falls through to the route's default `"hono"`. With the cross-surface uniformity test, this pins the umbrella spec's `@audit-uniform` invariant for v1.

`cli` itself is a hint from an already-authenticated caller rather than a claim the route can verify, since the CLI presents the same credentials any other HTTP client does. Read it as "which client said it did this", never as a permission.

To run locally:

```bash theme={null}
pnpm --filter @langwatch/enterprise-governance-process test:unit
pnpm lint:architecture
```

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 architecture lint will reject any direct-prisma shortcut at PR time.

## Cross-references

* [Governance CLI](https://langwatch.ai/docs/ai-governance/cli.md): `langwatch governance <resource> <verb>` thin shell
* [Governance MCP server](https://langwatch.ai/docs/ai-governance/mcp.md): same surface as MCP tools for agent use
* [Access Control (RBAC)](https://langwatch.ai/docs/platform/rbac.md): RBAC scopes that gate each verb
* [Audit log](https://langwatch.ai/docs/platform/audit-log.md): where state-change rows land
* [Compliance architecture](https://langwatch.ai/docs/ai-governance/compliance-architecture.md): 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, `enterprise/modules/governance/process/src/app/__tests__/governance.transport.unit.test.ts`


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.