API reference

KYE Protocol™ Admin API

188 operations · admin.yaml

Servers
https://admin.kyeprotocol.com/api/v1
Version
1.0.0
Source
admin.yaml

Owner-only REST surface for admin.kyeprotocol.com. All endpoints require a Clerk JWT with role=owner. Operates against Cloudflare D1 (KYE_DB binding).

Tenants

GET/tenantsList all active tenants

Auth: Session (Clerk JWT)

Responses

  • 200 OK
  • 401 Missing or invalid bearer token
  • 403 Role not owner
POST/tenantsCreate a tenant

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
name requiredstring
slugstring
envstringOne of prod, sandbox
regionstring
owner_emailstring
clerk_org_idstring
contract_statusstring
sla_tierstring
notesstring

Responses

  • 201 Created
  • 400 Resource not found
  • 409 Slug already taken
GET/tenants/{id}Get tenant by ID

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
  • 404 Resource not found
PATCH/tenants/{id}Update tenant

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
  • 404 Resource not found
DELETE/tenants/{id}Soft-delete tenant

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
  • 404 Resource not found
POST/tenants/{id}/revokeForce-revoke a tenant (operator emergency action)

Sets tenants.status='revoked' + revoked_at/by/reason atomically with an audit_events insert (event_type kye.admin.tenant.revoked.v1), then best-effort enqueues a kye.lifecycle.compensating.v1 message onto KYE_LIFECYCLE_QUEUE so billing + evidence-archiver consumers react. This is the dual-channel admin endpoint; the canonical email-action token URL at /email-action dispatches to the same logic — single-use is enforced by the email_action_token_used UNIQUE constraint + this handler's idempotent revoked-state check. Idempotent: revoking an already-revoked tenant returns 200 with idempotent: true. Reversibility: none (re-provisioning is a new pilot grant, not an un-revoke).

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstringTenant ID to revoke

Request body (required)

fieldtypedescription
reasonstringOne of customer_requested, non_payment, compliance_breach, fraud, operator_action, tenant_lifecycle_end
notestring

Responses

  • 200 Tenant revoked (or already-revoked idempotent return)
  • 400 Invalid JSON or unrecognised reason
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Tenant ID not found
  • 409 Conflicting concurrent modification (state changed mid-request)
  • 412 Second approver missing for dual-channel revocation (when configured)

LegalEntities

POST/legal-entitiesCreate legal entity

Auth: Session (Clerk JWT)

Responses

  • 201 Created

BillingAccounts

GET/billing-accounts

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystring

Responses

  • 200 OK
POST/billing-accounts

Auth: Session (Clerk JWT)

Responses

  • 201 Created
GET/billing-accounts/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
PATCH/billing-accounts/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
DELETE/billing-accounts/{id}Always returns 405 — billing accounts are never hard-deleted

DELETE is exported by the handler but unconditionally returns 405 with error: "billing_accounts_cannot_be_deleted" and the hint to set state=closed via PATCH instead. Billing records are financial audit artefacts; removal would break the §30 WORM retention contract. This operation exists so the OpenAPI ↔ Functions bijection gate observes the exported handler; callers never receive a 2XX from this verb.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 405 Billing accounts cannot be deleted — close via PATCH state=closed
  • default Billing-account DELETE is unconditionally rejected with 405 — see the 405 response below. The `default` response is declared so the OpenAPI 2XX-coverage rule is satisfied without lying about a 200 path that does not exist.

Domains

GET/domains

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystring

Responses

  • 200 OK
POST/domains

Auth: Session (Clerk JWT)

Responses

  • 201 Created
GET/domains/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
PATCH/domains/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
DELETE/domains/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK

Policies

GET/policies

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystring
regimequerystring

Responses

  • 200 OK
POST/policies

Auth: Session (Clerk JWT)

Responses

  • 201 Created
GET/policies/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
PATCH/policies/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
DELETE/policies/{id}Always returns 405 — admin policies are append-only audit artefacts

DELETE is exported by the handler but unconditionally returns 405 with error: "policies_are_immutable" and the hint to set state=retired via PATCH instead. Compiled policies carry signatures and integrity seals and are referenced from evidence chains, so removal is forbidden. This operation exists so the OpenAPI ↔ Functions bijection gate observes the exported handler; callers never receive a 2XX from this verb.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 405 Policies are immutable — retire via PATCH state=retired
  • default Policy DELETE is unconditionally rejected with 405 — see the 405 response below. The `default` response is declared so the OpenAPI 2XX-coverage rule is satisfied without lying about a 200 path that does not exist.
GET/purposesDeclared purposes across tenants, with lawful-basis filter

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
qquerystringFree-text filter
tenant_idquerystring
lawful_basisquerystring

Responses

  • 200 Matching purposes and the tenants they belong to
  • 503 `db_binding_missing`

Workspaces

GET/workspaces

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystring

Responses

  • 200 OK
POST/workspaces

Auth: Session (Clerk JWT)

Responses

  • 201 Created
GET/workspaces/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
PATCH/workspaces/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
DELETE/workspaces/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK

Projects

GET/projects

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystring
workspace_idquerystring

Responses

  • 200 OK
POST/projects

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
tenant_id requiredstring
name requiredstring
workspace_idstring,nullOptional pinning workspace. Null or absent = a project that spans workspaces; at least one of workspace_id / visible_in_workspaces must be supplied.
visible_in_workspacesarrayWorkspaces permitted to reference this project. Declared reach only — the acting principal still needs its acts_in row and a granted_access_to grant.

Responses

  • 201 Created
GET/projects/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
PATCH/projects/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
DELETE/projects/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK

Teams

GET/teams

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystring
workspace_idquerystring

Responses

  • 200 OK
POST/teams

Auth: Session (Clerk JWT)

Responses

  • 201 Created
GET/teams/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
PATCH/teams/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
DELETE/teams/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK

Principals

GET/principals

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystring
principal_classquerystring

Responses

  • 200 OK
POST/principals

Auth: Session (Clerk JWT)

Responses

  • 201 Created
GET/principals/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
PATCH/principals/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
DELETE/principals/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK

Resources

GET/resources

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystring
kindquerystring
workspace_idquerystring

Responses

  • 200 OK
POST/resources

Auth: Session (Clerk JWT)

Responses

  • 201 Created
GET/resources/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
PATCH/resources/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
DELETE/resources/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK

Models

GET/models

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystring
risk_tierquerystring

Responses

  • 200 OK
POST/models

Auth: Session (Clerk JWT)

Responses

  • 201 Created
GET/models/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
PATCH/models/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
DELETE/models/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK

Tools

GET/tools

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystring
workspace_idquerystring

Responses

  • 200 OK
POST/tools

Auth: Session (Clerk JWT)

Responses

  • 201 Created
GET/tools/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
PATCH/tools/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
DELETE/tools/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK

ExternalApps

GET/external-apps

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystring
connector_kindquerystring

Responses

  • 200 OK
POST/external-apps

Auth: Session (Clerk JWT)

Responses

  • 201 Created
GET/external-apps/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
PATCH/external-apps/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
DELETE/external-apps/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK

AuditStreams

GET/audit-streams

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystring

Responses

  • 200 OK
POST/audit-streams

Auth: Session (Clerk JWT)

Responses

  • 201 Created
GET/audit-streams/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
PATCH/audit-streams/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
DELETE/audit-streams/{id}Always returns 405 — audit streams are immutable

DELETE is exported by the handler but always returns 405 with error: "audit_streams_are_immutable". Audit streams are append-only under §30 WORM Retention. To retire a stream, PATCH it with state: "sealed". This operation exists so the OpenAPI ↔ Functions bijection gate observes the exported handler; callers never receive a 2XX from this verb.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 405 Audit streams are immutable
  • default Audit-stream DELETE is unconditionally rejected with 405 — see the 405 response below. The `default` response is declared so the OpenAPI 2XX-coverage rule is satisfied without lying about a 200 path that does not exist.

Relationships

GET/relationships/member-of

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
team_idquerystring
principal_idquerystring

Responses

  • 200 OK
POST/relationships/member-of

Auth: Session (Clerk JWT)

Responses

  • 201 Created
GET/relationships/member-of/{id}Fetch one team membership

Resolves a single team_members row. The composite key is passed either as ?team_id=&principal_id= query params or encoded in the path segment as base64url(team_id|principal_id). Returns 400 when neither form decodes, 404 when no membership exists.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring
team_idquerystring
principal_idquerystring

Responses

  • 200 Membership row
  • 400 Composite id not decodable and query params absent
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Resource not found
DELETE/relationships/member-of/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring
team_idquerystring
principal_idquerystring

Responses

  • 200 OK
GET/relationships/acts-in

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
principal_idquerystring
workspace_idquerystring

Responses

  • 200 OK
POST/relationships/acts-in

Auth: Session (Clerk JWT)

Responses

  • 201 Created
GET/relationships/acts-in/{id}Fetch one workspace membership

Resolves a single principal_workspaces row. The composite key is passed either as ?principal_id=&workspace_id= query params or encoded in the path segment as base64url(principal_id|workspace_id). Returns 400 when neither form decodes, 404 when no row exists.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring
principal_idquerystring
workspace_idquerystring

Responses

  • 200 Workspace-membership row
  • 400 Composite id not decodable and query params absent
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Resource not found
DELETE/relationships/acts-in/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring
principal_idquerystring
workspace_idquerystring

Responses

  • 200 OK
GET/relationships/granted-access-to

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
resource_idquerystring
grantee_idquerystring

Responses

  • 200 OK
POST/relationships/granted-access-to

Auth: Session (Clerk JWT)

Responses

  • 201 Created
GET/relationships/granted-access-to/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
DELETE/relationships/granted-access-to/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
GET/relationships/uses

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
agent_idquerystring
used_kindquerystring

Responses

  • 200 OK
POST/relationships/uses

Auth: Session (Clerk JWT)

Responses

  • 201 Created
GET/relationships/uses/{id}Fetch one agent capability grant

Resolves a single agent_capabilities row. Unlike the other relationship lookups the key is a triple, so the handler accepts ONLY query params — ?agent_id=&used_id=&used_kind= are all required; the path segment is not decoded. Returns 400 when any of the three is absent, 404 when no row matches.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring
agent_id requiredquerystring
used_id requiredquerystring
used_kind requiredquerystring

Responses

  • 200 Capability row
  • 400 One of agent_id / used_id / used_kind query params missing
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Resource not found
DELETE/relationships/uses/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring
agent_idquerystring
used_idquerystring
used_kindquerystring

Responses

  • 200 OK
GET/relationships/applies-to

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
policy_idquerystring
target_classquerystring

Responses

  • 200 OK
POST/relationships/applies-to

Auth: Session (Clerk JWT)

Responses

  • 201 Created
GET/relationships/applies-to/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
DELETE/relationships/applies-to/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK

StateRegistry

GET/state-machines

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystring

Responses

  • 200 OK
POST/state-machines

Auth: Session (Clerk JWT)

Responses

  • 201 Created
GET/state-events

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
entity_idquerystring
machine_idquerystring
limitqueryinteger

Responses

  • 200 OK
  • 400 entity_id or machine_id required
GET/state-events/{id}

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 OK
  • 404 Resource not found
PATCH/state-events/{id}Always returns 405 — state events are immutable

PATCH (like DELETE) is exported by the handler but always returns 405 with error: "state_events_are_immutable". State events are append-only under the §13 Resilience Loop™ + §30 WORM contract. To represent a correction, fire a compensating transition via POST /state-transitions. This operation exists so the OpenAPI ↔ Functions bijection gate observes the exported handler; callers never receive a 2XX from this verb.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 405 State events are immutable
  • default State-event PATCH is unconditionally rejected with 405 — see the 405 response below. The `default` response is declared so the OpenAPI 2XX-coverage rule is satisfied without lying about a 200 path that does not exist.
DELETE/state-events/{id}Always returns 405 — state events are immutable

DELETE (and PATCH) are exported by the handler but always return 405 with error: "state_events_are_immutable". State events are append-only under the §13 Resilience Loop™ + §30 WORM contract. To represent a reversal, fire a compensating transition via POST /state-transitions. This operation exists so the OpenAPI ↔ Functions bijection gate observes the exported handler; callers never receive a 2XX from this verb.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 405 State events are immutable
  • default State-event DELETE is unconditionally rejected with 405 — see the 405 response below. The `default` response is declared so the OpenAPI 2XX-coverage rule is satisfied without lying about a 200 path that does not exist.
POST/state-transitionsFire a state-machine transition (gateway to private evaluator)

Edge gateway only — forwards the transition request to the private STATE_EVALUATOR Worker which atomically (a) evaluates the transition guards, (b) signs the resulting state_event row with the tenant signing kid, and (c) propagates cascade effects. The transition-evaluation construction (guard order, canonical-form rule, cascade propagation) is patent-track and not disclosed here. Tenant scoping: every request is bounded by request.kye_session.tenantId — cross-tenant attempts return 401 with tenant_required before reaching the evaluator. Returns 501 when STATE_EVALUATOR is unbound (e.g. preview deploys).

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
entity_class requiredstring
entity_id requiredstring
to_state requiredstring
evidence_refsarray
actor_rolestring
second_approverstringSecond-approver email for dual-control transitions

Responses

  • 200 Transition fired; signed state_event row created
  • 400 Missing required fields or invalid JSON
  • 401 Tenant not bound on session (or invalid bearer)
  • 403 Role not owner
  • 409 Guard rejected the transition (conflicting state)
  • 412 Second approver missing for dual-control transition
  • 501 STATE_EVALUATOR binding not configured on this deploy
  • 503 STATE_EVALUATOR fetch failed

StateLibrary

GET/state-library

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
categoryquerystring
includequerystring

Responses

  • 200 OK
GET/state-machines/from-library

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_id requiredquerystring

Responses

  • 200 OK
POST/state-machines/from-library

Auth: Session (Clerk JWT)

Responses

  • 200 OK
GET/_internal/seed-state-librarySeed the bundled state library into D1 (idempotent)

Loads the bundled state-library records and reports the bundled total against what D1 holds afterwards. Owner-only internal surface.

Auth: Session (Clerk JWT)

Responses

  • 200 Seed result
  • 503 `db_binding_missing` — KYE_DB is not bound
POST/_internal/seed-state-librarySeed the bundled state library into D1 (idempotent)

Loads the bundled state-library records and reports the bundled total against what D1 holds afterwards. Owner-only internal surface.

Auth: Session (Clerk JWT)

Responses

  • 200 Seed result
  • 503 `db_binding_missing` — KYE_DB is not bound

SigningKeys

GET/keysList every signing key (active, rotating, revoked, retired)

Returns the contents of the signing_keys D1 table — every Ed25519 / ECDSA / RSA signing key registered across all tenants, sorted by status (active → rotating → retired → revoked) then created_at DESC. The response also enumerates which custody providers are writable (in-process) vs read-only (aws-kms, gcp-kms, azure-kv, pkcs11). Owner-only; does not emit a §0.3 envelope (read-only inventory call).

Auth: Session (Clerk JWT)

Responses

  • 200 Inventory of signing keys + custody-provider matrix
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 500 Internal error
  • 503 KYE_DB binding missing on this deploy
POST/keysGenerate a new in-process signing key

Generates a new asymmetric signing key in Workers crypto (ECDSA P-256 for EdDSA/ES256, P-384 for ES384, RSASSA-PKCS1 2048 for RS256) and inserts a row into signing_keys with status=active. The kid is derived from the JWK thumbprint (RFC 7638-style) prefixed with the purpose slug and current year. Externally-managed providers (aws-kms, gcp-kms, azure-kv, pkcs11) are read-only here and return 409 — those keys must be rotated via the KMS runbook. Owner-only; the operator's email is recorded as created_by.

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
algstringJWA algorithm identifier
purposestringFree-form purpose slug recorded on the key
custody_providerstringOne of in-process, aws-kms, gcp-kms, azure-kv, pkcs11
notesstring

Responses

  • 201 Key generated; public JWK returned. Private material stays encrypted at rest.
  • 400 Invalid body, unknown alg, or unknown custody_provider
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 409 Provider is read-only (managed externally) or kid collision
  • 500 Key generation failed in Workers crypto.subtle
DELETE/keysMark a signing key revoked (soft-delete)

Soft-deletes a signing key by setting status='revoked' and revoked_at=now. The row stays — historical audit-JWS payloads signed by this kid must still resolve for replay. Idempotent on already-revoked keys (returns 404 not_found_or_already_revoked). Owner-only; reversibility is none (rotation, not un-revocation, is the recovery path).

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
kid requiredquerystringKey identifier (JWK thumbprint suffix) to revoke

Responses

  • 200 Key marked revoked
  • 400 Missing `kid` query parameter
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Kid not found or already revoked
GET/key-rotationList signing keys with rotation status + pending rotation queue

Returns signing keys (optionally scoped by ?scope=) joined with rotation lifecycle columns (created_at, activated_at, rotation_due_at, retired_at) plus the queue of pending or in-progress rotations from key_rotations. The summary block reports active count, rotations due within 30 days, and overdue rotations. Owner-only; surfaces the canonical KMS lifecycle defined in internal.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
scopequerystringOptional scope filter (e.g. `platform`, `tenant:<id>`)

Responses

  • 200 Keys + pending rotations + summary counters
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 503 KYE_DB binding missing
POST/key-rotationManually enqueue a signing-key rotation

Inserts a queued row into key_rotations and (best-effort) sends a message onto the KEY_ROTATION_OUT queue for the kye-key-rotation-orchestrator agent to consume. The compromise:true flag marks this as a compromise rotation (otherwise scheduled). The orchestrator performs the actual key release ceremony per §51 multi-sig runbook. Owner-only; emits the §0.3 evidence chain when the orchestrator processes the rotation (not at queue time).

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
key_id requiredstring
reason requiredstring
compromisebooleanTrue → mark rotation kind=compromise

Responses

  • 202 Rotation queued for the orchestrator
  • 400 Missing key_id or reason below 4-char minimum
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 key_id not found in signing_keys
  • 409 Key is already retired — rotation cannot be queued

Authorities

GET/authoritiesList authority grants across all tenants

Reads the authority_grants D1 table — every capability / purpose / scope / data_use grant issued under §12 Purpose Permission™ + §13 Resilience Loop™ + §25 Edge Governance™. Supports filtering by tenant_id, grant class, status, and free-text on id/subject/issuer. Returns a KPI block (active / pending / expired_30d / revoked_30d) alongside the row list capped at 500. Owner-only, read-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystringFilter to one tenant
classquerystringGrant class filter
statusquerystring
qquerystringLIKE match against id / subject / issuer

Responses

  • 200 Grants + KPI rollup
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 503 KYE_DB binding missing
GET/issuersList registered authority issuers

Returns rows from authority_issuers — the registry of root / delegated / KYE-trust-anchor / partner-anchor signing identities. Every issuer is keyed by an Ed25519 (or other) kid registered in the signing_keys table; this endpoint surfaces the binding plus active_grants counters. Supports filter by class, status, tenant, and free-text q (LIKE on urn / kid / display_name). Owner-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
classquerystring
statusquerystring
tenant_idquerystring
qquerystringLIKE match on issuer_urn / signing_kid / display_name

Responses

  • 200 Issuers + distinct tenant list
  • 401 Missing or invalid bearer token
  • 403 Role not owner
POST/issuersRegister a new authority issuer

Inserts a new row into authority_issuers binding an issuer URN (must start with kye:issuer:) to a signing kid from the keys registry. Issuer class must be one of root_principal, delegated_principal, kye_trust_anchor, or partner_anchor. Status is set to active; the registering operator is recorded as registered_by. Reversibility: status can be moved to suspended / revoked via a separate PATCH (not declared yet); no DELETE.

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
issuer_urn requiredstring
issuer_class requiredstringOne of root_principal, delegated_principal, kye_trust_anchor, partner_anchor
signing_kid requiredstring
display_name requiredstring
tenant_idstringOptional — null for platform-level issuers
notesstring

Responses

  • 201 Issuer registered
  • 400 Bad URN prefix, invalid class, missing kid / display_name, or invalid JSON
  • 401 Missing or invalid bearer token
  • 403 Role not owner

Ingest

GET/ingestThe citation-namespace intake queue, newest first

Lists the 100 most recent kye.ingest_submission.v1 rows from both intake channels — the console and ingest@kyeprotocol.com — with their lifecycle state: received, queued, acquired, or refused.

A row is a REQUEST to acquire a source, never a source. state is the only honest summary: acquired (and only acquired) carries a resolved_source_id, and that field's presence is the proof the submission went through the canonical acquire path rather than around it. refused retains its refusal_reason deliberately — a discarded refusal invites the same bad source to be resubmitted next month.

Owner-only, read-only. Returns an empty list on a cold DB rather than failing.

Auth: Session (Clerk JWT)

Responses

  • 200 Submissions, newest first
  • 401 Unauthenticated
POST/ingestQueue a URL or document for acquisition

Accepts a URL (application/json) or a file (multipart/form-data) and records a kye.ingest_submission.v1. It does NOT fetch, hash, or verify anything, and it never writes kye:registry:research-sources.

That restraint is the design. "Verified" means one specific thing — the URL was retrieved, the bytes hashed, and the sentence being quoted is genuinely on the page that came back — and it is implemented once, in the canonical acquire path. A second implementation here would drift from the first and both would keep writing verified.

URLs must be https and are refused for private/loopback hosts. Uploads are hashed at intake, so the bytes acquisition later reads can be shown to be the bytes that arrived. Owner-only.

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
url requiredstringhttps only; private and loopback hosts are refused

Responses

  • 202 Queued for acquisition — NOT yet a citable source
  • 400 url_required, url_rejected, or malformed_request
  • 401 Unauthenticated
  • 413 file_too_large
  • 415 unsupported_content_type
  • 500 intake_failed

Revocations

GET/revocationsList authority-grant revocations + cascade status

Reads the revocations D1 table — every authority-grant revocation with its cascade lifecycle status (complete / pending / failed) and downstream-cancelled counter. Supports filters by cascade_status, reason, initiator, and free-text on revocation_id / grant_id. KPI block reports today's count + cascade rollup. Backs the revocations.html admin page. Owner-only, read-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
cascade_statusquerystring
reasonquerystring
initiatorquerystring
qquerystring

Responses

  • 200 Revocations + KPI rollup
  • 401 Missing or invalid bearer token
  • 403 Role not owner
POST/revocationsInitiate an authority-grant revocation

Inserts a new row into revocations with cascade_status='pending'. The cascade orchestrator picks the row up out-of-band and walks downstream credentials, sealing cascade_status when complete. Reason must be one of the five canonical reasons; initiator defaults to kye_owner when omitted. Emits to the §0.3 audit chain when the cascade orchestrator runs (not at insert time). Reversibility: none — revocations are append-only.

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
grant_id requiredstring
reason requiredstringOne of compromise_suspected, policy_change, tenant_offboarding, delegation_expired, operating_model_amendment
initiatorstringOne of tenant_admin, kye_owner, incident_response
tenant_idstring
notesstring

Responses

  • 201 Revocation initiated; cascade is pending
  • 400 Missing grant_id, invalid reason, or invalid JSON
  • 401 Missing or invalid bearer token
  • 403 Role not owner

AuditChain

GET/audit-chainRead the last N hash-chained audit events

Returns the most recent rows from audit_events (default 100, max 500) for client-side hash-chain integrity verification — the front-end walks rows chronologically (ASC) and compares each row's prev_hash to the prior row's hash, exposing any chain break. The endpoint is read-only; chain integrity is computed by the caller, not asserted by the server. Empty audit_events table returns events: [] honestly. Owner-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystringFilter to one tenant
limitqueryinteger

Responses

  • 200 Recent audit events + distinct tenant list for filter UI
  • 401 Missing or invalid bearer token
  • 403 Role not owner

AuditCold

GET/audit-coldBrowse R2 cold-storage audit archives

Lists objects in the kye-audit-cold R2 bucket (binding KYE_AUDIT_COLD_R2). When the R2 binding is unbound (e.g. preview deploys) returns an honest empty payload with note: "r2_not_bound" rather than failing. Filters by prefix or tenant (which maps to the ${tenant}/ prefix). Also returns the pending/approved restore requests from audit_cold_restores so the operator sees outstanding dual-control restores in the same view. Owner-only, read-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
prefixquerystring
tenantquerystringConvenience filter — equivalent to prefix=<tenant>/
cursorquerystringR2 list cursor for pagination

Responses

  • 200 R2 object listing (or honest unbound state) + pending restores
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 502 R2 list call failed
POST/audit-coldRequest (or 2nd-approve) a cold-storage audit restore

Two operations on the same endpoint, switched by op: (1) restore (default) — inserts a pending row into audit_cold_restores with the requesting operator, reason (min 8 chars), and target object_key. (2) approve — flips a pending restore to approved provided the approver is NOT the original requester (dual-control: anti self-approval enforced by 403). The restore is only executed once the row is approved, by a separate background consumer. Owner-only. Reversibility: a pending request can be cancelled out-of-band but not via this endpoint.

Auth: Session (Clerk JWT)

Responses

  • 200 Restore approved by 2nd approver
  • 201 Restore requested; status=pending awaiting 2nd approver
  • 400 Invalid JSON, missing object_key, or reason below 8-char minimum
  • 401 Missing or invalid bearer token
  • 403 Self-approval attempt — the requester cannot approve their own restore
  • 404 restore_id not found (approve op)
  • 409 Restore is not in `pending` status (approve op)
  • 412 Second approver missing for dual-control approve

Decisions

GET/decisionsList recent cross-tenant PDP decisions

Returns up to 500 recent rows from decisions — the canonical Purpose Permission™ Decision Engine output, written by every agent worker via the self-audit-daemon schema. Supports filters by tenant, decision verdict, since-timestamp, and free-text on decision_id / actor / capability. The response also includes a verdict-class tally and a top-50 per-tenant rollup. Empty table returns decisions: [] honestly. Owner-only, read-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenantquerystring
decisionquerystringVerdict filter (e.g. permit / deny / abstain)
sincequerystring
qquerystringLIKE on decision_id / actor / capability

Responses

  • 200 Decisions + per-verdict and per-tenant rollups
  • 401 Missing or invalid bearer token
  • 403 Role not owner
GET/analytics-decisions-per-hourDecision volume bucketed per hour for the calling tenant

Window defaults to the last 7 days and is capped at 90 days; longer windows are served by the warehouse path on the gateway worker.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
sincequerystringWindow start (default: until minus 7 days)
untilquerystringWindow end (default: now)

Responses

  • 200 Hourly buckets
  • 400 `invalid_since` · `invalid_until` · `since_must_precede_until` · `window_too_large`
  • 503 `db_binding_missing`

Evidence

GET/evidence-indexList signed evidence packs across all tenants

Reads evidence_index — every signed evidence pack indexed at emission per §30 WORM contract (R2 Object Lock is the authoritative store; this D1 row is the searchable index). Filters by tenant, pack class, time window (24h / 7d / 30d / 90d / all), and free-text on pack_id / tenant_id / signing_kid. KPI block reports total, 24h volume, currently-locked-by-retention count, and signature failures. Pagination via limit (default 200, max 500) and offset. Owner-only, read-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystring
pack_classquerystring
windowquerystring
qquerystring
limitqueryinteger
offsetqueryinteger

Responses

  • 200 Indexed evidence packs + KPI + tenant facet
  • 401 Missing or invalid bearer token
  • 403 Role not owner
GET/evidence-timelineGet the lifecycle timeline for one action proposal

Two modes: (1) picker — ?action_id= omitted → returns the 50 most recent rows from app_action_approvals for the proposal-picker UI. (2) timeline — ?action_id=<id> → returns the observable lifecycle steps (proposed → routed → decided → evidence sealed) for that one proposal. Envelope signing + replay-proof construction are patent-track and intentionally not surfaced here. The app_action_approvals table is owned by the Cloud surface (§0 — no duplicate runtime CREATE here); this is a cross-tenant read. Owner-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
action_idquerystringAction proposal ID — omit to list recent proposals

Responses

  • 200 Picker rows OR per-proposal timeline (empty steps for unknown id)
  • 401 Missing or invalid bearer token
  • 403 Role not owner
GET/data-flow-graphLatest sealed data-flow totals for the calling tenant

Reads the most recent of up to 200 data-flow seals; the latest seal's totals stand for the current view.

Auth: Session (Clerk JWT)

Responses

  • 200 Current sealed data-flow view
  • 503 `db_binding_missing`

CriticalReviews

GET/critical-point-reviewList dual-control reviews (two-person / two-person-with-legal)

Returns the subset of app_action_approvals whose approval_mode requires dual control: two_person (SR 11-7 model-risk control) or two_person_with_legal (EU AI Act Art. 14 human-oversight). Filters by mode, state (pending / approved / rejected / escalated), and free-text on proposal_id / actor_id / action_type / tenant_id. KPI block reports pending count, mode split, and decided count. The app_action_approvals table is owned by the Cloud surface (§0 — no duplicate runtime CREATE here). Owner-only, read-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
modequerystring
statequerystring
qquerystring

Responses

  • 200 Critical-point reviews + KPI
  • 401 Missing or invalid bearer token
  • 403 Role not owner

Replay

GET/replay-toolsList recent admin Replay-Proof™ runs with verdict KPI

Reads the append-only admin_replays table (newest 200) with optional filters by verdict (pending / verified / diverged / signature_failed) and tenant. Returns a KPI block counting each verdict over the returned rows plus the distinct tenant list for the filter dropdown. Verdicts are resolved out-of-band by the Replay Engine; this surface is read-only. Owner-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
verdictquerystring
tenant_idquerystring

Responses

  • 200 Replay runs + verdict KPI + tenant filter values
  • 401 Missing or invalid bearer token
  • 403 Role not owner
POST/replay-toolsQueue a Replay-Proof™ run for an evidence pack

Inserts an append-only row into admin_replays (WORM — no UPDATE/DELETE) with verdict='pending'. The Replay Engine consumes the queued run, resolves the verdict (verified / diverged / signature_failed), and appends the kye.replay.proof.v1 envelope out-of-band. The verification mechanism is patent-track and is not disclosed here. Returns 404 if the referenced evidence_pack_id is not in evidence_packs. Owner-only. Reversibility: none — replay records are append-only.

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
evidence_pack_id requiredstring
notesstring

Responses

  • 201 Replay queued; verdict=pending until the Replay Engine resolves it
  • 400 evidence_pack_id missing or does not start with `kye:evidence-pack:`
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 evidence_pack_id not found in evidence_packs

PilotApplications

GET/pilot-applicationsList pilot-pipeline applications with latest decision

LEFT-JOINs audit_pilot_applications (immutable submissions from the public site) with the most recent row in pilot_application_decisions per application — never mutates the application record (append-only decisions). Returns the 200 most recent applications plus a queue rollup (pending / granted / rejected counts). Empty audit_pilot_applications table returns applications: [] honestly. Owner-only, read-only.

Auth: Session (Clerk JWT)

Responses

  • 200 Applications + queue rollup
  • 401 Missing or invalid bearer token
  • 403 Role not owner
POST/pilot-applications/{id}/commercial-menuSend the commercial menu to a pilot applicant after their scoping call

Emails the applicant the SKUs the operator judged to fit that customer's workflow, plus a link to book the follow-up. Dispatches the canonical commercial-menu template through the Comms Engine.

The operator's selection is the input rather than a SKU id list: the panel already holds the catalogue it selected from, and a Pages Function cannot read the spec tree at runtime to resolve names.

Pilot SKUs are closed-registration / contact-for-pricing, so the rendered menu carries names and never prices. Idempotency covers the SELECTION, not just the application: re-sending the same set is a duplicate, while offering a different set is a new message.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstringaudit_pilot_applications.id

Request body (required)

fieldtypedescription
skus requiredarrayThe SKUs selected for this customer on the scoping call

Responses

  • 200 Menu dispatched
  • 400 No SKUs selected, too many SKUs, or a SKU missing its id or name
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Application not found
  • 422 Application has no email address to send to
  • 502 Comms Engine dispatch failed
  • 503 Database binding missing
POST/pilot-applications/{id}/grantApprove a pilot application and seed the full tenant tree

Approves a pilot and atomically seeds the v3 tenant hierarchy: tenants → legal_entities → billing_accounts → workspaces → teams → principals → team_members → principal_workspaces, plus an initial seed state_events row per entity and one append-only pilot_application_decisions row. Every INSERT uses INSERT OR IGNORE so re-grant is safe. Slug collisions are retried with a numeric suffix up to 4 attempts. Dual-channel admin: the email-action one-click URL dispatches to the same logic (single-use enforced by email_action_token_used UNIQUE constraint). Returns 409 if the application is already granted. Owner-only; emits §0.3 governance events via the state-evaluator.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstringaudit_pilot_applications.id

Request body

fieldtypedescription
reasonstringOptional rationale recorded on the decision row
regionstring
sla_tierstring
country_codestring

Responses

  • 200 Pilot granted; tenant tree seeded
  • 400 Missing application_id path parameter
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Application not found
  • 409 Application already granted (idempotency guard)
  • 412 Second approver missing for dual-channel grant (when configured)
POST/pilot-applications/{id}/rejectReject a pilot application (no tenant created)

Appends a reject row into pilot_application_decisions. No tenant tree is created. Idempotent on duplicate rejects — the decision row is append-only and multiple rows can coexist. Dual-channel admin: the email-action one-click URL dispatches to the same logic. Owner-only; the operator is recorded as decided_by. Reversibility: a subsequent grant POST CAN succeed for the same application (the latest-decision rule decides the application's current state).

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstringaudit_pilot_applications.id

Request body

fieldtypedescription
reasonstring

Responses

  • 200 Rejection recorded
  • 400 Missing application_id path parameter
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Application not found
POST/pilot-applications/{id}/send-sign-inEmail a one-click Clerk sign-in link to a granted pilot applicant

Step 2 of the grant flow. Looks up the applicant's email on audit_pilot_applications, finds or creates the Clerk user (passwordless, role=operator), mints a 24-hour Clerk sign_in_token and dispatches the canonical §38 Comms template admin.send-sign-in.v1 carrying the one-click dashboard URL. An audit row lands in pilot_application_signin_invites storing only the token's SHA-256 — the URL itself is never persisted (single-use; leaked-in-transit risk only). Dual-channel admin (§27 §4): the email-action channel converges on the same handler logic. Requires the CLERK_SECRET_KEY env var and the MAIL service binding; returns 503 when either is absent and 502 when the mail dispatch fails. Owner-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstringaudit_pilot_applications.id

Responses

  • 200 Invite emailed; audit row written with token hash only
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Application not found
  • 422 Application has no email address on file
  • 502 Mail dispatch via the §38 Comms Engine failed
  • 503 KYE_DB, CLERK_SECRET_KEY or MAIL binding missing

ExpertReviews

POST/expert-reviews/{id}/approvePublish a pending expert review (moderator action)

Sets expert_reviews.status='published' with moderator + timestamp. Idempotent: re-approving an already-published row returns 200 with a no-op note. Cannot republish a previously-rejected row directly. On a fresh approval, dispatches the §38 Comms Engine status email expert-review.approved.v1 to the submitter (background + fail-soft via waitUntil — comms failures NEVER fail the moderation action). Owner-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstringexpert_reviews.id

Responses

  • 200 Review published (or already-published no-op)
  • 400 Missing id path parameter
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Review id not found
POST/expert-reviews/{id}/rejectReject a pending expert review (moderator action)

Sets expert_reviews.status='rejected' with moderator + timestamp and stores the rejection reason in moderator_note (column added idempotently via ALTER TABLE). Dispatches the §38 Comms Engine status email expert-review.rejected.v1 to the submitter. The reason is recorded on the row but NOT included in the email body (the rejected template is reason-free, intentional). Owner-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstringexpert_reviews.id

Request body

fieldtypedescription
reasonstringModerator note recorded on the row (not emailed)

Responses

  • 200 Review rejected
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Review id not found
GET/expert-reviewsList the Expert Wall™ moderation queue

Reads the expert_reviews table (the same D1 instance the public expert-review intake writes to), newest 200 rows. The default sort surfaces pending first, then published, then rejected; ?status= narrows to one state. Returns a queue roll-up counting each status across the full table. Fails silent (empty list) on a cold DB. Owner-only, read-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
statusquerystring

Responses

  • 200 Moderation view + per-status counts
  • 401 Missing or invalid bearer token
  • 403 Role not owner
POST/expert-reviews/{id}/request-changesAsk the submitter for changes (non-terminal moderation action)

Unlike approve / reject this leaves the row's status as pending so the submission stays in the moderation queue — the expert_reviews.status CHECK only allows pending / published / rejected. The required moderator note (≤ 1000 chars) is written to expert_reviews.moderator_note and durably recorded as an expert_review_audit row with action='changes_requested'. On success the canonical §38 Comms template expert-review.changes-requested.v1 is dispatched to the submitter in the background (fail-soft — email failure never fails the action). Owner-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstringexpert_reviews.id

Request body (required)

fieldtypedescription
note requiredstringTells the submitter what to change

Responses

  • 200 Changes requested; row stays pending in the queue
  • 400 Missing id or empty moderator note
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Review not found

Entities

POST/entities/{id}/revokeForce-revoke an entity cross-tenant (operator emergency action)

Sets entities.lifecycle_state='revoked' with revoked_at/by/reason and best-effort enqueues a cascade onto REVOCATION_OUT if bound. Reason is required (minimum 4 characters). Idempotent — re-revoking is a no-op that returns 200 with note: "already revoked". Dual-channel admin: the email-action one-click URL dispatches to the same logic; single-use enforced by the email_action_token_used UNIQUE constraint. Owner-only emergency surface (§8 admin emergency section). Reversibility: none — recovery is re-issuance, not un-revoke.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstringentities.entity_id

Request body (required)

fieldtypedescription
reason requiredstring

Responses

  • 200 Entity revoked (or already-revoked idempotent return)
  • 400 Missing id or reason (or reason below 4-char minimum)
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Entity id not found
  • 409 Concurrent modification (lifecycle_state changed mid-request)
  • 412 Second approver missing for dual-channel revocation
GET/_internal/bootstrap-hierarchyCreate the entity-hierarchy tables and indexes if absent (idempotent)

Applies the entity-hierarchy DDL and reports what it had to create. Idempotent — tables that already exist are counted, never recreated. Owner-only internal surface; emits kye.bootstrap.hierarchy.v3.

Auth: Session (Clerk JWT)

Responses

  • 200 Bootstrap result
  • 503 `db_binding_missing` — KYE_DB is not bound
POST/_internal/bootstrap-hierarchyCreate the entity-hierarchy tables and indexes if absent (idempotent)

Applies the entity-hierarchy DDL and reports what it had to create. Idempotent — tables that already exist are counted, never recreated. Owner-only internal surface; emits kye.bootstrap.hierarchy.v3.

Auth: Session (Clerk JWT)

Responses

  • 200 Bootstrap result
  • 503 `db_binding_missing` — KYE_DB is not bound
GET/entitiesCross-tenant entity list with type tally

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
qquerystringFree-text filter
typequerystringEntity type
statequerystringLifecycle state
tenantquerystringRestrict to one tenant

Responses

  • 200 Matching entities plus a per-type tally
  • 503 `db_binding_missing`

Partners

GET/partner-programmeList partner-programme registrants with tier + KPI

Reads the partners D1 table with optional filters by tier (foundation / certified / advanced / strategic), status (active / onboarding / suspended), and free-text on name / id. Returns a KPI block (active count, certified count, total open_deals, YTD revshare cents). Backed by the §10 Partner constitution + §49 Universal Engagement Rail (partner is one of five engagement types). Owner-only, read-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tierquerystring
statusquerystring
qquerystringLIKE on name / id

Responses

  • 200 Partners + KPI
  • 401 Missing or invalid bearer token
  • 403 Role not owner
POST/partner-programmeOnboard a new partner into the partner registry

Inserts a row into the partners D1 table. name is required; tier defaults to foundation and status to onboarding when absent or invalid. The partner ID is minted as kye:partner:<name-slug>.<6-char-uuid> and certifications start empty. The operator (Clerk session email) is recorded as created_by. Backed by the §10 Partner constitution + §49 Universal Engagement Rail. Owner-only.

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
name requiredstring
tierstringOne of foundation, certified, advanced, strategic
statusstringOne of active, onboarding, suspended
contact_emailstring
regionstring
notesstring

Responses

  • 201 Partner created in onboarding state
  • 400 Missing name or non-JSON body
  • 401 Missing or invalid bearer token
  • 403 Role not owner
GET/admin/partners/listList partner entities with KPI roll-up

Operator-facing list over the partner_entities registry with optional filters by status (active / onboarding / suspended / offboarding / retired), raw tier (applicant / registered / certified / strategic / T1 / T2 / T3) and free-text q (LIKE on legal_name / display_name / id). Paginated via limit (max 500) + offset. Returns a KPI block (active, certified, applicant, offboarded). Honest empty state: an unprovisioned registry table returns 200 with zero counts and an explanatory note rather than an error. Owner-only, read-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
statusquerystring
tierquerystring
qquerystringLIKE on legal_name / display_name / id
limitqueryinteger
offsetqueryinteger

Responses

  • 200 Partners + KPI + total for pagination
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 503 KYE_DB binding missing
GET/admin/partners/{id}Single-partner detail with certifications, deals and audit log

Resolves one partner_entities row (rejecting IDs that do not start with kye:partner:) and joins the partner's most recent 50 certifications, 50 deals and 100 kye_partner_admin_actions audit rows. The stored body_json blob is parsed and inlined as partner.body. Returns 404 when the partner does not exist and 503 (registry_not_provisioned) when the registry table has not been migrated yet. Owner-only, read-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Responses

  • 200 Partner detail + related rows
  • 400 Path id does not start with the kye:partner: prefix
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Partner not found
  • 503 KYE_DB binding missing or partner registry not provisioned
POST/admin/partners/{id}/certifyPromote a partner to certified at tier T1–T3

Tier promotion through the single canonical partner-admin mutation path (applyPartnerAdminAction): atomic D1 batch of the partner_entities state mutation (partner_tier='certified', status='active'), an append-only kye_partner_admin_actions row (§30 WORM) and an audit_events row carrying the kye.partner_admin_action.v1 envelope (§0.3). A tier of T1, T2 or T3 is required — Tier 4+ does not exist (constitution §10 §2, "No Tier 4"). Owner-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Request body (required)

fieldtypedescription
tier requiredstringOne of T1, T2, T3
second_approver_idstring
reasonstring

Responses

  • 201 Certification applied; §0.3 evidence chain recorded
  • 400 Non-JSON body, invalid partner id, or tier missing / outside T1–T3
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Partner not found
  • 503 KYE_DB binding missing or partner-admin-actions migration not applied
POST/admin/partners/{id}/renewalRenew a partner's certification validity window

Refreshes the partner's certification window through the single canonical partner-admin mutation path, stamping renewed_at into body_json plus the append-only kye_partner_admin_actions + audit_events rows (§30 / §0.3). Non-destructive and non-tier-changing, so no dual approval and no tier are required (see DESTRUCTIVE_ACTIONS / TIER_REQUIRED_ACTIONS). The renewal action_type was already implemented in the shared library; only this HTTP binding was missing, so the console's "Renew certification" control posted to a 404. Owner-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Request body

fieldtypedescription
reasonstringOptional note recorded on the action row

Responses

  • 201 Renewal applied; §0.3 evidence chain recorded
  • 400 Non-JSON body or invalid partner id
  • 401 Missing or invalid bearer token
  • 404 Unknown partner
POST/admin/partners/{id}/revokeRevoke a partner (destructive; dual-approval required)

Destructive revocation through the single canonical partner-admin mutation path: partner_entities.status flips to offboarding with offboarded_at stamped, plus the append-only kye_partner_admin_actions + audit_events rows (§30 / §0.3). Banking-grade dual approval (constitution §0.4): the body MUST carry a reason of ≥ 20 characters and a second_approver_id distinct from the performer. Idempotent — revoking a partner already in offboarding returns 200 with idempotent: true. On success a kye.lifecycle.compensating.v1 message is enqueued to KYE_LIFECYCLE_QUEUE for downstream compensation. Owner-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring

Request body (required)

fieldtypedescription
reason requiredstring
second_approver_id requiredstringMust differ from the performing operator

Responses

  • 200 Partner already offboarding — idempotent no-op
  • 201 Revocation applied; §0.3 evidence chain recorded
  • 400 Non-JSON body, invalid partner id, reason under 20 chars, or second approver missing / same as performer
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Partner not found
  • 503 KYE_DB binding missing or partner-admin-actions migration not applied
GET/admin/partners/applications/listList partner engagement applications by workflow status

Reads engagement_applications filtered to engagement_type='partner' and the requested status (default pending), falling back to the legacy partner_applications table when the §49 store returns no rows — the response's source field reports which table served the data. Also returns a per-status counts roll-up over the partner engagement queue. Paginated via limit (max 500) + offset. Owner-only, read-only. Constitution §10 §8.1 + §49 §3.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
statusquerystring
limitqueryinteger
offsetqueryinteger

Responses

  • 200 Applications + per-status counts
  • 400 Unknown status value
  • 401 Missing or invalid bearer token
  • 403 Role not owner
POST/admin/partners/applications/{id}/grantApprove a partner application and create the partner entity

Atomic D1 batch: flips the application's workflow_status to approved (in engagement_applications, falling back to the legacy partner_applications table) and inserts a Tier-1 baseline partner_entities row whose ID is minted as kye:partner:<org-slug>.<6-char-uuid>. The optional tier body field (T1–T3, default T1) is recorded on the grant; the §0.3 evidence chain (action row + kye.partner_admin_action.v1 envelope) is then emitted via the canonical partner-admin mutation path. Idempotent — an already-approved application returns 200 with idempotent: true. Constitution §10 + §49 §3.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstringengagement_applications.application_id

Request body

fieldtypedescription
tierstringOne of T1, T2, T3
reasonstring
second_approver_idstring

Responses

  • 200 Application already approved — idempotent no-op
  • 201 Grant applied; partner entity created; §0.3 evidence chain recorded
  • 400 Missing application id path parameter
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Application not found in either store
  • 503 KYE_DB binding missing or partner registry not provisioned
POST/admin/partners/applications/{id}/rejectReject a partner application (dual-approval required)

Flips the application's workflow_status to rejected (in engagement_applications, falling back to the legacy partner_applications table), then records a retired synthetic kye:partner:rejected-application.<id> entity so the rejection flows through the same canonical partner-admin audit path (kye_partner_admin_actions + kye.partner_admin_action.v1 envelope, §0.3 / §30). Banking-grade dual approval (§0.4): the body MUST carry a reason of ≥ 20 characters and a second_approver_id. Constitution §10 + §49 §3.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstringengagement_applications.application_id

Request body (required)

fieldtypedescription
reason requiredstring
second_approver_id requiredstring

Responses

  • 201 Rejection applied; §0.3 evidence chain recorded
  • 400 Non-JSON body, missing application id, reason under 20 chars, or second approver missing
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Application not found in either store
  • 503 KYE_DB binding missing or partner registry not provisioned

AgentLibrary

GET/agent-libraryBrowse the KYE™ Agent Library™ catalog

Returns published rows from agent_library_entries — the platform's catalog of derivable agent templates (sourced from internal<category>/<slug>.v1.json and hydrated via /api/v1/_internal/seed-agent-library). Optional ?category= filter (one of 12 canonical categories). ?include=full inlines the JSON body for the picker; otherwise body_json is omitted to keep the list light. Tenant adoption flows through POST /api/v1/agents/from-library (separate operation). Owner-only, read-only. Schema: kye.agent.library_entry.v1.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
categoryquerystring
includequerystringWhen `full`, include the body_json blob inline

Responses

  • 200 Library entries (lightweight by default, full when include=full)
  • 400 Unknown category value
  • 401 Missing or invalid bearer token
  • 403 Role not owner
GET/agents/from-libraryList a tenant's Agent Library™ adoptions

Lists every adoption for the given tenant across both adoption modes — agent_subscriptions (mode subscription) and agent_derivations (mode derivation) — merged into one adoptions[] array sorted newest-first by adoption / derivation timestamp. tenant_id is required and must start with kye:tenant:. Derivation rows inline their parsed overrides. Owner-only, read-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_id requiredquerystring

Responses

  • 200 Subscriptions + derivations for the tenant
  • 400 tenant_id missing or not a kye:tenant:* ID
  • 401 Missing or invalid bearer token
  • 403 Role not owner
POST/agents/from-libraryAdopt a published Agent Library™ entry into a tenant

Adopts a published agent_library_entries row into the caller's tenant. The entry's machine_seal (sha256 over the canonical JSON body) is recomputed and verified before adoption — a mismatch returns 409. Mode subscription (default) inserts an agent_subscriptions row; mode derivation validates the overrides (additive only — removed_* keys are forbidden and a deny: on a platform-locked capability is rejected) and inserts an agent_derivations row. Both inserts are idempotent (INSERT OR REPLACE on the deterministic tenant-scoped ID). Owner-only.

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
tenant_id requiredstring
library_id requiredstring
agent_local_id requiredstringTenant-local identifier; lowercased and slug-sanitised
adoption_modestringOne of subscription, derivation
overridesobjectDerivation mode only — additive overrides

Responses

  • 200 Adoption recorded (subscription or derivation) with seal_verified=true
  • 400 Bad tenant/library id, missing agent_local_id, non-JSON body, or forbidden derivation override
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Library entry not found
  • 409 machine_seal mismatch — stored entry failed seal re-verification
GET/_internal/seed-agent-librarySeed the bundled agent library into D1 (idempotent)

Loads the bundled agent-library records and reports the bundled total against what D1 holds afterwards. Owner-only internal surface.

Auth: Session (Clerk JWT)

Responses

  • 200 Seed result
  • 503 `db_binding_missing` — KYE_DB is not bound
POST/_internal/seed-agent-librarySeed the bundled agent library into D1 (idempotent)

Loads the bundled agent-library records and reports the bundled total against what D1 holds afterwards. Owner-only internal surface.

Auth: Session (Clerk JWT)

Responses

  • 200 Seed result
  • 503 `db_binding_missing` — KYE_DB is not bound
GET/memoryAgent-memory totals for the calling tenant

Counts governed under §63 Memory Authority — totals only, never memory content.

Auth: Session (Clerk JWT)

Responses

  • 200 Memory totals by agent and class
  • 503 `db_binding_missing`

ApprovalQueue

GET/approval-queueCross-tenant view of the GovernedUI action-approval queue

Reads the app_action_approvals table (owned by the KYE™ Cloud action-approvals surface — §0 forbids a duplicate runtime CREATE TABLE here) cross-tenant, newest 500 proposals first. view selects a canned SQL condition: pending (default), high-risk, escalated, second-approval-pending, or decided. Additional filters: risk_level and free-text q over proposal_id / actor_id / action_type / tenant_id. Returns a KPI block (pending, high_risk, escalated, second_approval). Fails silent (empty list) on a cold DB. Owner-only, read-only. Schema authority: kye.governedui.action_proposal.v1.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
viewquerystring
risk_levelquerystring
qquerystringLIKE on proposal_id / actor_id / action_type / tenant_id

Responses

  • 200 Action proposals + KPI counts
  • 401 Missing or invalid bearer token
  • 403 Role not owner

Assurance

GET/assurance-issuanceList issued Assurance Cards with issuance KPI

Lists the newest 500 rows from assurance_cards with optional filters by tenant_id, framework (SOC2 / ISO27001 / ISO42001 / EU_AI_ACT / DORA / FCA_OPRES) and status (active / expiring / revoked). Returns a KPI block: issued in the last 30 days, active count, active cards expiring within 30 days, and revocations in the last 90 days. Owner-only, read-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystring
frameworkquerystring
statusquerystring

Responses

  • 200 Assurance Cards + KPI
  • 401 Missing or invalid bearer token
  • 403 Role not owner
POST/assurance-issuanceIssue a new Assurance Card for a tenant

Inserts an assurance_cards row with status active and a 90-day expiry (the §0.3 ≤90-day rotation window). Requires a kye:tenant:* tenant_id, one of the six locked frameworks, and a non-empty scope (≤ 500 chars). The card ID is minted as kye:assurance:<tenant-slug>.<framework>.<8-char-uuid> and the signing kid is resolved from the HSM key-registry context. The issuing operator (Clerk session email) is recorded as issued_by. Owner-only.

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
tenant_id requiredstring
framework requiredstringOne of SOC2, ISO27001, ISO42001, EU_AI_ACT, DORA, FCA_OPRES
scope requiredstring
notesstring

Responses

  • 201 Assurance Card issued with 90-day expiry
  • 400 Non-JSON body, bad tenant_id, invalid framework, or missing scope
  • 401 Missing or invalid bearer token
  • 403 Role not owner
GET/live-runtimeLive runtime decision feed with KPI rollup

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystring
decisionquerystringFilter by decision outcome
modequerystring
limitqueryinteger

Responses

  • 200 Feed rows, KPI rollup and the tenants present
  • 503 `db_binding_missing`

Billing

GET/billing/allStripe customer roll-up across all tenants

Cross-tenant revenue view (constitution §23 §11): assembles tenants × billing_customers × billing_usage_current_cycle as a LEFT-JOIN-style merge in JS so tenants without a subscription appear with billing: null. Returns aggregates (tenant_count, with_subscription, MRR in cents and USD, ARR run-rate, overage cents, dunning count) and the dunning rows. Honest empty state: zero tenants returns empty arrays with honest_empty_state: true. Owner-only, read-only.

Auth: Session (Clerk JWT)

Responses

  • 200 Per-tenant billing rows + cross-tenant aggregates
  • 401 Missing or invalid bearer token
  • 403 Role not owner

Classification

GET/classification-catalogCross-tenant data-classification assignments

Lists the newest 500 rows from data_classification_assignments (owned by the SQL migration tree — no runtime DDL here; queries fail silent on a cold DB) with optional filters by tenant_id, classification class (public / internal / confidential / restricted / top_secret / special_category), detection method, and free-text q over asset_id / classifier_kid. Returns a KPI block: total classified, special-category count, low-confidence (< 0.7) count, and distinct signing kids. Owner-only, read-only. Schema authority: kye.data_classification_assignment.v1.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystring
classquerystring
methodquerystring
qquerystringLIKE on asset_id / classifier_kid

Responses

  • 200 Classification assignments + KPI
  • 401 Missing or invalid bearer token
  • 403 Role not owner

CommercialLifecycle

GET/commercial-lifecycleList commercial workflows across the 16-state machine

Reads the newest 500 rows from commercial_workflows (owned by the commercial-lifecycle Worker — §0 forbids a duplicate runtime CREATE TABLE; the query fails silent on a cold DB) and rolls the §27 16-state machine into a 4-bucket KPI: pipeline (lead → contract_drafted), poc (poc_scoped → poc_evidence_delivered), live (paid → expansion_in_flight) and renewal (renewal_due). Transition signing + the payment-gate machine are resolved by the Worker, not this read-only console. Owner-only.

Auth: Session (Clerk JWT)

Responses

  • 200 Workflows + stage-bucket KPI
  • 401 Missing or invalid bearer token
  • 403 Role not owner

Compliance

GET/compliancePer-tenant framework-coverage heat-map

Reads tenant-reported rows from compliance_status, joins them against the locked 8-framework list (NIST AI RMF, EU AI Act, ISO 42001, DORA, GDPR, SR 11-7, BCBS 239, PSD2) and rolls everything into per-tenant and cross-tenant {full, part, none, na, total} coverage maps the UI renders directly. Optional tenant query filter narrows to one tenant. Honest empty state flagged when no rows exist. Owner-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenantquerystring

Responses

  • 200 Coverage roll-up per framework × tenant
  • 401 Missing or invalid bearer token
  • 403 Role not owner
POST/complianceAttest a framework capability's coverage for a tenant

Upserts one compliance_status row keyed on (tenant_id, framework, capability). Coverage must be one of full / part / none / na. The attesting operator (Clerk session email) and timestamp are stamped on the row; re-attesting the same capability replaces the prior coverage value. Used by the tenant-side controls surface and ad-hoc admin attestations. Owner-only.

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
tenant_id requiredstring
framework requiredstring
capability requiredstring
coverage requiredstringOne of full, part, none, na
evidence_refstring

Responses

  • 200 Attestation upserted
  • 400 Non-JSON body, missing fields, or bad coverage value
  • 401 Missing or invalid bearer token
  • 403 Role not owner

ConnectorCatalog

GET/connector-catalog-moderationList partner connector-kind submissions with moderation KPI

Lists the newest 500 rows from connector_catalog_submissions (declared canonically here) with optional filters by status (pending / under_review / approved / rejected), profile family (13 canonical families) and free-text q over proposed_kind / submitter_email. Returns a KPI block: approved kinds, pending queue depth, approvals in the last 90 days (certified) and rejections in the last 90 days (bounced). Owner-only, read-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
statusquerystring
familyquerystring
qquerystringLIKE on proposed_kind / submitter_email

Responses

  • 200 Submissions + moderation KPI
  • 401 Missing or invalid bearer token
  • 403 Role not owner
POST/connector-catalog-moderationRegister a proposed connector kind for moderation

Inserts a connector_catalog_submissions row in pending status. Requires proposed_kind and one of the 13 canonical profile families; conformance_score is clamped into [0, 1]. The submission ID is minted as kye:connector-submission:<kind-slug>.<6-char-uuid> and the operator (Clerk session email) is recorded as created_by. Approved kinds are later promoted into the canonical connector schema family. Owner-only.

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
proposed_kind requiredstring
profile_family requiredstringOne of payments, open_finance, identity, agent_runtime, commerce, compliance_evidence, security_siem, health, insurance, pension, utilities, legal, open_data
submitter_partner_idstring
submitter_emailstring
conformance_scorenumber
notesstring

Responses

  • 201 Submission registered in pending status
  • 400 Non-JSON body, missing proposed_kind, or invalid profile_family
  • 401 Missing or invalid bearer token
  • 403 Role not owner

Controls

GET/controlsList the locked control catalog with attestation status

Returns the locked control catalog (controls — seeded once with the CSF 2.0 + ISO 27001:2022 top-level rows when empty) alongside per-tenant control_attestations rows, plus a by_control map grouping attestations by control_id. Optional filters: framework narrows the catalog, tenant narrows the attestations. Honest empty state flagged when the catalog is empty. Owner-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
frameworkquerystringe.g. CSF 2.0 or ISO 27001:2022
tenantquerystring

Responses

  • 200 Catalog + attestations + by-control roll-up
  • 401 Missing or invalid bearer token
  • 403 Role not owner
POST/controlsAttest a control's implementation status for a tenant

Upserts one control_attestations row keyed on (control_id, tenant_id). Status must be one of implemented / partial / not_implemented / not_applicable; the referenced control must exist in the locked catalog (404 otherwise). The attesting operator and timestamp are stamped, and next_review_at is set one year out. Owner-only.

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
control_id requiredstring
tenant_id requiredstring
status requiredstringOne of implemented, partial, not_implemented, not_applicable
evidence_refstring

Responses

  • 200 Attestation upserted with one-year review horizon
  • 400 Non-JSON body, missing fields, or bad status value
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 control_id not present in the locked catalog

Directory

GET/directory-moderationList tenant-submitted directory listings awaiting moderation

Lists the newest 200 rows from directory_submissions with optional filters by listing type (rule_pack / agent / connector / assurance_card), tenant_id, status (pending / under_review / approved / rejected) and free-text q over submission_id / title. Returns a KPI block (pending queue depth, approvals and rejections in the last 7 days, average hours-to-decision) plus the distinct tenant list for the filter dropdown. Owner-only, read-only. Constitution §17 Directory Rail.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
typequerystring
tenant_idquerystring
statusquerystring
qquerystringLIKE on submission_id / title

Responses

  • 200 Submissions + moderation KPI + tenant filter values
  • 401 Missing or invalid bearer token
  • 403 Role not owner
POST/directory-moderationApprove or reject a directory submission

Decides one directory_submissions row: action must be approve or reject (reject accepts an optional reason, ≤ 1000 chars). The UPDATE only matches rows still in pending or under_review — a missing or already-decided submission returns 404, making the decision single-shot. The deciding operator and timestamp are stamped on the row. Owner-only.

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
submission_id requiredstring
action requiredstringOne of approve, reject
reasonstringRecorded as rejection_reason on reject

Responses

  • 200 Decision applied
  • 400 Non-JSON body, missing submission_id, or action not approve/reject
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Submission not found or already decided

DSAR

GET/dsarList Data Subject Access Requests with deadline KPI

Paginated view over dsar_requests (owned by the SQL migration tree; queries fail silent on a cold DB). status filters by open / assembly / released, while overdue maps to non-released rows past their statutory_deadline. Free-text q searches request_id / controller_id / subject_ref_hash / regime. Returns a KPI block (open, assembly, released, overdue) and a pagination envelope (page, page_size ≤ 100, total, total_pages). The 5-rule assembly pipeline and signing-suite construction are patent-track — only observable queue state is exposed. Owner-only, read-only. Schema: kye.dsar_evidence_pack.v1.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
statusquerystring
qquerystringLIKE on request_id / controller_id / subject_ref_hash / regime
pagequeryinteger
page_sizequeryinteger

Responses

  • 200 DSAR queue page + KPI
  • 401 Missing or invalid bearer token
  • 403 Role not owner

Onboarding

POST/onboarding/workflows/{id}/{action}Approve or reject an onboarding workflow

Transitions an onboarding_workflows row to approved or rejected, recording an onboarding_transitions audit row and (best-effort) emitting kye.admin.workflow.{approved,rejected}.v1 into the WORM audit chain (§0.3) plus a workflow.{approved, rejected} message onto KYE_LIFECYCLE_QUEUE so the provisioning agent acts on it. Idempotent — a workflow already in the target stage returns 200 with idempotent: true; a workflow in a DIFFERENT terminal stage (approved / rejected / active) returns 409 so approve-after-reject is impossible. Optional body fields: reason (≤ 500 chars) and operator (also read from the x-kye-operator-id header). Owner-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstring
action requiredpathstring

Request body

fieldtypedescription
reasonstring
operatorstringFalls back to the x-kye-operator-id header

Responses

  • 200 Transition applied (or idempotent no-op when already in target stage)
  • 400 Workflow id not kye:onboarding-workflow:* or action not approve/reject
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Workflow not found
  • 409 Workflow already in a different terminal stage
  • 500 D1 write failed mid-transition
GET/onboarding/workflowsOnboarding workflows filtered by lifecycle stage

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
stagequerystringLifecycle stage; `any` returns every stage.

Responses

  • 200 Workflows at the requested stage
  • 400 `bad_stage` — the response echoes the allowed set in `allowed`
  • 500 `db_query_failed`
  • 503 `db_binding_missing`

Reports

GET/reportsList sealed compliance reports for the caller's tenant

Returns the tenant's sealed kye.report.v1 envelopes, most recently sealed first (capped at 200), alongside two derived counters: how many were sealed since the start of the current calendar quarter, and how many distinct frameworks they cover. Tenant-scoped via the Clerk session; owner-gated by the middleware.

Read-only by construction. Sealing a report requires the Ed25519 signing seed and the immutable report bucket, and this surface holds neither binding — reports are sealed by the KYE™ Reporting Engine™ and merely read here. There is deliberately no POST: the one that used to stand here omitted three NOT NULL seal columns and wrote a headline_verdict the table's CHECK constraint rejects, so it could never write a row while reporting success to the operator.

An empty rows array means this tenant has no sealed reports, not that the query failed; the SELECTs return [] rather than throwing on a cold database.

Auth: Session (Clerk JWT)

Responses

  • 200 Sealed reports for the caller's tenant
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 503 KYE_DB binding missing
GET/analytics-widget-callsPer-widget call totals and success counts for the calling tenant

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
sincequerystringWindow start (default: until minus 7 days)
untilquerystringWindow end (default: now)

Responses

  • 200 Per-widget summary
  • 400 `invalid_since` · `invalid_until`
  • 503 `db_binding_missing`
GET/dashboard-statsCross-tenant operator dashboard headline figures

Counts are best-effort: a table that does not exist yet contributes 0 rather than failing the whole response.

Auth: Session (Clerk JWT)

Responses

  • 200 Headline figures as of the request
  • 503 `db_binding_missing`

RiskAudit

GET/risk-auditCross-tenant risk-assessment audit list

Lists the newest 500 rows from risk_assessments (owned by the SQL migration tree; created by the runtime engine, never by this console) with optional filters by tenant_id, risk tier (minimal / limited / high / unacceptable / prohibited), subject class, framework slug and free-text q over assessment_id / subject_id. Returns a 7-day KPI strip — prohibited and unacceptable verdicts, EU AI Act high+ floors, and DORA critical-or-important floors — plus the distinct tenant list for the filter dropdown. Owner-only, read-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenant_idquerystring
tierquerystring
subject_classquerystring
frameworkquerystring
qquerystringLIKE on assessment_id / subject_id

Responses

  • 200 Risk assessments + 7-day KPI
  • 401 Missing or invalid bearer token
  • 403 Role not owner

ScopesCatalog

GET/scopes-catalogList canonical scope entries with pin status

Lists scopes_catalog rows (pinned first, then newest) with optional filters by capability, jurisdiction and pinned state. Returns a KPI block — distinct scope count, pinned count, total tenant reuse (sum of tenant_count) and distinct jurisdiction count — plus the distinct capability list for the filter dropdown. Owner-only, read-only. Constitution §12 (scope triples) + §0 (single canonical per concept).

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
capabilityquerystring
jurisdictionquerystring
pinnedquerystring

Responses

  • 200 Scope entries + KPI + capability filter values
  • 401 Missing or invalid bearer token
  • 403 Role not owner
POST/scopes-catalogPin, unpin or create a canonical scope entry

Multiplexed by the op body field (default pin). pin / unpin toggle the pinned flag on an existing scope (404 when the scope_id is unknown), stamping the pinning operator and timestamp. create inserts a new scope with capability + optional jurisdiction (default GB) and dataset list; IDs not already in kye:scope:* form are minted as kye:scope:<capability-slug>.<jurisdiction>.<6-char-uuid>. Any other op returns 400 with the valid list. Owner-only.

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
opstringOne of pin, unpin, create
scope_id requiredstring
capabilitystringRequired when op=create
jurisdictionstring
datasetsarrayop=create only

Responses

  • 200 Pin state toggled
  • 201 Scope created (op=create)
  • 400 Non-JSON body, missing scope_id/capability, or unknown op
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 scope_id not found (pin / unpin)

Support

GET/supportList support tickets with status counts

Lists the newest 500 support_tickets rows (open tickets first) with optional exact-match filters by status and tenant. Returns a counts roll-up (open / in_progress / closed) across the full table and flags the honest empty state. Owner-only, read-only. Constitution §8 §3 (support tooling).

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
statusquerystring
tenantquerystring

Responses

  • 200 Tickets + status counts
  • 401 Missing or invalid bearer token
  • 403 Role not owner
POST/supportOpen a support ticket for a tenant

Inserts a support_tickets row in open status. Requires tenant_id and title; severity must be one of P0–P3 (default P3). The opening operator (Clerk session email) is recorded as opened_by and the ticket ID is minted as kye:support:<uuid>. Owner-only.

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
tenant_id requiredstring
title requiredstring
bodystring
severitystringOne of P0, P1, P2, P3

Responses

  • 201 Ticket opened
  • 400 Non-JSON body, missing tenant_id/title, or bad severity
  • 401 Missing or invalid bearer token
  • 403 Role not owner
POST/support/{id}/claimClaim a support ticket for the calling operator

Assigns the ticket to the caller (Clerk session email) and moves it to in_progress, stamping claimed_at. Idempotent for the same operator; a different operator re-claiming replaces the owner with a fresh claimed_at. Claiming a closed ticket returns 409. Owner-only. No request body.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstringsupport_tickets.id

Responses

  • 200 Ticket claimed and moved to in_progress
  • 400 Missing ticket id
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Ticket not found
  • 409 Ticket already closed
POST/support/{id}/closeClose a support ticket with a resolution note

Moves the ticket to closed, recording the required resolution (4–4000 chars) and closed_at. If the ticket was never claimed, the closing operator becomes the owner (COALESCE). Owner-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
id requiredpathstringsupport_tickets.id

Request body (required)

fieldtypedescription
resolution requiredstring

Responses

  • 200 Ticket closed
  • 400 Missing ticket id or resolution under 4 chars
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 404 Ticket not found

Transparency

GET/transparencyHash-chain rollup of the transparency log

Reads the transparency_log table (declared in migration 040_operating_models_and_transparency_log.sql — no runtime DDL, §16) newest-first with optional filters by tenant and entry kind and a limit capped at 500. Returns the chain head (latest seq + chain_hash), a per-kind tally, the total entry count and the honest empty-state flag. The log is populated by the self-audit daemon, the transparency-log appender and every agent that emits a signed artefact. Owner-only, read-only.

Auth: Session (Clerk JWT)

Parameters

nameintypedescription
tenantquerystring
kindquerystringentry_kind, e.g. self_audit_run or audit_batch
limitqueryinteger

Responses

  • 200 Chain head + per-kind tally + entries
  • 401 Missing or invalid bearer token
  • 403 Role not owner
POST/transparencyVerify the transparency chain via the private verifier worker

Accepts { "op": "verify" } only (any other op returns 400) and proxies the request to the private transparency-verifier worker bound as TRANSPARENCY_VERIFIER — the chain-reconstruction and integrity rules are patent-track, so this surface relays the verifier's response verbatim (status code included). When the binding is absent the endpoint returns 501 explaining how to enable it; an unreachable verifier returns 503. Owner-only.

Auth: Session (Clerk JWT)

Request body (required)

fieldtypedescription
op requiredstringOne of verify

Responses

  • 200 Verifier response relayed verbatim (shape owned by the private worker)
  • 400 op missing or not "verify"
  • 401 Missing or invalid bearer token
  • 403 Role not owner
  • 501 TRANSPARENCY_VERIFIER service binding not configured
  • 503 KYE_DB missing or verifier unreachable