---
title: "KYE Protocol™ App API | API reference"
description: "KYE Protocol™ App API: 132 operations from app.yaml, a published KYE Protocol™ OpenAPI contract."
url: https://kyeprotocol.com/developers/api/app/
lang: en
source: "KYE Protocol"
---

> KYE Protocol™ App API: 132 operations from app.yaml, a published KYE Protocol™ OpenAPI contract.

API reference

# KYE Protocol™ App API

132 operations · `app.yaml`

**Servers**

`https://app.kyeprotocol.com/api/v1`

**Version**

1.0.0

**Source**

[app.yaml](https://kyeprotocol.com/developers/api/app.yaml)

Clerk-gated customer app surface for app.kyeprotocol.com. All endpoints require a Clerk JWT. Responses are automatically scoped to the caller's tenant derived from the JWT org\_id. All entity endpoints are read-only for tenant principals.

Tenants 5 LegalEntities 2 BillingAccounts 2 Domains 2 Policies 2 Workspaces 2 Projects 2 Teams 2 Principals 2 Resources 2 Models 2 Tools 2 ExternalApps 2 AuditStreams 2 StateRegistry 2 StateLibrary 3 Analytics 2 Dashboard 5 Authority 3 Entities 3 Search 1 Events 1 Runtime 2 Memory 1 Onboarding 1 Purposes 3 DataGovernance 3 Evidence 9 Approvals 1 Actions 1 Agents 2 AICalls 1 Webhooks 4 ApiKeys 6 Apps 2 AssuranceCards 2 AuditEvents 2 BehaviourModel 2 Billing 2 Connectors 2 Delegations 3 DriftEvents 3 Decisions 2 Listings 2 Partners 2 Plugins 2 PurposePermissions 4 Replay 2 Risk 2 Scopes 2 StateMachines 2 Stripe 3 Usage 1 WhiteLabel 1 Widgets 4

## Tenants

GET `/tenants` Get caller's own tenant

**Auth:** Session (Clerk JWT)

### Responses

- `200` OK
- `404` Tenant not yet provisioned

GET `/tenants/{id}` Get tenant by id (only own tenant allowed)

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` OK
- `403` Cross-tenant access attempt
- `404` Resource not found

GET `/reports` List signed report envelopes for the caller's tenant (KYE™ Reporting Engine™ tenant view)

**Auth:** Session (Clerk JWT)

### Responses

- `200` OK
- `401` Missing or invalid bearer token

GET `/settings` Load merged tenant settings

**Auth:** Session (Clerk JWT)

### Responses

- `200` OK
- `401` Missing or invalid bearer token

PATCH `/settings` Update one or more tenant settings fields

**Auth:** Session (Clerk JWT)

### Responses

- `200` OK
- `400` Invalid field
- `401` Missing or invalid bearer token

## LegalEntities

GET `/legal-entities`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `limit` | query | integer |  |
| `offset` | query | integer |  |

### Responses

- `200` OK

GET `/legal-entities/{id}`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` OK
- `403` Cross-tenant access attempt
- `404` Resource not found

## BillingAccounts

GET `/billing-accounts`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `limit` | query | integer |  |

### Responses

- `200` OK

GET `/billing-accounts/{id}`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` OK
- `403` Cross-tenant access attempt

## Domains

GET `/domains`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `limit` | query | integer |  |

### Responses

- `200` OK

GET `/domains/{id}`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` OK
- `403` Cross-tenant access attempt

## Policies

GET `/policies`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `limit` | query | integer |  |

### Responses

- `200` OK

GET `/policies/{id}`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` OK
- `403` Cross-tenant access attempt

## Workspaces

GET `/workspaces`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `limit` | query | integer |  |

### Responses

- `200` OK

GET `/workspaces/{id}`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` OK
- `403` Cross-tenant access attempt

## Projects

GET `/projects`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `limit` | query | integer |  |

### Responses

- `200` OK

GET `/projects/{id}`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` OK
- `403` Cross-tenant access attempt

## Teams

GET `/teams`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `limit` | query | integer |  |

### Responses

- `200` OK

GET `/teams/{id}`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` OK
- `403` Cross-tenant access attempt

## Principals

GET `/principals`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `limit` | query | integer |  |

### Responses

- `200` OK

GET `/principals/{id}`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` OK
- `403` Cross-tenant access attempt

## Resources

GET `/resources`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `limit` | query | integer |  |

### Responses

- `200` OK

GET `/resources/{id}`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` OK
- `403` Cross-tenant access attempt

## Models

GET `/models`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `limit` | query | integer |  |

### Responses

- `200` OK

GET `/models/{id}`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` OK
- `403` Cross-tenant access attempt

## Tools

GET `/tools`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `limit` | query | integer |  |

### Responses

- `200` OK

GET `/tools/{id}`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` OK
- `403` Cross-tenant access attempt

## ExternalApps

GET `/external-apps`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `limit` | query | integer |  |

### Responses

- `200` OK

GET `/external-apps/{id}`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` OK
- `403` Cross-tenant access attempt

## AuditStreams

GET `/audit-streams`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `limit` | query | integer |  |

### Responses

- `200` OK

GET `/audit-streams/{id}`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` OK
- `403` Cross-tenant access attempt

## StateRegistry

GET `/state-events` List state events for an entity (tenant-scoped)

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `entity_id` **required** | query | string |  |
| `limit` | query | integer |  |

### Responses

- `200` OK
- `400` entity\_id required
- `403` Cross-tenant access attempt

GET `/state-events/{id}`

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` OK
- `403` Cross-tenant access attempt
- `404` Resource not found

## StateLibrary

GET `/state-library` Browse published KYE™ State Library entries

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `category` | query | string |  |
| `include` | query | string |  |

### Responses

- `200` OK

GET `/state-machines/from-library` List state machine derivations for caller's tenant

**Auth:** Session (Clerk JWT)

### Responses

- `200` OK

POST `/state-machines/from-library` Adopt a State Library entry into caller's tenant

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `library_id` **required** | string |  |
| `library_version` **required** | string |  |
| `tenant_entity_class` **required** | string |  |
| `overrides` | object |  |

### Responses

- `200` OK
- `400` Validation error
- `404` Library entry not found
- `409` Seal mismatch

## Analytics

GET `/analytics-decisions-per-hour` Hourly decision rollup for the caller's tenant

Aggregates the tenant's decisions ledger into hourly buckets with per-verdict counts (allow / deny / review / quarantine). The window is capped at 90 days; longer windows are served by the warehouse path on the gateway-worker. Powers the usage sparkline and dashboard time-series drilldowns.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `since` | query | string | Window start (defaults to until minus 7 days) |
| `until` | query | string | Window end (defaults to now) |

### Responses

- `200` Hourly rows with decision\_count, allow\_count, deny\_count, review\_count, quarantine\_count
- `400` Invalid since/until, since not before until, or window over 90 days
- `503` Required D1 / service binding not attached (db\_binding\_missing)

GET `/analytics-widget-calls` Per-widget call counts for the caller's tenant

Groups the tenant's widget\_calls telemetry by widget\_slug over the requested window (default trailing 7 days), reporting total and successful call counts per widget.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `since` | query | string | Window start (defaults to until minus 7 days) |
| `until` | query | string | Window end (defaults to now) |

### Responses

- `200` Per-widget summary rows with widget\_slug, total\_calls, ok\_calls
- `400` Invalid since or until timestamp
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Dashboard

GET `/dashboard-stats` Seven top-level dashboard KPIs for the caller's tenant

Honest D1 aggregates scoped to the caller's tenant — decisions today, pending reconfirmations (Purpose Permissions expiring within 30 days), open drift events, evidence packs awaiting sign-off, 24h decision breakdown, authority finality (in-force / attenuated / revoked delegations + p99 lookup), and the most active agent today. Cold tenants get flat zeros, never fabricated values.

**Auth:** Session (Clerk JWT)

### Responses

- `200` KPI snapshot for the tenant
- `503` Required D1 / service binding not attached (db\_binding\_missing)

GET `/dashboard-snapshot` Combined dashboard snapshot (SSE poll-fallback)

Poll fallback for clients whose EventSource connection to /stream/dashboard fails. Returns the last 50 decisions, the authority graph, the 24x7 density heatmap, the edge infra topology, and the §0.3 attestation envelope for the read. A client-supplied tenant\_id that diverges from the session-resolved tenant is refused per §0.11.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `tenant_id` | query | string | Optional echo of the caller's tenant; refused with 403 if it diverges from the session |

### Responses

- `200` Snapshot with decisions, density\_heatmap, authority\_graph, infra\_topology, attestation
- `401` Missing or invalid bearer token
- `403` cross\_tenant\_smuggle\_refused — client tenant\_id diverges from session (refusal attestation included)
- `503` Required D1 / service binding not attached (db\_binding\_missing)

GET `/stream/dashboard` Server-Sent Events stream for the realtime dashboard

Long-lived text/event-stream emitting typed events — subscribe envelope on open, then decision, authority\_change, metric\_tick and 25-second ping heartbeats, with a closing kye.compliance.attestation.v1 envelope. Connections are capped at 5 minutes; EventSource auto-reconnect re-verifies the rotating Clerk JWT. Client-supplied tenant\_id diverging from the session is refused before the stream opens (§0.11).

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `tenant_id` | query | string | Optional echo of the caller's tenant; refused with 403 if it diverges from the session |

### Responses

- `200` SSE stream of kye.dashboard.event.v1 frames
- `401` Missing or invalid bearer token
- `403` cross\_tenant\_smuggle\_refused — client tenant\_id diverges from session
- `503` Required D1 / service binding not attached (db\_binding\_missing)

GET `/density-heatmap` 24x7 decision-density grid for the caller's tenant

Buckets the tenant's decisions by UTC day-of-week and hour-of-day into a kye.density\_heatmap.v1 snapshot with per-cell allow / deny / review counts. Default window is the trailing 7 days.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `days` | query | integer | Trailing window in days |
| `tenant_id` | query | string | Optional echo of the caller's tenant; refused with 403 if it diverges from the session |

### Responses

- `200` kye.density\_heatmap.v1 snapshot with cells, max\_cell\_count, total\_count, attestation
- `401` Missing or invalid bearer token
- `403` cross\_tenant\_smuggle\_refused — client tenant\_id diverges from session
- `503` Required D1 / service binding not attached (db\_binding\_missing)

GET `/infra-topology` Edge-infrastructure topology snapshot

Returns the kye.infra\_topology.v1 graph (Worker, D1, Queue, R2, KV nodes plus read/write/produce/consume edges) for the kye-infra-topology web component. Each node carries the §51 No-SPOF posture; D1 status reflects whether the KYE\_DB binding is reachable. Per-tenant throughput values are honest zeros until the per-binding counter export ships.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `tenant_id` | query | string | Optional echo of the caller's tenant; refused with 403 if it diverges from the session |

### Responses

- `200` kye.infra\_topology.v1 snapshot with nodes, edges, attestation
- `401` Missing or invalid bearer token
- `403` cross\_tenant\_smuggle\_refused — client tenant\_id diverges from session

## Authority

GET `/authority-graph` Tenant-wide authority graph of in-force delegations

Derives a kye.authority\_graph.v1 node/edge graph from the tenant's most recent 200 delegations — nodes classified by URN segment (agent / capability / scope / policy / delegate / principal), edges typed delegates\_to with an in\_force flag and audit\_ref. Includes a §0.3 attestation envelope.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `tenant_id` | query | string | Optional echo of the caller's tenant; refused with 403 if it diverges from the session |

### Responses

- `200` kye.authority\_graph.v1 snapshot with nodes, edges, attestation
- `401` Missing or invalid bearer token
- `403` cross\_tenant\_smuggle\_refused — client tenant\_id diverges from session
- `503` Required D1 / service binding not attached (db\_binding\_missing)

GET `/authority-wallet` List the tenant's wallet credentials with KPI roll-up

A wallet credential is a presentable, revocable, downstream-delegatable authority grant held by the tenant principal. Read-only — credentials enter the wallet when grants are accepted or Operating Model seals are published. KPI counts active credentials, active delegations issued, credentials presentable for at most 30 more days, and revocations in the last 30 days; includes distinct holders for the filter bar.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Credential rows (credential\_id, credential\_class, issuer, holder, presentable\_until, status, issued\_at, revoked\_at) plus kpi and holders
- `503` Required D1 / service binding not attached (db\_binding\_missing)

GET `/authorities` Authorities held by the calling tenant, with issuer rollup

**Auth:** Session (Clerk JWT)

### Responses

- `200` Authorities, their issuers and a KPI rollup
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Entities

GET `/entities` List the tenant's entity inventory with KPI roll-up

Every actor bound to the tenant — principals, agents, model endpoints, tools, datasets, connectors, partners — each identified by a stable kye URN. Read-only; entities are registered through the Operating Model authoring flow, not created here. KPI counts agents, people and tools by entity class.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Entity list with id, entity\_class, display\_name, trust\_domain, status, last\_seen, plus kpi
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/entities` Admit an entity into the governed graph

The admission half of the one governed register. Validates the request against kye.entity\_registration.v1, checks whether the registrant may register into this trust domain, then runs the decision engine for the action entity.register. entity.register is a consequential action, so a request carrying no recorded approval is HELD (202) and nothing is written; the row and the decision that admitted it are created together or not at all. trust\_domain\_id is taken from the verified session and any value supplied in the body is ignored — a caller-asserted scope is not a scope.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `entity_id` **required** | string | Canonical entity URN |
| `entity_type` **required** | string |  |
| `display_name` | string,null |  |
| `registered_by_entity_id` **required** | string | The registering entity's URN |
| `basis` **required** | string | One of self\_registration, delegated\_registration, bulk\_import, agent\_nomination\_admitted, bootstrap\_genesis |
| `delegation_id` | string,null | Required for delegated\_registration and bulk\_import |
| `nomination_ref` | string,null | Required for agent\_nomination\_admitted |
| `consequential_action_classes` | array |  |
| `labels` | array |  |
| `approval_recorded` | boolean | Whether an approval for this admission is on record |

### Responses

- `201` Admitted — entity\_id plus the decision that admitted it
- `202` Held for approval — reason\_code and decision reference; nothing written
- `400` Contract violation — the offending field is named
- `403` Refused — registrant may not register into this trust domain
- `503` Required D1 / service binding not attached (db\_binding\_missing)

GET `/hierarchy` Entity-relation tree for the caller's tenant

Builds the full adjacency-list tree from the tenant's hierarchy nodes (delegation / containment / binding edges) with a KPI roll-up of node\_count, depth, agent\_count and principal\_count. Read-only — the tree is derived from the operating model, not authored here.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Nested tree of nodes with node\_class, label, lifecycle, edge\_kind, children, plus kpi
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Search

GET `/search` Tenant-scoped search across entities and decisions

Prefers the private kye-search-engine worker over a service binding (signed kye.search\_result.v1 envelope, lexical mode); falls back to tenant-scoped D1 LIKE matching across the entities and decisions tables when the binding is absent or the engine errors. An empty q returns an empty hit list.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `q` | query | string | Search term |
| `index` | query | string | Optional index filter (app\_entities, decisions, library\_entries, state\_events, policies) |

### Responses

- `200` Flat hit list with id, kind, title, snippet (plus score/classification on the engine path); source field names the path taken
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Events

GET `/events/search` Query the tenant's event index

Searches the tenant's indexed event store with full-text matching; when a free-text q is supplied. All filters combine with AND; rows are newest-first. Pure read with no mutation.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `q` | query | string | Free-text FTS5 match |
| `family` | query | string | Event family id |
| `action` | query | string | Action kind |
| `phase` | query | string |  |
| `actor` | query | string | Actor id |
| `risk` | query | string | Risk level |
| `since` | query | string |  |
| `until` | query | string |  |
| `limit` | query | integer |  |

### Responses

- `200` Echoed query, count, result rows (entry\_id, event\_family\_id, emitted\_at, phase, actor, verdict, audit\_chain\_ref, framework\_refs, tags), served\_at
- `401` Missing or invalid bearer token
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Runtime

GET `/live-runtime` Recent PDP decisions with a 5-minute KPI roll-up

Newest-first decision events for the caller's tenant with a KPI block covering the last 5 minutes (decisions/min rate plus allow / review / deny percentages) and distinct capability and actor lists for filter dropdowns. Updates the tenant's last-polled cursor. Read-only — decisions are emitted by the PDP, never created here.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `limit` | query | integer |  |
| `since` | query | string | Only decisions decided at or after this instant |
| `capability` | query | string | Filter by capability\_id |
| `actor` | query | string | Matches actor\_entity\_id or agent\_entity\_id |
| `decision` | query | string | Filter by decision verdict |

### Responses

- `200` Decision rows, kpi (rate, allow\_pct, review\_pct, deny\_pct), capabilities, actors
- `400` invalid\_since — unparseable timestamp
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/runtime/evaluate` Evaluate an agent action and return an Authority Finality decision + receipt

Runs the deterministic runtime authority decision for one proposed agent action and returns the three-outcome verdict (allow | require\_approval | deny), a replay-stable decision\_id, a patent-safe evidence reference, a receipt verify\_url, and an Ed25519-sealed, offline-verifiable Evidence Pack. Emits the full evidence-event family server-side. Auth is a minted API key (runtime:write or runtime:\* scope) or a Clerk session.

**Auth:** API key (kye\_live\_ / kye\_test\_) / Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `subject` | string | KYE™ URN of the acting agent/principal (e.g. kye:agent:acme:kyc-triage). Alias: agent. |
| `agent` | string | Alias for subject (quickstart-friendly). |
| `action` **required** | string | Dotted capability id (e.g. payments.transfer, kyc.screening.run). |
| `purpose` **required** | string | Declared purpose class the action is bound to. |
| `context` | object | Optional decision context (amount, jurisdiction, approval\_recorded, irreversible, …). |

### Responses

- `200` Governed decision: decision (allow | require\_approval | deny), reason\_code (canonical vocabulary), decision\_id (kye:decision:<hex>, replay-stable), evidence (patent-safe audit\_reference), verify\_url (receipt deep-link on evidence.html), replay\_seed, latency\_m…
- `400` invalid\_json | missing\_subject | subject\_not\_kye\_urn | missing\_action | action\_malformed | missing\_purpose
- `401` Missing or invalid bearer token
- `403` permission\_scope\_exceeded — key lacks runtime:write / runtime:\*
- `429` quota\_exceeded — the tenant's monthly agent\_action quota is exhausted (governed refusal with a patent-safe evidence reference and quota {used, limit, remaining, resets\_at}; configure via KYE\_AGENT\_ACTION\_MONTHLY\_QUOTA, 0 = unlimited).

## Memory

GET `/memory` List signed agent-memory records for the caller's tenant

Reads the agent\_memory table, excluding forgotten records. Exposes the observable contract only — id, agent\_entity\_id, memory\_class, purpose, created\_at, signed\_by\_kid — plus total, distinct-agent and distinct-class counts.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Memory rows (newest 200) with total, agents\_count, classes\_count
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Onboarding

GET `/onboarding/workflows` Read-only onboarding workflow projection

The workflow is a projection of the six per-step rows in the onboarding\_steps table — there is no separate workflow store (§0 one source of truth). Returns a single-element array when the tenant has started onboarding, otherwise an empty array.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Array of zero or one kye.onboarding.workflow.v1 objects
- `401` Missing or invalid bearer token
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Purposes

GET `/purposes` List the tenant's declared purposes with KPI roll-up

A purpose is the bounded reason any agent, connector or partner may act — every Purpose Permission grant cites one. Grants themselves live under /purpose-permissions; this registry holds the definitions.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Purpose rows (purpose\_id, class, display\_name, lawful\_basis, active\_grants, status) plus kpi
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/purposes` Declare a new purpose

Mints a kye:purpose URN for the tenant. The class must be a lowercase slug and the lawful basis one of the six GDPR Article 6 bases.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `class` **required** | string |  |
| `display_name` **required** | string |  |
| `lawful_basis` **required** | string | One of consent, contract, legal\_obligation, vital\_interests, public\_task, legitimate\_interests |

### Responses

- `201` Created purpose with its minted purpose\_id and active status
- `400` invalid\_json, invalid\_class, display\_name\_required or invalid\_lawful\_basis
- `503` Required D1 / service binding not attached (db\_binding\_missing)

GET `/purpose-least-privilege` Declared-vs-exercised purpose drift for the caller's tenant

Answers the least-privilege question no single existing surface answers: of the purposes this tenant declared and granted, how many were ever actually exercised? Joins the purpose registry and the grant ledger to the per-decision purpose attribution on the evidence-pack index, and classifies every declared purpose as exercised, unexercised, or not\_observable. Purpose attribution only exists on decisions sealed after the decision writer began recording it, so the response reports attributed and unattributed decision counts separately and a purpose whose whole life predates the attributed window is reported as not\_observable — never as unused, which would be a fabricated finding. Tenant scoping is applied in SQL on every statement.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `window_days` | query | integer | Observation window in days; out-of-range values are clamped, non-numeric falls back to the default |

### Responses

- `200` Per-purpose findings (exercised / unexercised / not\_observable), finding counts, the honest attributed and unattributed decision denominators, the observation window, unexercised live grants, and a compliance attestation
- `401` No session, or the session resolves to no tenant
- `403` A client-supplied tenant\_id did not match the session-resolved tenant (§0.11 cross-tenant refusal)
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## DataGovernance

GET `/data-flow-graph` List signed data-flow seals for the caller's tenant

Exposes the observable contract of the Data Mapping Agent — seal\_id, asset\_count, flow\_count, pii\_assets, sealed\_at, signed\_by\_kid — newest first (up to 200), with the latest seal's totals lifted to top-level asset\_total / flow\_total / pii\_assets.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Seal rows plus latest-seal totals
- `503` Required D1 / service binding not attached (db\_binding\_missing)

GET `/classification` List the tenant's data-asset classifications with KPI roll-up

Every classification is tenant-scoped and Ed25519-signed (the signing kid is recorded). KPI counts special-category and restricted assets plus rows still pending a signature.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Classification rows (classification\_id, asset\_id, classification, detection, confidence, signature\_kid, effective\_at) plus kpi
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/classification` Register a new asset classification

Records the classification exactly as submitted (effective immediately); signature\_kid is stored as supplied — empty until the record is signed.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `asset_id` **required** | string |  |
| `classification` **required** | string | One of public, internal, confidential, restricted, top\_secret, special\_category |
| `detection` | string | One of human\_review, regex\_scan, llm\_inference, gdpr\_art9\_match, sector\_template, schema\_inference |
| `confidence` | number |  |
| `signature_kid` | string |  |

### Responses

- `201` Created classification with its minted classification\_id
- `400` invalid\_json, asset\_id\_required, invalid\_classification, invalid\_detection or confidence\_must\_be\_0\_to\_1
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Evidence

GET `/publisher-access-ledger` List the tenant's Publisher Access Ledger snapshots with KPI roll-up

Backs the KYE™ Publisher Access Ledger™ dashboard — the tenant's licensed-vs-breach split, the advisory GBP 500-per-Product accrual per operator, and the possible-spoof surfacing. Every snapshot is tenant-scoped (newest first, up to 200) and the KPI totals sum snapshots / breach\_events / possible\_spoof\_events / accrued\_total. The schedule is ADVISORY (§70): the publisher-claimable accrual from attribution signals, never a KYE-metered charge; unknown/unattributed access is never invoiced and possible-spoof is surfaced, not invoiced as certain.

**Auth:** Session (Clerk JWT)

### Responses

- `200` ok, contract\_url, honesty{advisory,basis}, kpi roll-up and snapshots\[\] (each with summary + operators + effective\_at)
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/publisher-access-ledger` Derive and record a Publisher Access Ledger snapshot from a classified-access report

Accepts a kye.crawler\_classification.v1 report (records\[\]) from the collector (Tier A edge or Tier C log ingest), derives the advisory breach schedule via the ONE canonical breach-schedule engine (never re-implemented, §0), and records the tenant-scoped snapshot. The customer-visible response carries a patent-safe evidence reference built only by the canonical publicEvidence() helper (§0.35).

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `report` **required** | object | A kye.crawler\_classification.v1 instance |
| `access_fee_per_product` | integer | Per-website Clause 14 override of the advisory GBP 500-per-Product accrual |
| `currency` | string |  |

### Responses

- `201` ok, snapshot{snapshot\_id, summary, operators, honesty, effective\_at} and a patent-safe evidence reference
- `400` invalid\_json, report\_records\_required or invalid\_report
- `503` Required D1 / service binding not attached (db\_binding\_missing)

GET `/evidence-import` List the tenant's recent evidence imports

Tenant-visible history of GRC-export imports — source tool, target kind, format and row / mapped / error counts for the latest 100 batches.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Import history rows
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/evidence-import` Map a GRC-tool export into a canonical evidence batch

Forwards the upload to the single canonical mapping engine (kye-evidence-import-worker) over a service binding with the worker bearer, then records the import for tenant-visible history. Fails closed — with no service binding or bearer configured the request is refused with 503 rather than silently passing content through unmapped. Content is capped at 5 MiB of decoded text.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `source_tool` **required** | string |  |
| `format` **required** | string | One of csv, json |
| `target_kind` | string | One of control, obligation, evidence\_item, ai\_system |
| `content` **required** | string | Raw export text (max 5 MiB) |
| `column_map` | object | Optional source-column to canonical-field overrides |
| `defaults` | object | Optional default field values applied to every mapped row |

### Responses

- `200` Import summary (rows, mapped, errors) plus the mapped kye.connector.evidence\_import.v1 batch
- `400` invalid\_json, source\_tool\_required, invalid\_format, invalid\_target\_kind or content\_required
- `413` content\_too\_large — over the 5 MiB cap
- `502` mapping\_failed or import\_service\_unreachable — the mapping engine errored
- `503` Required D1 / service binding not attached (db\_binding\_missing)

GET `/evidence-coverage` Decision-to-Evidence-Pack coverage for the caller's tenant

Joins the tenant's decision ledger to its sealed-pack index on decision\_id and reports coverage as covered / decisions, plus the full list of decisions that resolve to no pack. Breakdowns are returned per verdict, per capability and per day. `dimensions` reports how many rows actually carry each decision axis, so an axis the ledger does not record reads as unrecorded rather than as an empty finding. Tenant scoping is applied in SQL on both sides of the join.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `since` | query | string | Window start (omit for the whole ledger) |
| `until` | query | string | Window end (omit for the whole ledger) |
| `limit` | query | integer | Maximum uncovered decisions returned |

### Responses

- `200` Coverage totals, per-verdict / per-capability / per-day breakdowns, axis-recording health, and the uncovered-decision list
- `400` Invalid since/until, or since not before until
- `503` Required D1 / service binding not attached (db\_binding\_missing)

GET `/evidence-packs` List evidence packs for the caller's tenant

Evidence Packs are signed audit-ready envelopes for each governed action window. This is the console-level registry view; sealing and signing mechanics live in the runtime.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Pack rows (window, actions, sealed\_at, status, sha256, compilation\_seal, size\_bytes, signers)
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/evidence-packs` Create a new draft evidence pack

Opens a draft pack for the chosen window (default daily) with zero actions and no signers.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `window` | string | One of daily, weekly, quarterly |

### Responses

- `201` Created draft pack
- `400` invalid\_json
- `503` Required D1 / service binding not attached (db\_binding\_missing)

GET `/evidence-packs/{id}` Fetch a single evidence pack

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` The pack row with parsed signers array
- `404` Resource not found
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/evidence-packs/{id}` Attest a pack (append signer)

Appends the caller as a signer; the pack advances from awaiting\_signoff to attested once three signers have signed. Each caller may sign only once.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Request body

| field | type | description |
| --- | --- | --- |
| `action` | string | One of attest |

### Responses

- `200` Updated signers list and status
- `400` unsupported\_action
- `404` Resource not found
- `409` already\_signed\_by\_caller
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Approvals

GET `/action-approvals` List the tenant's action proposals awaiting or decided review

Backs the GovernedUI approval queue (envelope kye.governedui.action\_proposal.v1). Optional risk-level and approval-mode filters; KPI counts pending / approved / rejected / escalated proposals.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `risk_level` | query | string |  |
| `approval_mode` | query | string |  |
| `limit` | query | integer |  |

### Responses

- `200` Proposal rows (proposal\_id, actor\_id, action\_type, target\_system, risk\_level, approval\_mode, state, proposed\_at, decided\_at, decided\_by) plus kpi
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Actions

GET `/actions` PDP-mediated action history for the caller's tenant

Console-level slice of the shared decisions ledger — each row maps a decision to its action\_id, actor, capability, verdict, reason\_code and decided\_at, newest first.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `decision` | query | string | Filter by decision verdict; "all" or absent returns every verdict |
| `limit` | query | integer |  |

### Responses

- `200` Action rows derived from the decisions ledger
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Agents

GET `/agents` List agents with activity roll-up

Merges the explicit agents registry with per-agent activity derived from the decisions ledger (decision\_count, last\_seen\_at, allow / review / deny counts). Registry rows are authoritative for metadata; agents observed only in decisions appear with null metadata. Sorted by last activity.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Merged agent list with registry metadata and activity counts
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/agents` Register a new agent

Mints a kye:agent URN. New agents start in the pilot lifecycle state with zero activity — no fabricated counts.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `label` **required** | string |  |
| `kind` | string | One of agent, service, human, model |
| `capability_id` | string |  |

### Responses

- `201` Registered agent in lifecycle\_state pilot
- `400` invalid\_json or missing\_label
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## AICalls

GET `/ai-calls` Per-call ledger of model invocations

Cost, latency, token counts, purpose binding and evidence link for every model invocation; reconciles to provider invoices. Includes tenant-level KPI aggregates and distinct purpose / model lists for filter dropdowns.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `date` | query | string | Restrict to a single UTC day (YYYY-MM-DD) |
| `purpose` | query | string |  |
| `model` | query | string |  |
| `min_cost` | query | number | Only calls with cost at or above this value |
| `limit` | query | integer |  |

### Responses

- `200` Call rows plus kpis (calls, cost\_total, tokens, avg\_latency) and distinct purposes/models
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Webhooks

GET `/webhooks` List the caller's tenant's webhook endpoints

Returns endpoint metadata only. The signing secret is stored (KYE™ must sign each outbound delivery with it) but is NEVER returned here — only a short non-usable hint. Includes a 30-day delivery summary per endpoint.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Endpoint list for the caller's tenant
- `401` Missing or invalid bearer token
- `503` Database binding unavailable

POST `/webhooks` Register a webhook endpoint (signing secret returned once)

Registers an HTTPS endpoint for governance-event delivery. The signing secret is generated server-side and returned EXACTLY once — no endpoint will hand it back. Plain http is refused rather than downgraded. One active subscription per endpoint URL per tenant.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `endpoint_url` **required** | string | Absolute HTTPS URL |
| `signal_types` | array | Defaults to every canonical signal type when omitted |

### Responses

- `201` Endpoint registered; signing secret returned once
- `400` Invalid JSON, non-HTTPS or malformed URL, or unknown signal type
- `401` Missing or invalid bearer token
- `409` An active subscription already exists for this endpoint URL
- `503` Database binding unavailable

GET `/webhooks/{id}` Read one webhook endpoint

Tenant-scoped read. An id belonging to another tenant resolves to 404, never to another tenant's record (§0.11).

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` Endpoint detail
- `401` Missing or invalid bearer token
- `404` Unknown endpoint for this tenant

DELETE `/webhooks/{id}` Disable a webhook endpoint

Disables rather than row-deletes: `webhook_deliveries` rows reference `subscriber_id`, so a hard delete would orphan the delivery history the §30 audit trail depends on. Deliveries stop immediately. Idempotent — disabling an already-disabled endpoint returns 200 with idempotent true.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` Endpoint disabled (or already disabled)
- `401` Missing or invalid bearer token
- `404` Unknown endpoint for this tenant

## ApiKeys

GET `/api-keys` List API keys for the caller's tenant

Returns key metadata only — id, label, display prefix, scope, env, created\_by/at, last\_used\_at, revoked\_at. The secret is never stored; only its SHA-256 hash is persisted.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Key metadata rows (no secrets)
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/api-keys` Create a new API key (secret returned once)

Mints a kye\_<live|test>\_ key with 192 bits of entropy. The plaintext secret is returned exactly once in this response; only its SHA-256 hash and 12-character display prefix are persisted. Unrecognised scopes are dropped; an empty scope defaults to runtime:read.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `label` **required** | string |  |
| `scope` | string | Comma-separated scopes from the canonical kye:dictionary:api-key-scopes: runtime:read, runtime:write, runtime:\*, evidence:read, evidence:write, attestation:write, directory:read, directory:write, admin:\* |
| `env` | string | One of live, test |
| `ttl_days` | integer | Optional TTL in days. When set, the key carries expires\_at = created\_at + ttl\_days and stops verifying after it. Omit for a long-lived key. |
| `ip_allowlist` | array | Optional CIDR/IP allowlist. When set, the verifier rejects a presented key from any source IP outside the list. |

### Responses

- `201` Key metadata (incl. expires\_at) plus the one-time plaintext secret, an Authority Finality receipt (patent-safe evidence), and a copy-now warning
- `400` invalid\_json, missing\_label or invalid\_ttl
- `503` Required D1 / service binding not attached (db\_binding\_missing)

DELETE `/api-keys/{id}` Revoke an API key (governed hard-kill)

Sets status=revoked + revoked\_at on the tenant's key and emits kye.admin.api\_key.revoked.v1 to the WORM chain. Idempotent revocations of an already-revoked or unknown key return 404.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` Revoked — returns id, revoked\_at and an Authority Finality receipt
- `404` not\_found\_or\_already\_revoked
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/api-keys/{id}/rotate` Rotate an API key (mint successor + grace-window the predecessor)

Mints a successor key inheriting the predecessor's scope/env, marks the predecessor status=rotated with a bounded grace window (default 3600s, during which it still verifies before it expires), and emits kye.admin.api\_key.rotated.v1 to the WORM chain. The plaintext successor secret is returned exactly once. The executable form of continuously re-earned authority (§0.33).

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Request body

| field | type | description |
| --- | --- | --- |
| `grace_seconds` | integer |  |
| `reason` | string |  |

### Responses

- `201` Successor key metadata + one-time secret + Authority Finality receipt
- `404` not\_found\_or\_not\_active
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/api-keys/agent/issue` Agent-callable M2M issuance of a short-lived heartbeat-bound key

The doctrine-core path (§0.30 agent principal, §0.33 Authority Finality™): an agent obtains a SHORT-LIVED, HEARTBEAT-BOUND bearer key whose authority must be continuously re-earned. The key carries a near-term expiry and a heartbeat binding; missing the check-in loses the mandate (#160). Governed identically to human issuance (§0.3 evidence family + metering). Kill- switchable via KYE\_KEY\_AUTHORITY\_AGENT\_ISSUE\_DISABLED.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `principal_id` **required** | string | The agent principal (kye:agent:\* / kye:principal:\*) the key is bound to. |
| `label` | string |  |
| `scope` | string | Comma-separated canonical scopes (defaults runtime:read). |
| `ttl_seconds` | integer | Short TTL. Capped low — this is not a long-lived credential. |
| `heartbeat_interval_seconds` | integer |  |

### Responses

- `201` Short-lived key metadata (incl. expires\_at + heartbeat) + one-time secret + Authority Finality receipt
- `400` invalid\_json or missing\_principal\_id
- `403` agent\_issuance\_disabled (kill-switch)
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/api-keys/agent/{id}/renew` Agent-callable heartbeat renewal (re-earn the mandate)

Re-earns a heartbeat-bound key's authority: a renewal MUST pass a fresh admissibility check (§12 PDP) within the heartbeat window, extending expires\_at by one interval and stamping last\_heartbeat\_at. A renewal outside the window is denied — the mandate is already lost and the key must be re-issued. This is authority as a STATE you continuously re-earn, not a grant you keep. Governed (§0.3) + metered (§23).

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` Renewed — new expires\_at + Authority Finality receipt
- `403` heartbeat\_window\_missed or not\_a\_heartbeat\_key
- `404` not\_found\_or\_not\_active
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Apps

GET `/apps` List the tenant's installed apps with KPI roll-up

Apps are tenant-installed AI agents, copilots, runbooks and partner-supplied integrations operating under the tenant's Operating Model. KPI counts active (calls in last 24h), shadow-mode and suspended apps.

**Auth:** Session (Clerk JWT)

### Responses

- `200` App rows (app\_id, category, display\_name, mode, status, calls\_24h) plus kpi
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/apps` Install an app

Mints a kye:app URN. A freshly installed app starts active with zero call counts; shadow mode logs decisions without enforcing them.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `category` **required** | string | One of ai\_agent, copilot, runbook, integration, connector\_app |
| `display_name` **required** | string |  |
| `mode` | string | One of shadow, advisory, guarded, strict |

### Responses

- `201` Installed app with its minted app\_id
- `400` invalid\_json, invalid\_category, display\_name\_required or invalid\_mode
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## AssuranceCards

GET `/assurance-cards` List the tenant's assurance cards with KPI roll-up

Assurance cards are time-bounded signed attestations mapping real evidence to regulatory framework controls, on a 90-day rotation per §0.3. KPI counts active, expiring (within 30 days), expired cards and distinct frameworks covered.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Card rows (card\_id, framework, controls, scope, status, attested\_at, expires\_at, verifier\_url) plus kpi
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/assurance-cards` Generate a new assurance card

Mints a kye:assurance-card URN with honest timestamps — attested now, expiring 90 days later — and a public verifier URL. Controls must be a non-empty array of control identifiers.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `framework` **required** | string | One of SOC2, ISO27001, ISO42001, EU\_AI\_ACT, DORA, FCA\_OPRES, NIST\_AI\_RMF, OSCAL |
| `controls` **required** | array |  |
| `scope` **required** | string |  |

### Responses

- `201` Created card with verifier\_url and a 90-day expires\_at
- `400` invalid\_json, invalid\_framework, scope\_required or controls\_required\_array
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## AuditEvents

GET `/audit-events` Query the tenant's append-only hash-chained audit trail

Each row stores prev\_hash and a SHA-256 hash over the canonical JSON of (seq, event, actor, summary, at, prev\_hash) so the chain is client-verifiable by recomputing from seq=1. Rows are returned newest first.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `family` | query | string | Event-name prefix filter |
| `from` | query | string |  |
| `to` | query | string |  |
| `q` | query | string | Substring match across actor, summary and id |
| `limit` | query | integer |  |

### Responses

- `200` Event rows with seq, event, actor, summary, at, prev\_hash, hash
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/audit-events` Append an event to the tenant's audit chain

Appends the next sequence entry, linking prev\_hash to the prior row (or a 64-zero genesis) and hashing the canonical JSON. Normally driven by the runtime; exposed so admins can record compliance markers such as a manually attested evidence pack.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `event` **required** | string |  |
| `actor` | string | Defaults to the caller's email or user id |
| `summary` | string |  |

### Responses

- `201` Appended event with its seq, prev\_hash and hash
- `400` invalid\_json or missing\_event
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## BehaviourModel

GET `/behaviour-model` Load the tenant's latest behaviour-model revision

Returns the highest revision of the tenant's behaviour model (allowed actions, obligations, stop-conditions, escalation paths). A tenant with no model yet gets revision 0 with empty rows.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Latest revision with rows, signed flag, updated\_at, updated\_by
- `503` Required D1 / service binding not attached (db\_binding\_missing)

PUT `/behaviour-model` Replace the tenant's behaviour model (new immutable revision)

Inserts a new unsigned revision; prior revisions are kept immutable for sign-off and replay. Rows are shape-validated and capped at 500; string fields are length-clamped.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `rows` **required** | array |  |

### Responses

- `200` New revision number, accepted row count and updated\_at
- `400` invalid\_json or rows\_array\_required
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Billing

GET `/billing` All-in-one billing view (subscription + invoices + seats)

Reads the canonical kye\_grants store (binding KYE\_GRANTS\_DB, populated by the commercial-lifecycle worker's Stripe projection): latest subscription, up to 50 invoices with hosted Stripe URLs, and attestation seats. Returns 503 db\_binding\_missing until the KYE\_GRANTS\_DB binding is attached to the Pages project.

**Auth:** Session (Clerk JWT)

### Responses

- `200` subscription (or null), invoices, seats
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/billing` Run the BPS fee simulator

Pure function per §23 §4 — 3 basis points on the transaction value with a £5 floor and £50 cap. The only supported action is bps\_simulate.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `action` **required** | string | One of bps\_simulate |
| `value_pence` | integer | Transaction value in pence |
| `currency` | string |  |
| `action_class` | string |  |

### Responses

- `200` Fee breakdown: rate\_bps, computed\_fee\_pence, final\_fee\_pence, was\_floored, was\_capped
- `400` invalid\_json or unsupported\_action
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Connectors

GET `/connectors` List the tenant's installed connectors with KPI roll-up

Connectors bring outside-system events into the tenant's KYE™ evidence stream; the canonical kind vocabulary is kye:dictionary:connectors. KPI counts healthy, degraded and disconnected connectors.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Connector rows (connector\_id, kind, profile\_family, display\_name, status, last\_harvest\_at, events\_24h) plus kpi
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/connectors` Install a connector

Mints a kye:connector URN. A freshly installed connector is disconnected until its first authenticated harvest — honest initial state, no fabricated health.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `kind` **required** | string |  |
| `display_name` **required** | string |  |
| `profile_family` **required** | string | One of payments, open\_finance, identity, agent\_runtime, commerce, compliance\_evidence, security\_siem, health, insurance, pension, utilities, legal, open\_data |

### Responses

- `201` Installed connector in disconnected status
- `400` invalid\_json, invalid\_kind, display\_name\_required or invalid\_profile\_family
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Delegations

GET `/delegations` List active and revoked delegations

Authority flows actor to principal to subject under a scope with an expiry; attenuations are separate rows referencing a parent\_id. State is derived lazily — revoked when revoked\_at is set, expired when past expires\_at, otherwise active.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Delegation rows with derived state (active / expired / revoked)
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/delegations` Create a new delegation

Issues a delegation with a TTL (default 365 days, max 730). When attenuating via parent\_id, the parent must exist and belong to the caller's tenant.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `actor` **required** | string |  |
| `principal` **required** | string |  |
| `subject` **required** | string |  |
| `scope` **required** | string |  |
| `parent_id` | string | Parent delegation to attenuate |
| `ttl_days` | integer |  |

### Responses

- `201` Created delegation with expires\_at
- `400` invalid\_json or missing\_fields
- `404` parent\_not\_found — attenuation parent missing or not owned by this tenant
- `503` Required D1 / service binding not attached (db\_binding\_missing)

DELETE `/delegations/{id}` Revoke a delegation (soft-delete)

Sets revoked\_at / revoked\_by and flips state to revoked; an optional reason is recorded.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |
| `reason` | query | string | Optional revocation reason |

### Responses

- `200` Revoked — returns id and revoked\_at
- `404` not\_found\_or\_already\_revoked
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## DriftEvents

GET `/drift-events` List drift events with open/closed counts

The runtime opens drift events automatically; humans close them. Optional state filter; counts power the tab badges.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `state` | query | string | Absent returns both |

### Responses

- `200` Event rows (type, actor, severity, opened, closed, resolution) plus open/closed counts
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/drift-events` Open a new drift event

Records a meaning-continuity drift marker. Unrecognised types default to meaning\_broken and unrecognised severities to P2.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `type` | string | One of meaning\_broken, obligation\_unmet, scope\_overflow |
| `severity` | string | One of P0, P1, P2 |
| `actor` | string |  |
| `detail` | string |  |

### Responses

- `201` Opened event with its id and opened timestamp
- `400` invalid\_json
- `503` Required D1 / service binding not attached (db\_binding\_missing)

PATCH `/drift-events/{id}` Close (resolve) a drift event

Marks the event closed with a resolution; unrecognised or absent resolutions default to remediated. The resolver identity is recorded.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Request body

| field | type | description |
| --- | --- | --- |
| `resolution` | string | One of reconfirmed, revoked, remediated |

### Responses

- `200` Closed — returns id, closed timestamp, resolution, resolved\_by
- `404` not\_found\_or\_already\_closed
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Decisions

GET `/refusal-map` Refusal clusters for the caller's tenant

Returns only the non-allow decisions (deny, reject, revoke, quarantine, require\_approval, require\_human\_review) for the caller's tenant, clustered by reason\_code x capability x actor\_entity\_id and ranked on each axis separately. Each cluster reports how many of its refusals resolve to an Evidence Pack. `dimensions` reports the real recording coverage of each axis over the refused set — reason\_code is null on historical rows and populates from newer decisions forward, and a null is never inferred or defaulted. Tenant scoping is applied in SQL.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `since` | query | string | Window start (omit for the whole ledger) |
| `until` | query | string | Window end (omit for the whole ledger) |
| `limit` | query | integer | Maximum clusters returned |

### Responses

- `200` Refusal totals, per-axis rankings, axis-recording health, and the reason\_code x capability x actor clusters
- `400` Invalid since/until, or since not before until
- `503` Required D1 / service binding not attached (db\_binding\_missing)

GET `/decisions` Decision records for the calling tenant

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `since` | query | string | Lower bound on decision time |
| `decision` | query | string | Filter by outcome |
| `actor` | query | string |  |
| `limit` | query | integer |  |

### Responses

- `200` Matching decision records
- `400` \`invalid\_since\`
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Listings

GET `/my-listings` List the tenant's directory listings with KPI roll-up

Listings are the tenant's public-facing entries in the KYE™ Directory — rule packs, agents, connectors, assurance cards — each requiring moderation before becoming visible. KPI counts published, in-review and draft listings plus removals in the last 90 days.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Listing rows (listing\_id, listing\_type, title, status, views\_30d) plus kpi
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/my-listings` Create a new directory listing (starts as draft)

Mints a kye:listing URN. A new listing starts as draft — it must be submitted and pass moderation before becoming visible; view counts start at zero.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `listing_type` **required** | string | One of rule\_pack, agent, connector, assurance\_card |
| `title` **required** | string |  |

### Responses

- `201` Created draft listing
- `400` invalid\_json, invalid\_listing\_type, title\_required or title\_too\_long
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Partners

GET `/partners` List partners under tenant authority

**Auth:** Session (Clerk JWT)

### Responses

- `200` Partner rows (name, class, assurance tier, status, since, onboarded\_by)
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/partners` Onboard a new partner

Registers a third-party partner under the tenant's authority. Unrecognised assurance values default to Tier 3 and unrecognised statuses to active.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `name` **required** | string |  |
| `class` **required** | string |  |
| `assurance` | string | One of Tier 1, Tier 2, Tier 3 |
| `status` | string | One of active, suspended, off-boarded |

### Responses

- `201` Onboarded partner with its minted id
- `400` invalid\_json or missing\_fields
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Plugins

GET `/plugins` List the tenant's installed plugins with KPI roll-up

Plugins are tenant-side extensions of the protocol — PDP rule packs, conformance probes, custom obligations, SDK shims. KPI counts enabled, failing and update-available plugins.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Plugin rows (plugin\_id, kind, display\_name, version, state, health, invocations\_24h, update\_available) plus kpi
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/plugins` Install a plugin

Mints a kye:plugin URN. A freshly installed plugin is disabled until the tenant enables it in the decision flow — honest initial state. Version must be semver-shaped.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `kind` **required** | string | One of rule\_pack, conformance\_probe, obligation, sdk\_shim |
| `display_name` **required** | string |  |
| `version` | string |  |

### Responses

- `201` Installed plugin in disabled state
- `400` invalid\_json, invalid\_kind, display\_name\_required or invalid\_version
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## PurposePermissions

GET `/purpose-permissions` List Purpose Permission grants for the caller's tenant

Every action under a KYE-governed agent must cite an issued Purpose Permission. Status is derived per row — revoked, pending\_reconfirm (past expiry), expiring (within 30 days) or active.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Grant rows (grantee, purpose, scope, expires, derived status, issued/revoked/reconfirmed audit fields)
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/purpose-permissions` Issue a new Purpose Permission

Issues an active grant with a TTL (default 90 days, max 365). The issuer identity is recorded from the session.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `grantee` **required** | string |  |
| `purpose` **required** | string |  |
| `scope` **required** | string |  |
| `ttl_days` | integer |  |

### Responses

- `201` Issued grant with expires timestamp
- `400` invalid\_json or missing\_fields
- `503` Required D1 / service binding not attached (db\_binding\_missing)

PATCH `/purpose-permissions/{id}` Reconfirm a Purpose Permission (extend expiry)

Extends the grant by ttl\_days (default 90, max 365) from now, records last\_reconfirmed\_at and resets status to active. Revoked grants cannot be reconfirmed.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Request body

| field | type | description |
| --- | --- | --- |
| `ttl_days` | integer |  |

### Responses

- `200` New expires, last\_reconfirmed\_at and reconfirmed\_by
- `404` not\_found\_or\_revoked
- `503` Required D1 / service binding not attached (db\_binding\_missing)

DELETE `/purpose-permissions/{id}` Revoke a Purpose Permission (soft-delete)

Sets revoked\_at / revoked\_by and flips status to revoked; an optional reason is recorded.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |
| `reason` | query | string | Optional revocation reason |

### Responses

- `200` Revoked — returns id and revoked\_at
- `404` not\_found\_or\_already\_revoked
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Replay

GET `/replay` List the tenant's replay runs with KPI roll-up

Replay runs reference an evidence\_pack\_id and are signed via kye.replay.proof.v1; divergence opens a Resilience Loop ticket automatically. KPI reports replays in the last 24h, overall match rate, divergences in the last 30 days and signature failures.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Run rows (replay\_id, evidence\_pack\_id, decision\_id, verdict, signature\_ok, requested/completed timestamps) plus kpi
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/replay` Queue a new replay of an evidence pack

Queues a run in the pending verdict — only the Replay Engine may resolve it to verified, diverged or signature\_failed. Signature status is unknown (false) until the engine runs.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `evidence_pack_id` **required** | string |  |
| `decision_id` | string | Optional single decision to replay |

### Responses

- `201` Queued run in pending verdict
- `400` invalid\_json or evidence\_pack\_id\_required
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Risk

GET `/risk` List the tenant's risk assessments with KPI roll-up

Assessments are tenant-scoped, audit-chained and framework-floor-governed. KPI counts prohibited verdicts in the last 24h plus high, limited and minimal tiers across the latest 500 assessments.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Assessment rows (assessment\_id, subject\_id, subject\_class, tier, score, reason\_codes, framework\_floor, effective\_at) plus kpi
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/risk` Register a new risk assessment for a subject

Records the assessment exactly as submitted — no fabricated scores or tier upgrades. Score is an integer 0-100.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `subject_id` **required** | string |  |
| `subject_class` **required** | string | One of decision, agent, capability, scenario, operating\_model, deal, rule\_pack, connector |
| `tier` | string | One of minimal, limited, high, unacceptable, prohibited |
| `score` | integer |  |
| `reason_codes` | array |  |
| `framework_floor` | string | One of eu\_ai\_act, dora, gdpr, nist\_ai\_rmf, iso\_42001, fca\_opres, pci\_dss, sox, none |

### Responses

- `201` Recorded assessment with its minted assessment\_id
- `400` invalid\_json, subject\_id\_required, invalid\_subject\_class, invalid\_tier, score\_must\_be\_0\_to\_100 or invalid\_framework\_floor
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Scopes

GET `/scopes` List the tenant's declared scopes with KPI roll-up

Scopes are capability + dataset + jurisdiction triples bounding what an agent may do within a granted purpose. KPI counts active scopes, scopes bound to at least one purpose, total datasets and distinct jurisdictions.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Scope rows (scope\_id, capability, datasets, jurisdiction, bound\_purposes, status) plus kpi
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/scopes` Declare a new scope

Mints a kye:scope URN. A freshly declared scope starts active with zero purpose bindings — bindings are created through the Purpose Permission rail. Datasets accept an array or comma-separated string.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `capability` **required** | string | One of read, write, delete, verify, attest, audit, delegate |
| `jurisdiction` | string | One of GB, EU, US, SG, AU |
| `datasets` | object |  |

### Responses

- `201` Declared scope with zero purpose bindings
- `400` invalid\_json, invalid\_capability or invalid\_jurisdiction
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## StateMachines

GET `/state-machine-assignment` List the tenant's state machine assignments with KPI roll-up

An assignment binds a tenant state machine to a specific entity and tracks its current state. KPI counts assignments, distinct bound entities, transitions in the last 24h and assignments with no transition in 30 days (stuck); includes distinct entity classes for the filter bar.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Assignment rows (assignment\_id, state\_machine\_id, entity\_id, entity\_class, current\_state, state\_since, last\_transition\_at, transitions\_24h) plus kpi and entity\_classes
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/state-machine-assignment` Assign a state machine to an entity

Verifies the referenced state machine exists and belongs to the caller's tenant, then creates the assignment in the given initial state (default pending) with zero transitions.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `state_machine_id` **required** | string |  |
| `entity_id` **required** | string |  |
| `entity_class` **required** | string | Lowercased; non-alphanumerics become underscores |
| `initial_state` | string |  |

### Responses

- `201` Created assignment in its initial state
- `400` invalid\_json, state\_machine\_id\_required, entity\_id\_required or entity\_class\_required
- `404` state\_machine\_not\_found\_or\_not\_owned
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Stripe

POST `/stripe/checkout-session` Create a Stripe Checkout Session for the tenant's subscription

Creates a subscription-mode Checkout Session via the Stripe REST API, stamping the tenant id into client\_reference\_id and metadata, and persists a checkout intent so the webhook can correlate checkout.session.completed back to the tenant. The client redirects to the returned url.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `price_id` **required** | string | Stripe Price id |
| `success_path` | string | Return path on success (default /billing.html?checkout=ok) |
| `cancel_path` | string | Return path on cancel (default /billing.html?checkout=cancel) |

### Responses

- `200` session\_id, hosted checkout url and expires\_at
- `400` invalid\_json or bad\_price\_id
- `502` stripe\_error — Stripe API rejected the request
- `503` Required D1 / service binding not attached (db\_binding\_missing)

GET `/stripe/invoices` List invoices for the caller's tenant

Reads the canonical invoices table in the kye\_grants store (binding KYE\_GRANTS\_DB), populated by the commercial-lifecycle worker's Stripe projection. Hosted Stripe invoice URLs are passed straight through.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Invoice rows with totals, status, hosted URL and billing period (latest 100)
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/stripe/portal-session` Create a Stripe Customer Portal session

Looks up the tenant's stripe\_customer\_id from its most recent non-cancelled subscription in the kye\_grants store, then creates a Billing Portal session returning to /billing.html.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Portal url for the caller's Stripe customer
- `404` no\_active\_subscription — create one via /stripe/checkout-session first
- `502` stripe\_error — Stripe API rejected the request
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## Usage

GET `/usage` Usage and billing aggregates for the caller's tenant

Month-to-date decision count, evidence packs created, AI cost and token totals, a 4-week weekly decision sparkline, and a 30-day meter summary grouped by meter class. Honest zeros when tables are empty.

**Auth:** Session (Clerk JWT)

### Responses

- `200` decisions\_month, evidence\_packs\_month, ai\_cost\_month, ai\_tokens\_month, weekly\_decisions, meters
- `503` Required D1 / service binding not attached (db\_binding\_missing)

## WhiteLabel

GET `/white-label-config` Read the active white-label config for the caller's tenant

Resolves the brand config through the tenant's active consultant link, preferring a tenant-specific row over the consultant-wide default. Read-only.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Brand config (brand\_name, colours, logo\_url, domain, support\_email, legal\_footer)
- `401` Missing or invalid bearer token
- `404` No active white-label config applies to this tenant
- `503` db\_binding\_missing or white-label tables not provisioned

## Widgets

GET `/widgets` List deployed widgets for the caller's tenant

Each widget carries its own licence and revocation path; the embed CDN returns 410 once a widget is revoked.

**Auth:** Session (Clerk JWT)

### Responses

- `200` Widget rows (kind, host, pages, licence, status, installed, views\_30d, revoked\_at)
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/widgets` Generate a new widget embed

Mints a kye:widget URN with a fresh 24-character embed key and returns the ready-to-paste embed snippet pointing at the widget CDN.

**Auth:** Session (Clerk JWT)

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `kind` **required** | string |  |
| `host` | string | Optional host the embed is installed on |
| `licence` | string | One of commercial, evaluation, open |

### Responses

- `201` Created widget with embed\_key and embed\_snippet
- `400` invalid\_json or missing\_kind
- `503` Required D1 / service binding not attached (db\_binding\_missing)

POST `/widgets/{id}` Rotate a widget's embed key

Issues a fresh 24-character embed key for an unrevoked widget. The only supported action is rotate.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Request body (required)

| field | type | description |
| --- | --- | --- |
| `action` **required** | string | One of rotate |

### Responses

- `200` New embed\_key for the widget
- `400` unsupported\_action
- `404` not\_found\_or\_revoked
- `503` Required D1 / service binding not attached (db\_binding\_missing)

DELETE `/widgets/{id}` Revoke a widget (soft-delete)

Flips status to revoked and sets revoked\_at; the embed CDN then serves 410 for this widget.

**Auth:** Session (Clerk JWT)

### Parameters

| name | in | type | description |
| --- | --- | --- | --- |
| `id` **required** | path | string |  |

### Responses

- `200` Revoked — returns id and revoked\_at
- `404` not\_found\_or\_already\_revoked
- `503` Required D1 / service binding not attached (db\_binding\_missing)
